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:
@@ -73,6 +73,14 @@ func booleanProp(desc string) map[string]any {
|
||||
return map[string]any{"type": "boolean", "description": desc}
|
||||
}
|
||||
|
||||
func arrayOfStringsProp(desc string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "array",
|
||||
"description": desc,
|
||||
"items": map[string]any{"type": "string"},
|
||||
}
|
||||
}
|
||||
|
||||
// toolCatalog is the full list advertised via tools/list. Descriptions
|
||||
// are intentionally short — clients are expected to fetch help() for
|
||||
// detail. Schemas mirror the param structs in tools.go.
|
||||
@@ -239,12 +247,70 @@ func toolCatalog() []toolDescriptor {
|
||||
},
|
||||
{
|
||||
Name: "timer_wait",
|
||||
Description: "Sleep server-side for `seconds` and return a timer id (use to pace polling).",
|
||||
Description: "Schedule a delay timer that injects a fixed `[system]` line into your pane when it fires (legacy; prefer timer_set).",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"seconds": numberProp("Sleep duration."),
|
||||
"seconds": numberProp("Delay duration."),
|
||||
"label": stringProp("Optional label for diagnostics."),
|
||||
}, []string{"seconds"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_set",
|
||||
Description: "Schedule a one-shot delay timer that delivers `body` to the owning agent as a fresh user turn when it fires.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"seconds": numberProp("Delay duration."),
|
||||
"body": stringProp("Message delivered verbatim to the owning agent as a user turn when the timer fires."),
|
||||
"label": stringProp("Optional label for diagnostics."),
|
||||
"owner_process_id": stringProp("Owner process id; defaults to the caller. Top-level callers must supply this explicitly."),
|
||||
}, []string{"seconds", "body"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_fire_when_idle_any",
|
||||
Description: "Schedule a timer that fires when any watched process enters idle (already-idle entries excluded), or when max_wait_seconds elapses.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"watched": arrayOfStringsProp("Process ids to watch."),
|
||||
"body": stringProp("Message delivered verbatim to the owning agent when the timer fires."),
|
||||
"label": stringProp("Optional label for diagnostics."),
|
||||
"max_wait_seconds": numberProp("Optional cap; 0 means no fallback fire."),
|
||||
"owner_process_id": stringProp("Owner process id; defaults to the caller."),
|
||||
}, []string{"watched", "body"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_fire_when_idle_all",
|
||||
Description: "Schedule a timer that fires when all watched processes are idle (already-idle entries count as satisfied), or when max_wait_seconds elapses.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"watched": arrayOfStringsProp("Process ids to watch."),
|
||||
"body": stringProp("Message delivered verbatim to the owning agent when the timer fires."),
|
||||
"label": stringProp("Optional label for diagnostics."),
|
||||
"max_wait_seconds": numberProp("Optional cap; 0 means no fallback fire."),
|
||||
"owner_process_id": stringProp("Owner process id; defaults to the caller."),
|
||||
}, []string{"watched", "body"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_cancel",
|
||||
Description: "Cancel one pending timer owned by the caller.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"timer_id": stringProp("Timer id returned by a previous timer_* call."),
|
||||
}, []string{"timer_id"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_pause",
|
||||
Description: "Pause one pending timer owned by the caller. Idle-aware timers stop listening to state changes; delay timers preserve their remaining wait.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"timer_id": stringProp("Timer id."),
|
||||
}, []string{"timer_id"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_resume",
|
||||
Description: "Resume one paused timer owned by the caller.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"timer_id": stringProp("Timer id."),
|
||||
}, []string{"timer_id"}),
|
||||
},
|
||||
{
|
||||
Name: "timer_list",
|
||||
Description: "List pending and paused timers owned by the caller.",
|
||||
InputSchema: objectSchema(nil, nil),
|
||||
},
|
||||
{
|
||||
Name: "scratchpad_list",
|
||||
Description: "List shared per-project scratchpad entries.",
|
||||
|
||||
Reference in New Issue
Block a user