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:
@@ -57,6 +57,11 @@ type Emulator interface {
|
||||
// ActiveScreen reports whether we are on the primary or alternate buffer.
|
||||
ActiveScreen() (Screen, error)
|
||||
|
||||
// Title returns the most recently set window title (OSC 0/2). Returns
|
||||
// an empty string if no title has been set. Used by idle detection
|
||||
// for the osc_title_stability and osc_title_status strategies.
|
||||
Title() (string, error)
|
||||
|
||||
// ScrollViewportTop moves the viewport to the top of the scrollback.
|
||||
ScrollViewportTop() error
|
||||
|
||||
|
||||
@@ -544,6 +544,27 @@ func (e *GhosttyEmulator) Cursor() (CursorState, error) {
|
||||
return CursorState{Col: uint16(col), Row: uint16(row), Visible: bool(visible)}, nil
|
||||
}
|
||||
|
||||
// Title returns the most recent window title set by OSC 0/2 escape
|
||||
// sequences. The libghostty-vt API hands back a borrowed pointer that
|
||||
// stays valid only until the next vt_write/reset, so we copy out to a
|
||||
// Go string under the same mutex that gates writes. An empty string
|
||||
// (len=0) means no title has been set.
|
||||
func (e *GhosttyEmulator) Title() (string, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.closed {
|
||||
return "", errors.New("vt: emulator closed")
|
||||
}
|
||||
var s C.GhosttyString
|
||||
if rc := C.ghostty_terminal_get(e.term, C.GHOSTTY_TERMINAL_DATA_TITLE, unsafe.Pointer(&s)); rc != C.GHOSTTY_SUCCESS {
|
||||
return "", fmt.Errorf("vt: get title failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
if s.ptr == nil || s.len == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)), nil
|
||||
}
|
||||
|
||||
func (e *GhosttyEmulator) ActiveScreen() (Screen, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -24,6 +24,7 @@ func (e *GhosttyEmulator) SerializeVT() ([]byte, error) { return nil, errStub
|
||||
func (e *GhosttyEmulator) StyledScreenVT() ([]byte, error) { return nil, errStub }
|
||||
func (e *GhosttyEmulator) Cursor() (CursorState, error) { return CursorState{}, errStub }
|
||||
func (e *GhosttyEmulator) ActiveScreen() (Screen, error) { return 0, errStub }
|
||||
func (e *GhosttyEmulator) Title() (string, error) { return "", errStub }
|
||||
func (e *GhosttyEmulator) ScrollViewportTop() error { return errStub }
|
||||
func (e *GhosttyEmulator) ScrollViewportBottom() error { return errStub }
|
||||
func (e *GhosttyEmulator) ScrollViewportDelta(int) error { return errStub }
|
||||
|
||||
Reference in New Issue
Block a user