Skip to content

feat(conditions): AskUserQuestion condition + ghost-render fix (PR-4) - #329

Merged
Juliusolsson05 merged 2 commits into
integration/conditions-frameworkfrom
feat/conditions-auq-module
Jun 22, 2026
Merged

Juliusolsson05 merged 2 commits into
integration/conditions-frameworkfrom
feat/conditions-auq-module

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

PR-4 of the conditions-framework stack — integrates Claude's AskUserQuestion picker with the modular conditions system and fixes the interrupted-question ghost render. Targets integration/conditions-framework.

Pairs with claude-code-headless#26 (gitlink bumped 9939c1c → a0eeb74), which makes the picker a first-class, deduped condition in the unified snapshot.

1. Ghost-render fix — the reported bug

"a question being interrupted might still render even tho we are like 4 messages past"

An AskUserQuestion tool_use blocks the agent until a tool_result lands. If the user interrupts instead of answering (Esc / sends a new prompt), no result is ever emitted, so the block stayed resultAt == null forever and BlockRow kept routing it to the live, clickable picker — it ghosted in the feed indefinitely.

foldEvent.ts turn_stopped now stamps resultAt on pending AskUserQuestion blocks. This is the transcript-backed dismissal — not screen-derived, so it can't flicker the way a screen-snapshot gate would. Scoped strictly to toolName === 'AskUserQuestion': every other pending tool can still resolve with a late result across a stop (stamping them would drop real output); AskUserQuestion is unique in that it cannot resolve after an interrupt.

2. Shared-union entry

claude.ask-user-question (+ ClaudeAskUserQuestionState / …Option) — the app-side mirror of the headless parser's shape, on this side of the submodule import wall.

3. Minimal renderer consumer

A 'QUESTION' dispatch attention badge when the live condition is present (selectors.ts). Purely informational — it cannot touch the inline picker's render gate (stays semantic, !resultAt) or its answering path, so it can't regress the working single-select flow.

Deliberately deferred to PR-5

The sendThenReparse answering driver (multi-select / free-text / multi-question) and the answerability gate that closes the documented "answered-via-terminal → stray digit" race. Those are coupled to the driver, not to this PR; building them here would half-wire the driver or risk regressing the working path.

Why no screen-snapshot shortcut

Presence/dismissal of the inline row stays semantic (the tool block's resultAt); the screen-derived condition is a secondary signal (badge now, answering affordances in PR-5). That split is the entire point of the framework over the old snap.askUserQuestion direction.

Verification

tsc -b (web + node): no new errors — 15 pre-existing failures in unrelated subsystems (dictation, pidusage, ghostJournal, …), all three touched files clean. Headless tsc --noEmit clean. Vendor-sync of conditions-core: zero drift.

⚠️ Merge order when authorized: claude-code-headless#26 first (with --merge, to keep the gitlink commit reachable), then re-point the gitlink if its merge SHA differs, then this PR.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits June 22, 2026 11:26
…app)

App-side half of PR-4. Pairs with claude-code-headless#26 (gitlink bumped to
a0eeb74) which makes the AskUserQuestion picker a first-class, deduped condition
in the unified snapshot.

Three changes:

1. Ghost-render fix (foldEvent turn_stopped). An AskUserQuestion tool_use blocks
   the agent until a tool_result lands. If the user INTERRUPTS instead of
   answering, no result is ever emitted, so the block stayed `resultAt == null`
   forever and BlockRow kept routing it to the live, clickable picker — it
   "ghosted" in the feed many messages later (the reported bug). turn_stopped
   now stamps `resultAt` on pending AskUserQuestion blocks, the transcript-backed
   dismissal (NOT screen-derived, so it can't flicker). Scoped STRICTLY to
   `toolName === 'AskUserQuestion'`: every other pending tool can still resolve
   with a late result across a stop, so stamping them would drop real output;
   AskUserQuestion is unique in that it cannot resolve after an interrupt.

2. Shared-union entry: `claude.ask-user-question` (+ ClaudeAskUserQuestionState /
   Option), the app-side mirror of the headless parser's shape — the type the
   wire snapshot is validated against on this side of the submodule import wall.

3. Minimal renderer consumer: a 'QUESTION' dispatch attention badge when the
   live condition is present (selectors.ts). Purely informational — it cannot
   touch the inline picker's render gate (stays semantic, `!resultAt`) or its
   answering path, so it can't regress the working single-select flow. The
   answerability gate + multi-step answering driver are PR-5.

Why no screen-snapshot shortcut: presence/dismissal of the inline row stays
SEMANTIC (the tool block's resultAt); the screen-derived condition is a
secondary signal only. That split is the point of the framework.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent-code PR must point at the merge commit that now exists on claude-code-headless main, not the pre-merge topic branch head. Keeping the gitlink on the integrated SHA avoids depending on a branch-only object after the submodule PR lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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