feat: add Daemon management UI and graceful degradation
Assist now continues working even if anvilsrv fails to start, and provides a Daemon management window for server control. Changes: - Assist no longer fails fatally if anvilsrv won't start - Logs ERROR and continues with disconnected state - Add Daemon command to open /AnviLLM/Daemon window - Daemon window shows server status with Start/Stop/Restart buttons - Add isConnected() checks to all 9P operations - Display friendly "not connected" message in main window - Auto-reconnect when starting daemon from Daemon window Daemon Window Features: - Shows current daemon status (RUNNING/NOT RUNNING) - Displays 9P socket path and existence - Shows connection state - Start button: runs `anvilsrv start` and reconnects - Stop button: runs `anvilsrv stop` and disconnects - Restart button: stops then starts - Get button: refreshes status display User Experience: - Can still use Assist UI features (Sandbox, etc.) without daemon - Session-related features show "not connected" errors - Daemon command provides easy server management - No more hard failures on startup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
825d527b46
commit
55ebd64677
|
|
@ -72,7 +72,7 @@ Type `Assist` in Acme and middle-click to open the `/AnviLLM/` session manager.
|
|||
|
||||
### Commands
|
||||
|
||||
**Main window** (`/AnviLLM/`) tag: `Get Attach Stop Restart Kill Refresh Alias Context Sandbox`
|
||||
**Main window** (`/AnviLLM/`) tag: `Get Attach Stop Restart Kill Refresh Alias Context Daemon Sandbox`
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
|
|
@ -85,6 +85,7 @@ Type `Assist` in Acme and middle-click to open the `/AnviLLM/` session manager.
|
|||
| `Attach <id>` | Open tmux terminal |
|
||||
| `Alias <id> <name>` | Name a session |
|
||||
| `Context <id>` | Edit context (prepended to prompts) |
|
||||
| `Daemon` | Manage anvilsrv daemon (start/stop/status) |
|
||||
| `Sandbox` | Configure sandbox settings |
|
||||
|
||||
Right-click a session ID to open its prompt window. Select text anywhere and 2-1 chord on a session ID for fire-and-forget prompts.
|
||||
|
|
|
|||
|
|
@ -64,23 +64,27 @@ func main() {
|
|||
log.Printf("anvilsrv not running, attempting to start...")
|
||||
startCmd := exec.Command("anvilsrv", "start")
|
||||
if err := startCmd.Run(); err != nil {
|
||||
log.Fatalf("Failed to start anvilsrv: %v", err)
|
||||
}
|
||||
|
||||
// Wait a moment for daemon to initialize
|
||||
for i := 0; i < 20; i++ {
|
||||
fs, err = connectToServer()
|
||||
if err == nil {
|
||||
break
|
||||
log.Printf("ERROR: Failed to start anvilsrv: %v", err)
|
||||
log.Printf("Continuing without daemon connection. Use 'Daemon' command to manage server.")
|
||||
} else {
|
||||
// Wait a moment for daemon to initialize
|
||||
for i := 0; i < 20; i++ {
|
||||
fs, err = connectToServer()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to anvilsrv after starting: %v", err)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to connect to anvilsrv after starting: %v", err)
|
||||
log.Printf("Continuing without daemon connection. Use 'Daemon' command to manage server.")
|
||||
}
|
||||
}
|
||||
}
|
||||
defer fs.Close()
|
||||
if fs != nil {
|
||||
defer fs.Close()
|
||||
}
|
||||
|
||||
// Start notification daemon in background
|
||||
notifyCmd := exec.Command("anvillm-notify")
|
||||
|
|
@ -100,7 +104,7 @@ func main() {
|
|||
defer w.CloseFiles()
|
||||
|
||||
w.Name(windowName)
|
||||
w.Write("tag", []byte("Get Attach Stop Restart Kill Refresh Alias Context Sandbox "))
|
||||
w.Write("tag", []byte("Get Attach Stop Restart Kill Refresh Alias Context Daemon Sandbox "))
|
||||
refreshList(w)
|
||||
w.Ctl("clean")
|
||||
|
||||
|
|
@ -247,6 +251,10 @@ func main() {
|
|||
if err := openSandboxWindow(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening sandbox window: %v\n", err)
|
||||
}
|
||||
case "Daemon":
|
||||
if err := openDaemonWindow(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening daemon window: %v\n", err)
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteEvent(e)
|
||||
|
|
@ -310,7 +318,14 @@ func connectToServer() (*client.Fsys, error) {
|
|||
return client.MountService("agent")
|
||||
}
|
||||
|
||||
func isConnected() bool {
|
||||
return fs != nil
|
||||
}
|
||||
|
||||
func createSession(backend, cwd string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv (use Daemon command to start server)")
|
||||
}
|
||||
// Validate and clean the path
|
||||
cleanPath := filepath.Clean(cwd)
|
||||
|
||||
|
|
@ -342,6 +357,9 @@ func createSession(backend, cwd string) error {
|
|||
}
|
||||
|
||||
func controlSession(id, cmd string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
path := filepath.Join(id, "ctl")
|
||||
fid, err := fs.Open(path, plan9.OWRITE)
|
||||
if err != nil {
|
||||
|
|
@ -354,6 +372,9 @@ func controlSession(id, cmd string) error {
|
|||
}
|
||||
|
||||
func sendPrompt(id, prompt string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
path := filepath.Join(id, "in")
|
||||
fid, err := fs.Open(path, plan9.OWRITE)
|
||||
if err != nil {
|
||||
|
|
@ -366,6 +387,9 @@ func sendPrompt(id, prompt string) error {
|
|||
}
|
||||
|
||||
func setAlias(id, alias string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
path := filepath.Join(id, "alias")
|
||||
fid, err := fs.Open(path, plan9.OWRITE)
|
||||
if err != nil {
|
||||
|
|
@ -410,6 +434,9 @@ func getSession(id string) (*SessionInfo, error) {
|
|||
}
|
||||
|
||||
func listSessions() ([]*SessionInfo, error) {
|
||||
if !isConnected() {
|
||||
return nil, fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
data, err := readFile("list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -447,6 +474,9 @@ func listSessions() ([]*SessionInfo, error) {
|
|||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
if !isConnected() {
|
||||
return nil, fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
fid, err := fs.Open(path, plan9.OREAD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -468,6 +498,9 @@ func readFile(path string) ([]byte, error) {
|
|||
}
|
||||
|
||||
func writeFile(path string, data []byte) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
fid, err := fs.Open(path, plan9.OWRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -479,14 +512,28 @@ func writeFile(path string, data []byte) error {
|
|||
}
|
||||
|
||||
func refreshList(w *acme.Win) {
|
||||
sessions, err := listSessions()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to list sessions: %v\n", err)
|
||||
var buf strings.Builder
|
||||
buf.WriteString("Backends: [Kiro] [Claude]\n\n")
|
||||
|
||||
if !isConnected() {
|
||||
buf.WriteString("Not connected to anvilsrv daemon.\n")
|
||||
buf.WriteString("Use 'Daemon' command to start the server.\n")
|
||||
w.Addr(",")
|
||||
w.Write("data", []byte(buf.String()))
|
||||
w.Ctl("clean")
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := listSessions()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to list sessions: %v\n", err)
|
||||
buf.WriteString("Error listing sessions: " + err.Error() + "\n")
|
||||
w.Addr(",")
|
||||
w.Write("data", []byte(buf.String()))
|
||||
w.Ctl("clean")
|
||||
return
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString("Backends: [Kiro] [Claude]\n\n")
|
||||
buf.WriteString(fmt.Sprintf("%-5s %-10s %-9s %-16s %s\n", "ID", "Backend", "State", "Alias", "Cwd"))
|
||||
buf.WriteString(fmt.Sprintf("%-5s %-10s %-9s %-16s %s\n", "-----", "----------", "---------", "----------------", strings.Repeat("-", 40)))
|
||||
|
||||
|
|
@ -775,3 +822,161 @@ func showSandboxStatus(w *acme.Win) {
|
|||
w.Addr("$")
|
||||
w.Write("data", []byte(buf.String()))
|
||||
}
|
||||
|
||||
// openDaemonWindow opens the daemon management window
|
||||
func openDaemonWindow() error {
|
||||
w, err := acme.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Name("/AnviLLM/Daemon")
|
||||
w.Write("tag", []byte("Get Stop Start Restart "))
|
||||
|
||||
go handleDaemonWindow(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleDaemonWindow(w *acme.Win) {
|
||||
defer w.CloseFiles()
|
||||
|
||||
// Load and display current status
|
||||
refreshDaemonWindow(w)
|
||||
|
||||
for e := range w.EventChan() {
|
||||
switch e.C2 {
|
||||
case 'x', 'X':
|
||||
cmd := string(e.Text)
|
||||
|
||||
switch cmd {
|
||||
case "Get":
|
||||
refreshDaemonWindow(w)
|
||||
case "Stop":
|
||||
stopDaemon(w)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
refreshDaemonWindow(w)
|
||||
case "Start":
|
||||
startDaemon(w)
|
||||
time.Sleep(1 * time.Second)
|
||||
refreshDaemonWindow(w)
|
||||
case "Restart":
|
||||
stopDaemon(w)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
startDaemon(w)
|
||||
time.Sleep(1 * time.Second)
|
||||
refreshDaemonWindow(w)
|
||||
default:
|
||||
w.WriteEvent(e)
|
||||
}
|
||||
default:
|
||||
w.WriteEvent(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshDaemonWindow(w *acme.Win) {
|
||||
var buf strings.Builder
|
||||
|
||||
buf.WriteString("AnviLLM Daemon Status\n")
|
||||
buf.WriteString(strings.Repeat("=", 60) + "\n\n")
|
||||
|
||||
// Check daemon status
|
||||
statusCmd := exec.Command("anvilsrv", "status")
|
||||
output, err := statusCmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
// Not running or error
|
||||
buf.WriteString("Status: NOT RUNNING\n")
|
||||
if len(output) > 0 {
|
||||
buf.WriteString(string(output))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
} else {
|
||||
// Running
|
||||
buf.WriteString("Status: RUNNING\n")
|
||||
buf.WriteString(string(output))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
// Check socket
|
||||
ns := client.Namespace()
|
||||
if ns != "" {
|
||||
sockPath := filepath.Join(ns, "agent")
|
||||
if _, err := os.Stat(sockPath); err == nil {
|
||||
buf.WriteString("9P Socket: " + sockPath + " (exists)\n")
|
||||
} else {
|
||||
buf.WriteString("9P Socket: " + sockPath + " (missing)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Check connection
|
||||
if fs != nil {
|
||||
buf.WriteString("Connection: CONNECTED\n")
|
||||
} else {
|
||||
buf.WriteString("Connection: DISCONNECTED\n")
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(strings.Repeat("-", 60) + "\n")
|
||||
buf.WriteString("Commands:\n")
|
||||
buf.WriteString(" Start - Start the daemon (anvilsrv start)\n")
|
||||
buf.WriteString(" Stop - Stop the daemon (anvilsrv stop)\n")
|
||||
buf.WriteString(" Restart - Restart the daemon\n")
|
||||
buf.WriteString(" Get - Refresh this window\n\n")
|
||||
|
||||
buf.WriteString("Note: Use 'anvilsrv fgstart' in terminal for debug logs.\n")
|
||||
|
||||
w.Addr(",")
|
||||
w.Write("data", []byte(buf.String()))
|
||||
w.Ctl("clean")
|
||||
}
|
||||
|
||||
func startDaemon(w *acme.Win) {
|
||||
w.Addr("$")
|
||||
w.Write("data", []byte("\nStarting daemon...\n"))
|
||||
|
||||
cmd := exec.Command("anvilsrv", "start")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
w.Write("data", []byte(fmt.Sprintf("Error: %v\n%s\n", err, output)))
|
||||
} else {
|
||||
w.Write("data", []byte("Daemon started successfully\n"))
|
||||
if len(output) > 0 {
|
||||
w.Write("data", []byte(string(output)+"\n"))
|
||||
}
|
||||
|
||||
// Try to reconnect
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if newFs, err := connectToServer(); err == nil {
|
||||
if fs != nil {
|
||||
fs.Close()
|
||||
}
|
||||
fs = newFs
|
||||
w.Write("data", []byte("Reconnected to daemon\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopDaemon(w *acme.Win) {
|
||||
w.Addr("$")
|
||||
w.Write("data", []byte("\nStopping daemon...\n"))
|
||||
|
||||
cmd := exec.Command("anvilsrv", "stop")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
w.Write("data", []byte(fmt.Sprintf("Error: %v\n%s\n", err, output)))
|
||||
} else {
|
||||
w.Write("data", []byte("Daemon stopped\n"))
|
||||
if len(output) > 0 {
|
||||
w.Write("data", []byte(string(output)+"\n"))
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
if fs != nil {
|
||||
fs.Close()
|
||||
fs = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue