From 0e45125b264bd36fbbbb8ce299a6c6e38c223715 Mon Sep 17 00:00:00 2001 From: Max schwenk Date: Thu, 12 Nov 2026 13:39:59 -0500 Subject: [PATCH] fix: Claude question previews render monospace and the chosen one reaches Claude Claude Code's AskUserQuestion tells the model each option preview renders as markdown in a monospace box, so agents put ASCII mockups and message drafts there. T3 rendered them in the proportional chat font with collapsed spaces, which scrambled mockups, and the answer sent back to Claude dropped `annotations`, so Claude never got the "selected preview:" line its tool result carries. Previews now render in a monospace, code-block-styled box whose paragraphs keep their spaces and newlines. The Claude adapter sends `annotations[question].preview` when the picked option has a preview and leaves `updatedInput` unchanged otherwise. --- .../Adapters/ClaudeAdapterV2.test.ts | 42 +++++++++++-------- .../Adapters/ClaudeAdapterV2.ts | 20 ++++++++- .../chat/ComposerPendingUserInputPanel.tsx | 16 +++---- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 95432c40e6887..da77dea4db126 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -421,23 +421,9 @@ describe("ClaudeAdapterV2 runtime query policy", () => { it("projects AskUserQuestion input with question text as the answer key", () => { const preview = "\n **Proposed change**\n\n```ts\n\tconst enabled = true;\n```\n "; - assert.deepEqual( - ClaudeAdapterV2.claudeUserInputQuestions({ - questions: [ - { - header: "Approach", - question: "Which approach?", - options: [ - { label: "Simple", description: "Use fewer moving parts", preview }, - { label: "Safe", description: "Keep the current setup" }, - ], - multiSelect: true, - }, - ], - }), - [ + const questions = ClaudeAdapterV2.claudeUserInputQuestions({ + questions: [ { - id: "Which approach?", header: "Approach", question: "Which approach?", options: [ @@ -447,7 +433,19 @@ describe("ClaudeAdapterV2 runtime query policy", () => { multiSelect: true, }, ], - ); + }); + assert.deepEqual(questions, [ + { + id: "Which approach?", + header: "Approach", + question: "Which approach?", + options: [ + { label: "Simple", description: "Use fewer moving parts", preview }, + { label: "Safe", description: "Keep the current setup" }, + ], + multiSelect: true, + }, + ]); const sdkAnswers: NonNullable = ClaudeAdapterV2.claudeSdkUserInputAnswers({ "Which approach?": ["Simple", "Safe"], @@ -457,6 +455,16 @@ describe("ClaudeAdapterV2 runtime query policy", () => { "Which approach?": "Simple, Safe", "Deploy now?": "Yes", }); + assert.deepEqual( + ClaudeAdapterV2.claudeSdkUserInputAnnotations(questions, { "Which approach?": "Simple" }), + { "Which approach?": { preview } }, + ); + for (const answer of ["Safe", "Something else entirely"]) { + assert.deepEqual( + ClaudeAdapterV2.claudeSdkUserInputAnnotations(questions, { "Which approach?": answer }), + {}, + ); + } assert.isTrue( ClaudeAdapterV2.ClaudeProviderCapabilitiesV2.planning.supportsStructuredQuestions, ); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 3e10e09906796..4090492add407 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -5007,6 +5007,21 @@ export function claudeSdkUserInputAnswers( ); } +// Claude Code echoes the chosen option's preview back to the model as "selected preview:". +export function claudeSdkUserInputAnnotations( + questions: ReadonlyArray, + answers: NonNullable, +): NonNullable { + return Object.fromEntries( + questions.flatMap((question) => { + const preview = question.options.find( + (option) => option.label === answers[question.id], + )?.preview; + return preview === undefined ? [] : [[question.id, { preview }]]; + }), + ); +} + function claudeTodoSteps(input: unknown): ReadonlyArray { const value = typeof input === "object" && input !== null && Reflect.get(input, "type") === "record" @@ -11493,6 +11508,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ), ); if (!(yield* Deferred.isDone(answers))) yield* cancelWithdrawnRequest(artifacts); + const sdkAnswers = claudeSdkUserInputAnswers(resolvedAnswers); + const annotations = claudeSdkUserInputAnnotations(questions, sdkAnswers); return callbackOptions.signal.aborted ? ({ behavior: "deny", @@ -11502,7 +11519,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( behavior: "allow", updatedInput: { questions: toolInput.questions, - answers: claudeSdkUserInputAnswers(resolvedAnswers), + answers: sdkAnswers, + ...(Object.keys(annotations).length > 0 ? { annotations } : {}), }, toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult); diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 7cd188fa96bc4..18ec64aea2d7b 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -325,13 +325,15 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( {content} {option.preview ? ( - +
+ {/* Claude writes previews for a monospace box, so paragraphs keep their spaces and newlines to hold ASCII mockups aligned. */} + +
) : null} );