fix(recap): render the suggestion ghost in the composer before Tab - #833
Merged
Conversation
The recap suggestion (#828) armed correctly — Tab inserted it — but the ghost never painted: appLayout's placeholder gate keyed on `composer.empty`, which is CONVERSATION-emptiness (useMainApp: `!historyItems.some(m => m.kind !== 'intro')`), not input-emptiness. So the slot was blanked in the only state where a suggestion exists (mid-conversation), and every state-level test still passed. The slot selection now lives in `composerPlaceholder({busy, conversationEmpty, pendingSuggestion})`: busy blanks it, an armed suggestion wins whenever idle, and the static `Try "..."` hint stays fresh-conversation-only (historical behavior byte-preserved). Input-emptiness was never this gate's concern — TextInput itself hides any placeholder once the input has text. Regression coverage at the level that would have caught this: a new screen-level e2e (tests/test_tui_recap_ghost_e2e.py) runs the REAL built TUI in a POSIX pty against a deterministic fake agent-server and reads the terminal with pyte — asserting the suggestion is VISIBLE before any Tab, and that Tab + typing APPENDS (ghost became editable input). Mutation-verified in both directions: with the fix stashed and dist rebuilt it fails exactly on the pre-Tab ghost assertion. Skip-guarded via importorskip (pyte, pty) + node/dist checks — a missing `pty` on Windows is a clean skip, not a collection error (simulated). pyte added to requirements.dev.txt + pyproject dev extras in lockstep. Plus 4 unit tests pinning the composerPlaceholder truth table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test Results 2 files 2 suites 24m 13s ⏱️ For more details on these failures, see this check. Results for commit 5fc454a. ♻️ This comment has been updated with latest results. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Follow-up to #828 (end-of-turn recap + Tab suggestion), fixing a user-reported gap: the suggested next prompt now appears as ghost text in the composer before Tab is pressed — inverted cursor block on the first character, dim text after it, exactly the CC behavior. Tab then accepts it into real, editable input.
Root cause
#828armed the state correctly (Tab worked) but the ghost never painted. appLayout's placeholder gate keyed oncomposer.empty— which is conversation-emptiness (useMainApp.ts: !historyItems.some(m => m.kind !== 'intro')), not input-emptiness — so the ghost slot was blanked in the only state where a suggestion can exist: mid-conversation. Every state-level test passed, because the bug lived purely in the render gate.Fix
The slot selection is now an exported, tested function —
composerPlaceholder({busy, conversationEmpty, pendingSuggestion})inui-tui/src/content/placeholders.ts:Try "…"hint stays fresh-conversation-only (historical behavior unchanged).Input-emptiness was never this gate's concern: TextInput already hides any placeholder the moment the input has text (which is also why typing dismisses the ghost and deleting back to empty restores it).
Regression coverage at the level that would have caught this
New screen-level e2e (
tests/test_tui_recap_ghost_e2e.py): runs the real built TUI (ui-tui/dist/entry.js) in a POSIX pty against a deterministic fake agent-server (inline NDJSON: init → stream → result → recap frame with a fixed suggestion) and reads the terminal with pyte, asserting:Mutation-verified both directions: with the fix stashed and dist rebuilt, the e2e fails precisely on "suggestion ghost did not render before Tab"; with the fix, it passes (~8s). Skip-guarded (importorskip for
pyteandpty, node/dist presence, win32) — a missingptyon Windows is a clean collection-time skip, not an error (simulated with a shadowed module).pyteadded torequirements.dev.txtand pyproject dev extras in lockstep. Plus 4 unit tests pinning thecomposerPlaceholdertruth table (19 in the recap suite, 35 across recap+tab-accept suites).Review
Adversarial critic pass: REVISE → all findings applied (the blocking one: top-level
import ptywould have been a pytest collection error on windows-latest CI once pyte installs there; now importorskip) → re-verified → APPROVE. Typecheck clean, eslint zero new findings, no TypeScript behavior touched beyond the gate.🤖 Generated with Claude Code