fix: v2.7.5 — the chat shows each AskUserQuestion with the user's answer
- Chat: an AskUserQuestion is its own card (AskCard in MessageList.tsx), not part of the collapsed work line — each question with the option picked or the text typed, "declined" when refused. The transcript route passes the answers from Claude Code's toolUseResult (askAnswers, beside diff). - Chat: the activity panel keeps the user's open/close per session for the tab, and opens by default only once the session has an output. - Workspace home: the sent prompt's draft is dropped even though opening the session unmounts the home. - Terminal: Ctrl+V with text on the clipboard pastes the text; only an empty clipboard still sends ^V. - AGENTS.md / CLAUDE.md: run only the tests a change touches while iterating, the full suite once before committing.
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
* files ends with Claude Desktop's "Edited N files" card: every file with the
|
||||
* lines added and removed (the tool results' `diff`); a click opens the file
|
||||
* when the caller can (`onOpenFile`), and a row (or the card's header, for
|
||||
* every file in it) drags into a prompt box as an `@path` attachment. A thinking block that carries text is
|
||||
* every file in it) drags into a prompt box as an `@path` attachment. An
|
||||
* AskUserQuestion is its own card (`AskCard`), not part of the collapsed work
|
||||
* line: each question with the answer the user picked or typed. A thinking block that carries text is
|
||||
* the model's narration to the user (Claude Code ≥ 2.1.280 stores real
|
||||
* thinking empty) and reads as prose, like Claude Desktop shows it; a live
|
||||
* chat (`live`) ends, until its turn is over, with what the model is doing
|
||||
@@ -95,6 +97,8 @@ import {
|
||||
Check,
|
||||
Hand,
|
||||
SquareTerminal,
|
||||
MessageCircleQuestion,
|
||||
CornerDownRight,
|
||||
} from "lucide-react";
|
||||
import type { TranscriptMessage, TranscriptContent, TranscriptSender } from "../../lib/types";
|
||||
|
||||
@@ -461,6 +465,7 @@ type Item =
|
||||
| { kind: "user"; key: string; msg: TranscriptMessage }
|
||||
| { kind: "text"; key: string; msg: TranscriptMessage; text: string }
|
||||
| { kind: "work"; key: string; blocks: TranscriptContent[] }
|
||||
| { kind: "ask"; key: string; block: TranscriptContent }
|
||||
| { kind: "edits"; key: string; blocks: TranscriptContent[] }
|
||||
| { kind: "done"; key: string; ms: number; output: number };
|
||||
|
||||
@@ -522,7 +527,10 @@ export function chatItems(messages: TranscriptMessage[], keys: string[]): Item[]
|
||||
msg.content.forEach((block, b) => {
|
||||
const last = items[items.length - 1];
|
||||
if (block.type === "tool_use" && EDIT_TOOLS.has(block.name ?? "")) edits.push(block);
|
||||
if (block.type === "tool_use") {
|
||||
// A question to the user is a turn of the conversation, not hidden work.
|
||||
if (block.type === "tool_use" && block.name === "AskUserQuestion") {
|
||||
items.push({ kind: "ask", key: `${key}:${b}`, block });
|
||||
} else if (block.type === "tool_use") {
|
||||
if (last?.kind === "work") last.blocks.push(block);
|
||||
else items.push({ kind: "work", key: `${key}:${b}`, blocks: [block] });
|
||||
} else if ((block.type === "text" || block.type === "thinking") && block.text?.trim()) {
|
||||
@@ -669,6 +677,14 @@ export function MessageList({
|
||||
return (
|
||||
<WorkGroup key={item.key} blocks={item.blocks} toolResultMap={toolResultMap} />
|
||||
);
|
||||
case "ask":
|
||||
return (
|
||||
<AskCard
|
||||
key={item.key}
|
||||
block={item.block}
|
||||
result={item.block.id ? (toolResultMap.get(item.block.id) ?? null) : null}
|
||||
/>
|
||||
);
|
||||
case "edits":
|
||||
// The card closes a finished turn; one still working has none yet.
|
||||
if (pending && item === items[items.length - 1]) return null;
|
||||
@@ -935,6 +951,50 @@ function RunningStep({
|
||||
}
|
||||
|
||||
/** Under a turn that ended: how long it took and the output it made. */
|
||||
/** An AskUserQuestion: each question with the answer the user picked or
|
||||
* typed (the result's `answers`), "declined" when they refused it, no answer
|
||||
* yet while the CLI still asks. */
|
||||
function AskCard({
|
||||
block,
|
||||
result,
|
||||
}: {
|
||||
block: TranscriptContent;
|
||||
result: TranscriptContent | null;
|
||||
}) {
|
||||
const { t } = useTranslation("sessions");
|
||||
const input = block.input as { questions?: unknown } | undefined;
|
||||
const questions = (Array.isArray(input?.questions) ? input.questions : [])
|
||||
.map((q) => (q && typeof q === "object" ? (q as { question?: unknown }).question : null))
|
||||
.filter((q): q is string => typeof q === "string");
|
||||
return (
|
||||
<div
|
||||
data-testid="chat-ask"
|
||||
className="space-y-2 rounded-xl border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
{questions.map((q, i) => {
|
||||
const answer = result?.answers?.[q];
|
||||
return (
|
||||
<div key={i}>
|
||||
<div className="flex items-start gap-1.5 text-fg-secondary">
|
||||
<MessageCircleQuestion className="mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>{q}</span>
|
||||
</div>
|
||||
{answer && (
|
||||
<div className="ml-5 mt-0.5 flex items-start gap-1.5 font-medium text-fg-primary">
|
||||
<CornerDownRight className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-fg-muted" />
|
||||
<span className="whitespace-pre-wrap">{answer}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{result?.is_error && (
|
||||
<div className="text-xs text-fg-muted">{t("conversation.askDeclined")}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TurnDone({ ms, output }: { ms: number; output: number }) {
|
||||
const { t } = useTranslation("sessions");
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user