Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3191,6 +3191,9 @@ describe("ClaudeAdapterLive", () => {
assert.equal(typeof requestId, "string");
assert.equal(requestedEvent.value.payload.questions.length, 1);
assert.equal(requestedEvent.value.payload.questions[0]?.question, "Which framework?");
// Regression for #2388: `id` must equal the full question text so the
// UI's draft-answer key matches what the SDK looks up downstream.
assert.equal(requestedEvent.value.payload.questions[0]?.id, "Which framework?");
assert.deepEqual(requestedEvent.value.providerRefs, {
providerItemId: ProviderItemId.make("tool-ask-1"),
});
Expand Down Expand Up @@ -3225,6 +3228,34 @@ describe("ClaudeAdapterLive", () => {
assert.deepEqual(updatedInput.answers, { "Which framework?": "React" });
// Original questions should be passed through.
assert.deepEqual(updatedInput.questions, askInput.questions);

// Compatibility check for #2388: the answers shape we hand to the SDK
// must produce a non-empty rendered tool_result on BOTH SDK iteration
// patterns we have seen, so we don't regress the issue and we don't
// break users still on the older Claude CLI.
const sdkAnswers = updatedInput.answers as Record<string, unknown>;
const sdkQuestions = updatedInput.questions as ReadonlyArray<{
readonly question: string;
}>;

// Claude CLI 2.1.119 — key-agnostic Object.entries iteration. Any key
// works here, but it must at least round-trip into a non-empty string.
const v119Rendered = Object.entries(sdkAnswers)
.map(([key, value]) => `"${key}"="${String(value)}"`)
.join(", ");
assert.equal(v119Rendered, '"Which framework?"="React"');

// Claude CLI 2.1.121 — lookup by full question text. This is the path
// that regressed in #2388 when the answers were keyed by `header`.
const v121Rendered = sdkQuestions
.map(({ question }) => {
const answer = sdkAnswers[question];
return answer === undefined ? null : `"${question}"="${String(answer)}"`;
})
.filter((entry): entry is string => entry !== null)
.join(", ");
assert.notEqual(v121Rendered, "", "Expected non-empty SDK 2.1.121 tool_result (#2388)");
assert.equal(v121Rendered, '"Which framework?"="React"');
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2549,10 +2549,14 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);

// Parse questions from the SDK's AskUserQuestion input.
// `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks
// up answers by question text in `mapToolResultToToolResultBlockParam`,
// so the key the UI uses to keep its draft answer must match the SDK's
// expected lookup key. See https://github.com/pingdotgg/t3code/issues/2388
const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : [];
const questions: Array<UserInputQuestion> = rawQuestions.map(
(q: Record<string, unknown>, idx: number) => ({
id: typeof q.header === "string" ? q.header : `q-${idx}`,
id: typeof q.question === "string" && q.question.length > 0 ? q.question : `q-${idx}`,
header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`,
question: typeof q.question === "string" ? q.question : "",
options: Array.isArray(q.options)
Expand Down
Loading