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} );