ExtensionRunner.emit shared the generic 30s EXTENSION_HANDLER_TIMEOUT_MS budget with every event, including the fire-and-forget session_shutdown teardown event extensions cannot observe. A hung third-party handler — observed on Windows with omp-discord-presence 0.1.2 waiting on a stuck Discord IPC pipe — held AgentSession.dispose() for the full window, making Ctrl+C look ignored for 30s.
session_shutdown now uses a dedicated 2s SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS cap routed through a per-event handlerTimeoutForEvent() lookup so generic and shutdown budgets are independently configurable. The interactive-mode Ctrl+C path adds a defence-in-depth hard-exit: when isShuttingDown is true a fresh Ctrl+C exits with code 130 (the session JSONL has already been sync-flushed by the first press) instead of stacking another no-op shutdown() call.
Fixes#2600
Deferred MCP discovery wrote 'Connecting to MCP servers: …' straight to process.stderr while the TUI owned the terminal, overdrawing the chat input box border. onMCPConnecting now emits McpConnectingEvent on the mcp:connecting channel; InteractiveMode subscribes and renders it via showStatus (status container), mirroring the LSP-startup pattern. New mcp/startup-events.ts holds the channel, type, and formatMCPConnectingMessage.
- Added unified `omp setup speech` flow with JSON/check modes and model picker.
- Added local STT pipeline with sherpa workers, recorder/download flow, and streaming inference.
- Added local TTS pipeline with `omp say`, backend selection, and streaming vocalization.
- Replaced legacy speech settings with unified `speech`/`speechgen` configuration keys.
Reviewer flagged a race left open by the streaming guard added in #2455:
getUserInput() arms onInputCallback and schedules an 800 ms goal
continuation timer; when /goal set takes the streaming branch (or any
extension/hook starts a turn inside that window), the timer fired
unchecked. The downstream onInputCallback resolved the main waiter with
a goal-continuation submission, submitInteractiveInput called
session.promptCustomMessage without a streamingBehavior, and the same
AgentBusyError the PR set out to fix resurfaced.
Make the continuation timer streaming-aware: at fire time, bail out
when session.isStreaming || isCompacting || hasPostPromptWork is true.
Reuses the auto-submit busy check loop mode already relies on (renamed
#isLoopAutoSubmitBlocked -> #isAutoSubmitBlocked since both flows have
the same notion of 'agent is busy, do not submit'). The next agent_end
in #handleGoalSessionEvent reschedules normally.
Fixes#2454
Kept /plan <prompt> from paused plan mode on the prompted entry path while retaining the no-arg third-toggle exit.
Added regression coverage for paused plan mode resuming and submitting the prompt.
Fixes#2510
- Added an `isEmpty` getter on `AgentHubOverlayComponent` to report whether no subagent rows were loaded.
- Extended `showAgentHub` with an optional `requireContent` flag so the overlay is disposed early when empty under the double-<- path.
- Added tests for double-<- gating behavior with no subagents, with subagents, and explicit hub-open behavior.
- Moved the ctrl+p model-role cycle rendering from `showStatus` to a dedicated anchored cycle container above the editor.
- Updated InteractiveMode to rebuild the cycle container in place and auto-clear the track after 4 seconds.
- Added tests that validate no scrollback stacking, in-place replacement, and timer-based clearing behavior.
- Added session-domain modules and exports for session-entries, context, listing, loader, and migrations.
- Changed persistence to async append writes plus writeTextAtomic, removing sync line APIs.
- Added compaction-aware session context rebuild with dangling tool-call cleanup.
- Added resumable session resolution with status inference, id/stem/suffix matching, and backup recovery.
Addresses Copilot + Codex review on #2521:
- computeEditorMaxHeight returned 1 on terminals too small to host both the
editor and the chrome reserve, but the bordered editor never renders fewer
than 3 rows (2 border + 1 content). The cap now floors at that real minimum
(EDITOR_MIN_RENDERED_ROWS) so it no longer misreports the rows the editor
occupies; rendering is unchanged, and the contract is documented honestly
(reserve holds once terminalRows >= 7).
- #resolveOverlayLayout now always resolves maxHeight (?? availHeight), so the
maxHeight !== undefined branch in effectiveHeight, the composite slice guard,
and the number | undefined return type were dead. Tightened all three.
- Mirrored the maxHeight-default contract in the render stress oracle
(resolveExpectedOverlayLayout + compositeExpectedOverlays) so the randomized
sweep validates the real clipped geometry instead of the obsolete unclipped
one; updated the oracle helper test expectation accordingly.
Editor-height tests rewritten to assert the real contract (reserve when the
terminal can host both; pinned to the bordered minimum below that).
Addresses Codex review on #2520:
- Cancel path: `#approvePlan` returned on `compactOutcome === "cancelled"`
without restoring the deferred pre-plan model, stranding the next turn on
the plan model and leaking `#planModePreviousModelState`. The model
transition now runs for the cancelled outcome too (the operator aborted
only the compaction, not the approval) before the early return.
- Queue-flush ordering: `executeCompaction` flushes input queued during
compaction before returning, so the post-return model switch landed after
the queued turn began streaming (deferred one turn via #pendingModelSwitch).
Added a `beforeFlush(outcome)` hook to `executeCompaction`/`handleCompactCommand`;
`#approvePlan` runs the transition through it (and idempotently re-runs it
afterward to cover the message-count short-circuit).
Tests cover the cancel restore and the before-flush ordering.
Plan approval dispatched the executor's first synthetic prompt without
checking whether the agent was still streaming the post-resolve
continuation (or a turn started by the approve-time compaction/clear),
surfacing "Failed to finalize approved plan: ... Agent is already
processing". Loop auto-submit and goal continuations hit the same throw
via submitInteractiveInput, which always called prompt/promptCustomMessage
without a streamingBehavior.
#approvePlan now aborts any in-flight turn before the synthetic prompt,
and submitInteractiveInput routes submissions through the steer/follow-up
queue (streamingBehavior: "followUp") when the session is streaming.
Non-streaming call shapes are unchanged. Extends the manual-/goal fix
(#2454) to the continuation and plan-approval paths.
Two bounded TUI overlap fixes:
- Editor max-height (coding-agent): the [6,18] clamp's floor of 6 exceeded
available space on terminals <=18 rows, letting the editor crowd the
transcript/status. Extracted a pure computeEditorMaxHeight(rows) with an
EDITOR_MIN_CHROME_ROWS=4 upper bound; identical for rows >=18.
- Overlay overflow (tui): #resolveOverlayLayout left maxHeight undefined when
the option was unset, so a tall overlay's bottom rows were dropped
off-screen. maxHeight now defaults to availHeight so every overlay is
sliced to fit and re-clamps on resize.
When a plan is approved via "Approve and compact context", #exitPlanMode
restored the pre-plan model before the compaction summarizer ran, so the
request cold-missed the plan model's warm prompt cache. The model switch is
now deferred: compaction runs on the plan model, and the switch to the
execution (slider) or pre-plan model happens only after a successful
compaction. A failed compaction stays on the plan model; cancellation is
unchanged. Also clears any queued plan-role model switch when deferring the
restore so it cannot later clobber the restored model.
handlePlanModeCommand had branches for entering and pausing but fell
through to #enterPlanMode() when planModePaused was true, so once a
session entered plan mode it cycled forever between plan and plan_paused.
/goal and other mode-gated commands were permanently blocked because
they refuse to run while planModeEnabled || planModePaused is true.
Add a paused branch that clears the paused flag, resets the reentry
marker (so the next /plan logs as a fresh entry, not a resume), and
appends a mode_change to "none". Tools, model, and plan state were
already restored by the prior #exitPlanMode({ paused: true }), so no
extra cleanup is needed.
Fixes#2510
The whole-line editor decorate ran on `displayText` after the editor appended
the zero-width CURSOR_MARKER and cursor glyph; both start with ESC, so the
magic-keyword regex's right-boundary `(?!\S)` rejected `ultrathink` glued to
the marker and dropped the gradient until a trailing character was typed.
- `Editor.#decorate` (pi-tui) now splits around CURSOR_MARKER and decorates
each user-text segment independently, so word-boundary lookarounds resolve
correctly on both sides; the matching comment block is corrected.
- `KeywordHighlighter` / `highlightMagicKeywords` gain an optional `phase` in
[0, 1) that cyclically rotates the gradient stops. `0` (default) yields
the static palette, so sent-bubble rendering is unaffected.
- `CustomEditor.decorateText` derives `phase` from `Date.now()` and chains
`setTimeout(SHIMMER_FRAME_MS)` ticks while focused, the buffer holds a
magic keyword, and `magicKeywords.enabled` is on — the render itself
schedules the next frame, so losing focus, deleting the keyword, or
flipping the setting stops the animation on its own. `interactive-mode`
wires the repaint hook to `requestComponentRender(editor)` on construction
and after `setEditorComponent`.
- Adds `hasMagicKeyword(text)` (cheap prose-aware probe) and tests for the
seam fix, the phase cycle, the gating, and timer cleanup.
Fixes#2475
InteractiveMode.refreshSlashCommandState() built the autocomplete
provider from builtins, hook/custom/skill commands, and file-based
slash commands but never read session.promptTemplates, so templates
loaded from cwd/.omp/prompts/ and the agent prompts directory expanded
when typed manually yet never appeared in the / picker. Pass them
through alongside file commands; filter out templates whose names
collide with an existing command so the picker mirrors the runtime
expansion order (expandSlashCommand precedes expandPromptTemplate in
AgentSession.prompt).
Added a regression test that constructs InteractiveMode with a session
that carries a prompt template, spies on editor.setAutocompleteProvider,
and asserts the captured provider returns the template for both the
empty / menu and a fuzzy /rev prefix, plus the collision-dedup contract
against the builtin /exit.
Fixes#2462
- Replaced queued-message interrupt flow with session abort calls on empty submit and escape.
- Removed interrupting state and notifyInterrupting teardown paths from abort handling.
- Updated AgentSession queue operations to use shared steering and follow-up queue views.
- Propagated isAborting through session state and collab payloads to suppress late updates.
Stopped goal objective commands from resolving the interactive input waiter while the agent is already streaming. The goal context still uses steer immediately, and the next idle goal continuation submits the objective work without AgentBusyError spam.
Added goal-mode integration coverage for both initial and replacement objective commands during streaming.
Fixes#2454
- InputController now dispatched Esc to active viewSession operations, aborting compaction, handoff, and retry directly.
- Removed competing onEscape handler swaps across command and event controllers so overlapping auto/manual flow events no longer overwrote cancellation callbacks.
- Compaction now propagated fetch options and rethrew aborted signals so cancellations were not treated as remote failures.
- Added a SessionFocusController to switch transcript and input context between main and subagent sessions.
- Added agent-hub Enter activation and double-left return behavior for focused local agents.
- Added view-session-based event and render logic to avoid stale focus-session state.
- Added status-line focused agent display with ghost icon and focused-mode border dimming.
- Added detached spawn metadata to lifecycle, progress, and executor session payloads so task and evaluator runs can mark background jobs.
- Updated subagent session tracking and HUD rendering to only show active detached spawns.
- Extended HUD tests to verify non-detached sync and eval spawns are excluded while detached flags propagate through events.
- Moved completion/hint builder functions and BUILTIN_SLASH_COMMANDS materialization into slash-commands/builtin-registry.ts.
- Removed those builder definitions from extensibility/slash-commands and shifted type exports to the types module.
- Updated interactive-mode to import BUILTIN_SLASH_COMMANDS from builtin-registry instead of the extensibility wrapper.
Dynamically assigns segment colors by sweeping across the full HSV hue spectrum based on their track position. This ensures each segment has a distinct, predictable hue and removes the need for explicit `color` assignments.
Pressing Ctrl+T (toggle thinking-block visibility) — or any other path that calls rebuildChatFromMessages, e.g. the theme/preset selector — during the pre-streaming window after a submission cleared the just-submitted user message until the first assistant token arrived.
startPendingSubmission paints the user message optimistically before session.prompt(...) lands it in session entries; rebuildChatFromMessages reads buildTranscriptSessionContext() which has no record of it yet, so the rebuild erased it and the streaming-component re-add block in toggleThinkingBlockVisibility was a no-op (no stream yet).
Centralize the fix in rebuildChatFromMessages: after rendering the transcript, replay the in-flight optimistic user submission. EventController#handleMessageStart already clears optimisticUserMessageSignature once the real user message_start lands, so the replay is a no-op post-streaming and cannot duplicate. cancelPendingSubmission clears the signature and marks the submission cancelled before its own rebuild, so cancellation paths still wipe the message.
Fixes#2372
- Added an anchored Subagents HUD renderer that formats active subagent sessions as `Id: description` rows.
- Integrated a new interactive-mode container and observer-driven render path so the HUD updates with session events and clears when no subagents are active.
- Exported `formatTaskId` for shared HUD formatting and added tests covering active-only filtering, fallback task/progress text, and line truncation.
The root compose recorded seams last-writer-wins, so the status
container's new seam (mounted loader) overwrote the transcript's seam
whenever both reported in the same frame. With a volatile live block
(no commit-safe end) the boundary then moved below the transcript's
still-mutable rows and committed them as stale history — the same
duplication class this PR fixes for the loader, relocated upward.
Commits are prefix-only: the first child reporting a live region (plus
its own commit-safe extension) already bounds everything below it, so
the topmost seam now wins. Adds a two-reporter regression test and
documents the multi-reporter contract on NativeScrollbackLiveRegion.
Status line painted theme.statusLineBg across the bar, which renders as
visible dark/black blocks on terminals (Ghostty, transparent themes,
custom palettes) whose background does not match the theme's hardcoded
status-line color. Powerline end caps inherited the same fill so they
also stood out against the surrounding terminal.
The new statusLine.transparent appearance setting (default off) routes
the bar to the terminal's default background via the existing empty-bg
sentinel (\x1b[49m) and drops the powerline end caps, which need a
contrasting fill to bridge into the terminal.
Theme authors can opt in per-theme by setting statusLineBg to '' — the
existing empty-string sentinel already resolves to the same ANSI escape
the new toggle uses.
Fixes#2306
- Removed `StartupInput` pre-TUI buffering and replay, and read startup input directly each loop.
- Removed pre-startup model/provider/LSP discovery and startup handoff into session setup.
- Updated `WelcomeComponent` to use per-instance tips and show `No recent sessions` when empty.
- Documented task option removals and startup splash/input behavior changes in `CHANGELOG.md`.
The session-observer overlay is gone. The Agent Hub (ctrl+s, alt+a, or double-tap left arrow on an empty editor) presents one overlay with two views: a live registry table (status, unread irc count, current task, last activity; j/k to navigate, r to revive, x to abort/release) and per-agent chat (transcript + input line) — submitting revives a parked agent and steers it via the normal prompt path. Main is the ambient chat and stays out of the table.\n\nrenderInitialMessages no longer takes a prebuilt context: every redraw now reaches for AgentSession.buildTranscriptSessionContext() (full-history transcript with each compaction emitted inline at the point it fired, snapcompact frames re-attached on rebuild). UiHelpers drops the deferred-compaction render and the IRC autoreply branch that the new mailbox bus deprecated. CompactionSummaryMessage renders as a slim divider (── 📷 compacted · ctrl+o ──), expanding to the summary + snapcompact frame count. session-manager.buildSessionContext gains a { transcript: true } mode; an exported buildSessionContextFromFile() reads a session file without taking the writer lock so the hub chat view can tail any agent (parked or live). Theme picks up icon.camera + tool.irc symbol entries.
- Dark themes: warm hues (0-120), Light themes: cool hues (180-300)
- Collision check against 17 major theme colors with 10° threshold,
clamped to the hue band so it never drifts into an unrelated range
- Hex values stored at construction instead of round-tripping through
ANSI escape parsing (removes #ansiToHex)
- WCAG AA contrast bisection on light surfaces
- Per-session uniqueness via name hash within the hue band
- Added pre-TUI startup input capture that queued non-empty Enter submissions.
- Replayed queued startup submissions after initialization and injected startup editor text into InteractiveMode.
- Handled startup Ctrl+C to clear typed text and Ctrl+D to exit, with detach restoring terminal state.
- Extended TUI Editor `onSubmit` to accept `void | Promise<void>` for async handlers.