* fix(cli): render the OpenTUI confirmation as one surface and bound the transcript flood
The mem0 confirmation scenario exposed three stacked gaps in the OpenTUI
renderer: the awaiting-approval path never reached the transcript model,
the expanded dialog body overflowed a fixed alt-screen viewport, and the
confirming card duplicated the payload the dialog already carries.
Emit confirm/confirm-resolved transcript events from the real scheduler
awaiting path, render the expanded body as a tail window budgeted against
the terminal height (alt-screen has no scrollback to fall back to), let
the pending card yield its description to the dialog, and cap every tool
card description to a bounded head with ink's own hidden-tail vocabulary.
A shared config.initialize() once-guard stops submit-time "Chat not
initialized" races, and a batch where every call was cancelled now ends
the turn without a follow-up model request, matching the ink stream.
The mem0 e2e post-approval wait moves to request bodies so it does not
depend on raw pty bytes; both legs pass all five scenarios.
* fix(cli): auto-open the OpenTUI auth dialog at boot (U-6)
Ink's boot auto-open is two triggers: no auth type configured (useAuth
initial state) and the one-shot startup authError
(useInitializationAuthError); shouldOpenAuthDialog is dead in production
code. The OpenTUI entry now computes that request once and seeds the
shell's dialog state with it, and the auth dialog surfaces the startup
failure through its existing error surface. Also repairs the entry
fallback-contract tests, which had been failing since f6213a18cd because
their mock config lacked initialize for the shared once-guard.
* fix(cli): drain deferred OpenTUI update notifications on idle (G-3)
Ink flushes handleAutoUpdate's pendingNotifications when the turn returns
to idle; the OpenTUI entry only kept cleanup, so a mid-turn update notice
sat queued forever. Hold { cleanup, flush } in a ref and flush when
live.streaming flips false, mirroring AppContainer's idle effect. The
check bootstrap, banner slot, and registration were already renderer-
neutral on main — the ledger's G-3 gap was the drain only (Decision 9).
* fix(cli): route OpenTUI settings sub-dialogs and own the composer fill (U-9)
The mount's fillInput/onSelectSetting seams existed since Batch 4 but the
shell fed neither, so settings sub-dialog rows reported an unmounted
dialog and the arena picker's fill was reported lost. The shell now
routes ui.theme/editor and the fast/vision model rows like ink's
DialogManager, and fills the composer through the entry-owned handle via
the polling injector (the picker detaches the handle before remounting
the prompt). With an owner present a named selection no longer closes
afterwards — the owner replaces the request, and closing would clobber
the dialog it opens (Decision 10).
* feat(cli): OpenTUI follow-up suggestions with the ghost placeholder (U-7/G-2)
Port the ink follow-up suggestion feature: the entry generates on the
streaming→idle edge through a new useFollowupSuggestionGeneration hook
(ink's gate set verbatim; abort on every turn boundary and unmount), and
the composer consumes it via the shared renderer-neutral controller —
ghost placeholder, Tab/Right/Enter accept into the buffer without
submitting, typing dismisses, submit clears.
* fix(cli): correct WaitingCallInfo import source in follow-up generation
WaitingCallInfo lives in live-session.ts, not live-session-model.ts; the
wrong source only surfaced under tsc because the hook never calls the
type at runtime. Also give the parked-confirmation test case a
well-formed WaitingCallInfo and pin the it.each table to
Partial<FollowupGenerationParams> so the per-case unions typecheck.
* feat(cli): record OpenTUI steering hops for /resume (U-32)
Ink's steering accept() writes each surviving steered message through
recordMidTurnUserMessage so a session resume replays the mid-turn API
shape (user parts riding the same Content as tool results); the OpenTUI
port carried the user echo but never recorded, leaving steers invisible
to /resume. resolveSteeredPromptParts now collects per-message
recordings alongside the echo events — same membership as the echo, so
a declined message is neither echoed nor recorded — and the drain loop
records them at the commit point, after the restore check: a restored
(aborted) hop records nothing, keeping the all-or-nothing contract.
* fix(cli): join the OpenTUI queue Esc-restore with a blank line (U-11)
ink aggregates popped queue texts with '\n\n' everywhere
(aggregateUserMessages, getQueuedMessagesText), matching the model-side
steering join; the OpenTUI popQueue used a single newline. Align to the
blank line. ink's peer and slash-command queue filters have no OpenTUI
counterpart to port: the live queue only ever holds plain composer text
(slash commands defer in the shell instead of queueing) and peer
messaging has no OpenTUI queue.
* feat(cli): render the four command cards in the OpenTUI transcript (U-34)
advisor, away_recap, arena_agent_complete and arena_session_complete
previously projected to null, so /advisor, /arena and /recap output was
invisible on OpenTUI. Each kind now carries a structured stream event
through the fold into a dedicated transcript row mirroring the ink
component shapes (recap gutter with bold label and italic body, advisor
markdown card, arena status/token/tool lines and comparison sections).
Status colors map onto the live palette so rows track theme swaps; the
arena comparison helpers are mirrored from the ink card. tool_use_summary
and diff_stats stay explicit no-ops (no OpenTUI writer for either), and
the kinds remain live-only: neither renderer replays them on resume.
* feat(cli): add the OpenTUI ! shell mode (U-33)
Empty-buffer `!` toggles shell mode (Esc exits), the chrome shows the `!`
prefix, slash dispatch stays first, and a submission runs through a thin
executor over core's ShellExecutionService — a `$ ` command row plus a
synthetic run_shell_command card on the transcript, with the command+result
injected into the LLM history via ink's shared processor.
* fix(cli): close Batch 10 review-round findings on the OpenTUI surface
- Reconciler crash: stop nesting <text> inside <text> in the theme,
permissions, settings, and MCP dialogs (five sites, one mechanism).
- Shell mode: derive the card id from a UUID so two commands started in
the same millisecond cannot collide, and report generic tool-end
summaries so the `!` card no longer prints the command twice.
- Confirmation body: ignore ctrl-s on a body that already fits — the
expanded tail window can be shorter and would silently drop head rows.
- Transcript replay: pass mid_turn_user_message through to /resume while
side-band subtyped user records stay skipped.
- ensureConfigInitialized: keep a settled failure cached (core never
retries initialization) so later submits surface the real cause, and
correct the stale re-entry rationale in the comments and design doc.
- Tests: first mount coverage for transcript-view and transcript-adapter,
the idle-flush hook extraction with its own suite, shell-mode id
uniqueness, and the fitting-body ctrl-s guard; the gating spec's
turn-done marker moves onto the waitForScreen channel; revert an
unrelated SKILL.md formatting drive-by.
* fix(cli): close two round-2 review findings on the OpenTUI surface
- Restore the dim styling of the permission-scope description, which the
nested-text fix had flattened into the label color.
- Add a direct-drive pin for the cached rejected initialization: core flips
`initialized` before awaiting the flight, so a re-entry after a settled
failure reports "Config was already initialized" instead of the real cause.
The existing submit-path test masks this because its mock rejects with the
same error on every call.
* fix(cli): restore ink parity on the scope row and the theme preview diff
Two round-3 review corrections, each verified against ink's own code:
- The permission-scope label goes back to the single-string form
(`${label} ${description}`, one colour). ink's RadioButtonSelect
renders the regular path as a single <Text color={titleColor}> and
PermissionsDialog builds that label with four spaces, so the row-box
with a dim description diverged twice from ink; the nested-<text>
shape it replaced never rendered at all.
- THEME_PREVIEW_DIFF's hunk header declared two lines per side and
supplied one. ink's DiffRenderer tolerates that header; OpenTUI's
<diff> parser rejects the hunk and the preview pane rendered an error
string instead of the diff. Counts corrected to -1,1 +1,1; rendered
output is unchanged because neither renderer displays the header.
* fix(cli): land review round-3 on the OpenTUI surface (13 Criticals + 16 Suggestions)
Criticals: settings sub-dialog routing + arena composer fill owned by the
shell (U-9); confirm-resolved carries the approved/rejected outcome so a
No/Esc decline is not mislabeled; the mid-turn steering restore re-queues
texts when the abort lands behind a resolved hop; displayText replay and
attachment placeholder on /resume; tool-card description stays visible for
confirmation types the dialog does not carry; wide-char rows measured in
display columns for the content caps; followup ghost abort/dismiss split;
Esc in shell mode exits and interrupts in one keypress; status text shows
Shell mode; stale-dropdown guard for the `!` toggle; bidi stripping on
arena rows; boot-seeded auth error keeps Esc dismissible; pending-card
gating.
Suggestions: POSIX wrap closes its brace group on its own line (trailing
comments no longer swallow the wrapper), shell status summary is
cancelled for user aborts, shell lane reuses the dispatcher's admission
rule and passes cwd/signal/config through, pwd/history writes only on the
chat that started the command, `!` child width mirrors ink, expand guard
for short terminals, expanded tail keeps its hidden-rows label, in-place
settings flips invalidate the suggestion effect, MCP fixture pins both
name fields, design-doc coverage paragraphs updated to the extracted hook.
14 remaining Suggestion-level items are recorded as U-39 in #8662.
* test(cli): pin idle slash dispatch over the shell lane in shell mode (U-33)
The mid-turn tag and the drain's shell branch are pinned by the ?btw and
PATH cases; this adds the idle precedence half of the routing.
* test(integration): align mem0 long-confirmation oracle with both renderer legs
The expanded-state oracle asserted a whole-viewport absence of the
hidden-tail label, but OpenTUI's fixed alt-screen viewport keeps the
transcript's own capped copy of the payload (with its label) above the
dialog, so the negative check could never pass on that leg. Scope the
label assertion to the confirmation section there, and keep the
whole-screen form for ink, whose expanded dialog scrolls the section
heading out of the viewport. The OpenTUI leg also needs a taller PTY
(his expand guard only opens ctrl-s when the tail window reveals more
rows than the collapsed head), while ink derives its cap from terminal
height and must stay at the historical 38 rows for the bounded-view
oracle to trigger.
* fix(cli): honor the ctrl-s promise and keep pending payloads approvable
TextBody only offers ctrl-s when expansion actually reveals rows the
collapsed window hides, and labels the expanded view's dropped head rows
as first-N instead of last-N; a pending tool card keeps its own bounded
row budget instead of the settled 5-row cap, so the tail of the payload
being approved stays on screen (round-7 R5-1/R5-2/R5-9).
* fix(cli): hold plain prompts behind running shell commands and harden the shell lane
Four defects around the `!` shell lane and the deferred-command queue:
- A plain prompt typed while a `!` command was running or a slash
submission was streaming now waits in the deferred queue instead of
racing the stream, and is routed to the prompt turn when the lane
frees; slash-form input stays exempt because the dispatcher admits it
itself.
- A drain pause (a queued turn/dialog that owns the UI) no longer
re-arms the drain's completion bump, which used to run the queue
remainder before the turn or dialog it waits for even started; the
bump still re-arms for entries that arrive mid-drain.
- executeUserShell is async and its seam reports a rejection instead of
letting it strand the shell gate, and both chat-identity reads are
guarded so an uninitialized or swapped client can neither throw nor
inject a stale session's output into a fresh chat.
- One physical `!` keypress toggles shell mode once across press and
release events.
Pinned by unit tests in opentui-app-shell.test.tsx, shell-mode.test.ts,
input-prompt.test.tsx; every fix is mutation-verified.
* fix(cli): restore steering texts when the abort lands after the hop
An abort that arrives after the streaming hop already resolved used to
fall through the same path as a normal end-of-turn, so the steering
texts the model never saw were consumed without ever reaching it. The
branch now restores the steering queue and only writes the collected
function responses back to history, mirroring the pre-hop abort path.
* fix(cli): let Esc close the auth dialog when boot already seeded the error
When the boot flow failed before any auth type existed, it opened the
dialog with the diagnostic already set. Esc ran the normal handler,
which fell through to the unauthenticated arm, overwrote the boot
diagnostic with the must-connect message and wedged the dialog shut.
Esc now closes first when the displayed error is the boot's own.
* fix(cli): close a dangling line continuation before the shell terminator (R6-8)
A `!` command ending in an odd run of backslashes escaped the appended
`;` into a literal argument, so the wrapped execution ran a command the
user never wrote (`ls \` became `ls ';'`). Close the continuation with a
newline before appending the terminator, in both the ink and OpenTUI
shell lanes.
* fix(cli): carry held-prompt attachments through the drain and re-check the gate between entries (R5-5', R6-4)
Round-5's fix rerouted prompts queued behind a running `!` command into
the drain, but dropped their image attachments on the way: the queue
entry carried only the text. Store the attachments on the entry and pass
them back to onSubmitPrompt at drain time.
And the drain checked the holdsUi gate once before the loop, so an entry
awaiting inside the loop (a `!` command finishing, a dialog opening)
let the remaining entries run against a gate that just closed. Re-read
the gate before each entry and re-queue the remainder paused — same
shape as the holdsUi branch, so the dep change that closed the gate is
what resumes the batch.
* fix(cli): write the completed batch to history on both abort windows, gate mid-turn recordings on delivery, and gate onComplete on the signal (R6-5, R6-7, R6-6)
The R5-4 history write covered only the abort-after-clean-hop window: an
abort landing inside the @-read hop returned restore() and fell through
with no addHistory and no return, so the loop sent a continuation on the
dead signal and dropped the batch's function responses. Hoist the abort
check above the restore branch so both windows write history and return.
The U-32 mid-turn recording was written before the continuation send was
delivered — a send that throws would persist a mid-turn user message the
model never saw and never hand the drained texts back. Stash the
recordings at the sampling boundary and flush them only once the
continuation send yields or completes cleanly; on a throw or an abort
before that, restore the raw texts to the queue instead (ink records
from onDelivered).
And the abort paths inside the prompt generator end with a normal
return, so runTurn's seq guard alone fired a command's onComplete for a
turn the user cancelled with Esc — gate it on the abort signal too. A
decline of every confirmation without Esc stays a completed turn.
* fix(cli): dismiss submitOnAccept ghosts, fold multiline card descriptions, re-arm early-input injection (R6-1, R6-2, R6-3)
* fix(cli): delete unreachable textarea submit route (global Enter handler always wins)
* fix(cli): budget a pending tool card below the confirmation dialog footprint
The confirmation dialog renders in flow beneath the transcript on OpenTUI's
fixed alt-screen viewport (ink scrolls overflow into scrollback). A pending
card left at maxHistoryItemRows (terminalHeight * 4 — 320 rows at h=80)
pushed the dialog's outcome list, hidden-lines label, and ctrl-s hint off
screen: the mem0 interactive e2e timed out waiting for the hint three runs
in a row. The pending description budget is now viewport-aware
(pendingCardMaxRows) and reserves room for the dialog, the transcript rows
above the card, and the card's own chrome; short payloads render in full
(R5-9 preserved) and short terminals fall back to the settled cap.
* fix(cli): budget a pending card for the dialog's expanded payload
The pending-card budget bounded only the dialog's collapsed footprint, so
the mem0 confirmation (whose dialog body duplicates the card's ~3.9k-char
description) expanded past the 80-row viewport and its tail never came on
screen. pendingCardMaxRows now also reserves rows for the payload the
dialog will render, estimated from the card's own description width; the
collapsed-dialog bound keeps MCP-native cards (dialogs without args)
generous, and short terminals still fall back to the settled cap.