Skip to content

Commit 5b3158c

Browse files
gaoyu06claude
andcommitted
fix(claude): fill tool cards from can_use_tool; readable AskUserQuestion prompt
Tool cards showed no details in ask mode: the input arrives with the approval request (can_use_tool carries tool_use_id + full input), but only the summary used it. Now the adapter also fills/creates the tool card from that input so the command / diff / path show even before the tool runs — alongside the existing streamed-input path. Also parse AskUserQuestion prompts into a readable question + bulleted options instead of dumping raw {"questions":[...]} JSON into the approval card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7477cbd commit 5b3158c

2 files changed

Lines changed: 91 additions & 15 deletions

File tree

src/lib/backends/claude.test.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -621,15 +621,19 @@ describe('claude adapter: approvals', () => {
621621
const adapter = createClaudeAdapter();
622622
boot(adapter, lines);
623623
const events = adapter.translate(canUseBash);
624-
expect(events).toHaveLength(1);
625-
expect(events[0]).toMatchObject({
624+
// The approval request also fills the tool card from its input (so the card
625+
// shows the command even in ask mode, before it streams/completes).
626+
expect(events).toContainEqual({ type: 'tool_start', call_id: 'toolu_01UET', name: 'bash' });
627+
expect(events).toContainEqual({ type: 'tool_update', call_id: 'toolu_01UET', output: JSON.stringify({ command: 'mkdir probe-dir' }) });
628+
const approval = events.find((e) => e.type === 'approval_request')!;
629+
expect(approval).toMatchObject({
626630
type: 'approval_request',
627631
call_id: 'approval-1',
628632
name: 'bash',
629633
subagent_id: null,
630634
hunks: null
631635
});
632-
expect(String(events[0].summary)).toContain('mkdir probe-dir');
636+
expect(String(approval.summary)).toContain('mkdir probe-dir');
633637

634638
const frames = adapter.encodeOp({ op: 'approve', call_id: 'approval-1', decision: 'allow' });
635639
expect(frames).toHaveLength(1);
@@ -702,8 +706,45 @@ describe('claude adapter: approvals', () => {
702706
tool_use_id: 'toolu_w2'
703707
}
704708
});
705-
expect(events[0]).toMatchObject({ type: 'approval_request', name: 'write' });
706-
expect(String(events[0].summary)).toBe('/proj/probe-note.txt\n+hello');
709+
// The card is filled from the request; the approval_request follows.
710+
expect(events).toContainEqual({ type: 'tool_start', call_id: 'toolu_w2', name: 'write' });
711+
const approval = events.find((e) => e.type === 'approval_request')!;
712+
expect(approval).toMatchObject({ type: 'approval_request', name: 'write' });
713+
expect(String(approval.summary)).toBe('/proj/probe-note.txt\n+hello');
714+
});
715+
716+
it('renders an AskUserQuestion prompt as readable question + options (not raw JSON)', () => {
717+
const { lines } = makeIo();
718+
const adapter = createClaudeAdapter();
719+
boot(adapter, lines);
720+
const events = adapter.translate({
721+
type: 'control_request',
722+
request_id: 'req-q',
723+
request: {
724+
subtype: 'can_use_tool',
725+
tool_name: 'AskUserQuestion',
726+
tool_use_id: 'toolu_q',
727+
input: {
728+
questions: [
729+
{
730+
question: 'How to handle the existing index.html?',
731+
header: 'How',
732+
options: [
733+
{ label: 'New file', description: 'keep index.html, write blog.html' },
734+
{ label: 'Overwrite', description: 'replace index.html' }
735+
],
736+
multiSelect: false
737+
}
738+
]
739+
}
740+
}
741+
});
742+
const approval = events.find((e) => e.type === 'approval_request')!;
743+
const summary = String(approval.summary);
744+
expect(summary).toContain('How to handle the existing index.html?');
745+
expect(summary).toContain('• New file — keep index.html, write blog.html');
746+
expect(summary).toContain('• Overwrite — replace index.html');
747+
expect(summary).not.toContain('{"questions"'); // no raw JSON
707748
});
708749

709750
it('answers unsupported control requests with an error instead of hanging', () => {

src/lib/backends/claude.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,29 @@ function approvalSummary(req: CanUseToolRequest): string {
292292
}
293293
case 'Read':
294294
return str(input.file_path);
295+
case 'AskUserQuestion': {
296+
// The model asking the user a multiple-choice question — render the
297+
// question(s) + options readably instead of dumping raw JSON.
298+
const questions = Array.isArray(input.questions) ? input.questions : [];
299+
const text = questions
300+
.map((q) => {
301+
const m = rec(q);
302+
if (!m) return '';
303+
const opts = (Array.isArray(m.options) ? m.options : [])
304+
.map((o) => {
305+
const om = rec(o);
306+
if (!om) return '';
307+
const d = str(om.description);
308+
return ` • ${str(om.label)}${d ? ` — ${d}` : ''}`;
309+
})
310+
.filter(Boolean)
311+
.join('\n');
312+
return [str(m.question), opts].filter(Boolean).join('\n');
313+
})
314+
.filter(Boolean)
315+
.join('\n\n');
316+
return cap(text || JSON.stringify(req.input));
317+
}
295318
default: {
296319
const detail = str(req.description) || JSON.stringify(req.input);
297320
return cap(`${req.tool_name}: ${detail}`);
@@ -788,18 +811,30 @@ export function createClaudeAdapter(): EngineAdapter {
788811
const req = frame.request;
789812
if (req?.subtype === 'can_use_tool') {
790813
const r = req as unknown as CanUseToolRequest;
814+
const events: NormalizedEvent[] = [];
815+
// The approval request carries the tool's FULL input — fill the tool card
816+
// from it (in ask mode the streamed input may not have reached the card),
817+
// creating the card if the assistant stream hasn't shown it yet.
818+
const tid = str(r.tool_use_id);
819+
if (tid) {
820+
const input = rec(r.input) ?? {};
821+
const name = mapToolName(str(r.tool_name));
822+
const known = tools.get(tid);
823+
tools.set(tid, { claudeName: str(r.tool_name), name, input });
824+
if (!known) events.push({ type: 'tool_start', call_id: tid, name });
825+
events.push({ type: 'tool_update', call_id: tid, output: toolCardJson(str(r.tool_name), input) });
826+
}
791827
const callId = `approval-${++approvalSeq}`;
792828
approvals.set(callId, { requestId: str(frame.request_id), request: r });
793-
return [
794-
{
795-
type: 'approval_request',
796-
call_id: callId,
797-
name: mapToolName(str(r.tool_name)),
798-
summary: approvalSummary(r),
799-
subagent_id: null,
800-
hunks: null
801-
}
802-
];
829+
events.push({
830+
type: 'approval_request',
831+
call_id: callId,
832+
name: mapToolName(str(r.tool_name)),
833+
summary: approvalSummary(r),
834+
subagent_id: null,
835+
hunks: null
836+
});
837+
return events;
803838
}
804839
// Unsupported CLI→client request (request_user_dialog, elicitation, …):
805840
// answer with an error so the CLI can resolve it instead of hanging.

0 commit comments

Comments
 (0)