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:
@@ -40,9 +40,42 @@ type Preset struct {
|
||||
Shell bool `json:"shell,omitempty"`
|
||||
|
||||
// Agent-only. SPEC §10.
|
||||
MCPInjection *MCPInjection `json:"mcp_injection,omitempty"`
|
||||
ReadySignal *ReadySignal `json:"ready_signal,omitempty"`
|
||||
ChromeTrimHints []string `json:"chrome_trim_hints,omitempty"`
|
||||
MCPInjection *MCPInjection `json:"mcp_injection,omitempty"`
|
||||
ReadySignal *ReadySignal `json:"ready_signal,omitempty"`
|
||||
ChromeTrimHints []string `json:"chrome_trim_hints,omitempty"`
|
||||
IdleDetection *IdleDetection `json:"idle_detection,omitempty"`
|
||||
}
|
||||
|
||||
// IdleDetection configures steady-state idle classification for an
|
||||
// agent preset. Independent of ReadySignal (which is startup-only).
|
||||
// All fields are optional; when the whole block is nil the runtime
|
||||
// falls back to output_activity with a 2s threshold.
|
||||
//
|
||||
// Strategy selects the primary signal:
|
||||
// - "output_activity": ms since last PTY output (Claude, OpenCode).
|
||||
// - "osc_title_stability": ms since last OSC 0/2 title change
|
||||
// (Codex, Amp — title changes mean activity).
|
||||
// - "osc_title_status": substring-match the current title against
|
||||
// TitleStatusMap (Gemini — title carries a status word).
|
||||
//
|
||||
// Promoter patterns are applied on top of the strategy. They run
|
||||
// against the recent ring-buffer tail; the first match wins in
|
||||
// error > permission > thinking precedence and promotes the state
|
||||
// over whatever the strategy returned.
|
||||
type IdleDetection struct {
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
IdleThresholdMS int `json:"idle_threshold_ms,omitempty"`
|
||||
|
||||
// TitleStatusMap maps a (case-insensitive) substring of the OSC
|
||||
// title to a state. Only meaningful for "osc_title_status".
|
||||
// Allowed values: "idle", "working", "thinking", "permission", "error".
|
||||
TitleStatusMap map[string]string `json:"title_status_map,omitempty"`
|
||||
|
||||
// Output regex promoters. Compiled at load time; bad patterns are
|
||||
// surfaced as warnings and skipped.
|
||||
PermissionPatterns []string `json:"permission_patterns,omitempty"`
|
||||
ThinkingPatterns []string `json:"thinking_patterns,omitempty"`
|
||||
ErrorPatterns []string `json:"error_patterns,omitempty"`
|
||||
}
|
||||
|
||||
// MCPInjection covers the strategies SPEC §10 enumerates plus
|
||||
@@ -196,6 +229,15 @@ func ensureDefaults(base string) error {
|
||||
"argv": ["claude"],
|
||||
"mcp_injection": { "kind": "flag", "flag": "--mcp-config" },
|
||||
"ready_signal": { "idle_ms": 1000 },
|
||||
"idle_detection": {
|
||||
"strategy": "output_activity",
|
||||
"idle_threshold_ms": 2000,
|
||||
"permission_patterns": [
|
||||
"Do you want to proceed\\?",
|
||||
"❯ 1\\. Yes",
|
||||
"1\\. Yes, and don't ask"
|
||||
]
|
||||
},
|
||||
"chrome_trim_hints": [
|
||||
"^Welcome to Claude Code",
|
||||
"^/help for help",
|
||||
@@ -220,6 +262,10 @@ func ensureDefaults(base string) error {
|
||||
"format": "toml"
|
||||
},
|
||||
"ready_signal": { "idle_ms": 1000 },
|
||||
"idle_detection": {
|
||||
"strategy": "osc_title_stability",
|
||||
"idle_threshold_ms": 2000
|
||||
},
|
||||
"chrome_trim_hints": [
|
||||
"^OpenAI Codex",
|
||||
"^\\s*model:",
|
||||
@@ -243,6 +289,10 @@ func ensureDefaults(base string) error {
|
||||
"var": "OPENCODE_CONFIG_CONTENT"
|
||||
},
|
||||
"ready_signal": { "idle_ms": 1000 },
|
||||
"idle_detection": {
|
||||
"strategy": "output_activity",
|
||||
"idle_threshold_ms": 2000
|
||||
},
|
||||
"chrome_trim_hints": [
|
||||
"^\\s*█",
|
||||
"^\\s*opencode v",
|
||||
|
||||
Reference in New Issue
Block a user