feat(02-03): client socket, autostart daemon, switch handler, workspace wiring

- Client struct sends JSON requests to daemon over Unix socket
- EnsureDaemon auto-starts the daemon if not running (retry 50ms x 20)
- Switch handler uses FuzzyMatch + SwitchToWorkspace via i3 IPC
- InitWorkspaceResolver wires BuildTerminalWorkspaceMap + ResolveWorkspace
- sysattr_linux.go for Setsid detach on daemon spawn
This commit is contained in:
Pierre Martin
2026-03-23 17:51:31 +01:00
parent a388c9477d
commit a79a0e154c
4 changed files with 340 additions and 0 deletions

View File

@@ -151,6 +151,7 @@ type Daemon struct {
procDir string
claudeDir string
workspaceResolver func(claudePID int) string // nil = no workspace resolution
i3commander I3Commander
pollInterval time.Duration
stopCh chan struct{}
listener net.Listener
@@ -169,6 +170,21 @@ func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon
}
}
// InitWorkspaceResolver sets up i3+X11 workspace resolution.
// If i3 or X11 is unavailable, logs a warning and continues without workspace info.
func (d *Daemon) InitWorkspaceResolver(treeProvider I3TreeProvider, x11 X11PIDResolver) {
if treeProvider == nil || x11 == nil {
return
}
d.workspaceResolver = func(claudePID int) string {
termMap, err := BuildTerminalWorkspaceMap(treeProvider, x11)
if err != nil {
return ""
}
return ResolveWorkspace(d.procDir, claudePID, termMap)
}
}
// Start runs the daemon: initial scan, then listens on the Unix socket
// and polls for sessions in the background.
func (d *Daemon) Start() error {
@@ -329,6 +345,32 @@ func (d *Daemon) handleConnection(conn net.Conn) {
d.registry.mu.Unlock()
writeResponse(conn, Response{OK: true})
case "switch":
var args SwitchArgs
if err := json.Unmarshal(req.Args, &args); err != nil {
writeResponse(conn, Response{Error: "invalid switch args: " + err.Error()})
return
}
sessions := d.registry.List()
match := FuzzyMatch(args.Query, sessions)
if match == nil {
writeResponse(conn, Response{Error: "no session matching: " + args.Query})
return
}
if match.Workspace == "" {
writeResponse(conn, Response{Error: "no workspace for session " + match.SessionID})
return
}
if d.i3commander == nil {
writeResponse(conn, Response{Error: "i3 commander not available"})
return
}
if err := SwitchToWorkspace(d.i3commander, match.Workspace); err != nil {
writeResponse(conn, Response{Error: "switch workspace: " + err.Error()})
return
}
writeResponse(conn, Response{OK: true})
case "stop":
writeResponse(conn, Response{OK: true})
d.Stop()