Add idle-state classifier and Solo-parity timer tools
Classifies every running child as idle/working/thinking/permission/error using one of three pluggable strategies (output_activity, osc_title_stability, osc_title_status) plus optional regex promoters applied to the tail of recent output. State and last-match reason are exposed via MCP on ProcessInfo and get_process_status. Per-preset configuration lives on a new preset.IdleDetection block with bundled defaults for the first-party claude/codex/opencode presets. OSC title plumbing is exposed as Emulator.Title(), polled from the session pump after each emulator write so title-change activity feeds into the classifier without an extra cgo callback. The MCP timer surface expands to match Solo: timer_set, timer_fire_when_idle_any/all, timer_cancel, timer_pause, timer_resume, timer_list. timer_wait is now a thin wrapper that shares the same manager so it shows up in timer_list while pending. Timer bodies are delivered to the owner process through the existing InjectAsOrchestrator path. Top-level (non-agent) callers can attach timers to a specific process via owner_process_id; omitting it grants universal cancel/pause/resume/list privileges. The sidebar gains a state glyph per process row and appends a nearest-timer indicator when one is pending or paused. Tests: idle_test.go covers the classify() pure function across the three strategies and regex promotion; timers_test.go covers the manager. Harness scenarios cover output_activity, osc_title_stability, osc_title_status, and regex promotion, plus timer_set delivery, cancel, pause/resume, idle_any-on-transition, idle_all-pending, and idle_all-already-satisfied. A new wait_until_mcp harness step type polls an MCP method until an assertion holds.
This commit is contained in:
@@ -140,6 +140,7 @@ func (l *Launcher) LaunchAgent(p *preset.Preset, displayName, initialPrompt, par
|
||||
cleanup()
|
||||
return nil, err
|
||||
}
|
||||
c.setIdleDetection(resolveIdleDetection(p.IdleDetection))
|
||||
|
||||
// Wait for the preset's ready signal, then type the initial prompt.
|
||||
idle := time.Duration(1000) * time.Millisecond
|
||||
@@ -171,7 +172,7 @@ func (l *Launcher) LaunchCommandPreset(p *preset.Preset, displayName, parentID s
|
||||
env = append(env, k+"="+v)
|
||||
}
|
||||
cols, rows := l.size()
|
||||
return l.sess.Spawn(SpawnSpec{
|
||||
c, err := l.sess.Spawn(SpawnSpec{
|
||||
Kind: KindCommand,
|
||||
Argv: p.ResolvedArgv(),
|
||||
Env: env,
|
||||
@@ -180,6 +181,11 @@ func (l *Launcher) LaunchCommandPreset(p *preset.Preset, displayName, parentID s
|
||||
WorkDir: p.WorkingDir,
|
||||
PresetRef: p.Name,
|
||||
}, cols, rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.setIdleDetection(resolveIdleDetection(p.IdleDetection))
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// LaunchCommandArgv spawns a freeform-argv command entry. Trust gating
|
||||
|
||||
Reference in New Issue
Block a user