Skip to content

fix(recap): render the suggestion ghost in the composer before Tab - #833

Merged
ericleepi314 merged 1 commit into
mainfrom
fix/recap-ghost-visibility
Aug 12, 2026
Merged

fix(recap): render the suggestion ghost in the composer before Tab#833
ericleepi314 merged 1 commit into
mainfrom
fix/recap-ghost-visibility

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

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

#828 armed the state correctly (Tab worked) but the ghost never painted. appLayout's placeholder gate keyed on composer.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}) in ui-tui/src/content/placeholders.ts:

  • busy → blank;
  • an armed suggestion wins whenever idle (mid-conversation included — the whole feature);
  • the static 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:

  1. the suggestion is visible on the composer row before any Tab, and
  2. after Tab, typing a marker appends to it (ghost became editable input; pre-Tab typing would replace it).

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 pyte and pty, node/dist presence, win32) — a missing pty on Windows is a clean collection-time skip, not an error (simulated with a shadowed module). pyte added to requirements.dev.txt and pyproject dev extras in lockstep. Plus 4 unit tests pinning the composerPlaceholder truth 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 pty would 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

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>
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Test Results

     2 files       2 suites   24m 13s ⏱️
10 066 tests 10 040 ✅ 16 💤 10 ❌
20 810 runs  20 748 ✅ 51 💤 11 ❌

For more details on these failures, see this check.

Results for commit 5fc454a.

♻️ This comment has been updated with latest results.

@ericleepi314
ericleepi314 merged commit 2ed1ff7 into main Aug 12, 2026
6 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant