streaming round: thinking collapses by default, live tool-input cards, one glimmer grammar

This commit is contained in:
amosblomqvist
2026-10-02 02:09:38 +03:00
parent a99cbf3d90
commit 0751fec9d8
6 changed files with 159 additions and 26 deletions
+6 -1
View File
@@ -5,6 +5,7 @@ import { CheckIcon, SettingsIcon, XIcon } from "lucide-react";
import { JobsContext } from "@/components/jobs-context";
import { ChatActionsContext } from "@/components/chat";
import { isModelsRefusal, isProviderKeyRefusal, type JobKind, type JobRecord } from "@/src/core";
import { BrailleSpinner } from "@/components/braille-spinner";
/**
* The dispatch card (contracts.ts "THE ASYNC PROTOCOL" §4): the tool-call
@@ -94,7 +95,11 @@ function DispatchActivityCard({ job, label }: { job: JobRecord | null; label: st
data-testid="dispatch-card"
>
<div className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
<span className="inline-block size-2 animate-pulse rounded-full bg-primary" />
{/* The working-state grammar (owner, streaming round 2026-10-01):
the Braille spinner replaces the pulsing dot — one indicator
everywhere. This is the ONLY dispatch-card change; the card's
shape is untouched. */}
<BrailleSpinner label={`${label} running`} />
{label}
{elapsed !== null && <span className="font-normal">· {elapsed}s</span>}
</div>
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { ChevronRight } from "lucide-react";
import { BrailleSpinner } from "@/components/braille-spinner";
import { Shimmer } from "@/components/ai-elements/shimmer";
import { toolActivityLabel } from "@/src/core";
import type { ArtifactPartProps } from "@/src/core";
/**
* The LIVE tool-input card (owner, streaming round 2026-10-01): what a
* tool call looks like while the agent WRITES it. A tool part sits in
* `input-streaming` for its whole execution (the bridge defers
* input-available to tool_execution_end), so this card IS the working
* state — spinner + shimmered activity label in the header (the retired
* transcript bottom line's text, same recipe as the thinking row), and
* the streaming input itself watchable on expand. The moment the call
* settles, the part re-renders through the artifact registry and the
* quiet settled card replaces this one.
*
* The one working-state grammar everywhere (owner): Braille spinner +
* Shimmer(duration 1) — the thinking row, this card, the sidebar row.
*/
interface StreamingToolPart {
type: string;
state: string;
toolCallId: string;
input?: unknown;
}
export function LiveToolCard({ part }: ArtifactPartProps) {
const p = part as StreamingToolPart;
const tool = p.type.replace(/^tool-/, "");
const inp = (p.input ?? {}) as Record<string, unknown>;
// The headline argument (ToolCallCard's rule, shown as given): path for
// read/ls, pattern for find/grep, session for list_propositions.
let arg = "";
if (typeof inp.path === "string" && inp.path) arg = inp.path;
else if (typeof inp.pattern === "string" && inp.pattern) arg = inp.pattern;
else if (typeof inp.session === "string" && inp.session) arg = inp.session;
const hasInput = p.input != null && Object.keys(inp).length > 0;
return (
<details
data-testid="live-tool-card"
data-tool={tool}
className="group my-1 w-full max-w-2xl select-none"
>
<summary className="flex cursor-pointer list-none items-center gap-2 text-xs text-muted-foreground [&::-webkit-details-marker]:hidden">
<BrailleSpinner label={`${toolActivityLabel(tool)} — live`} />
<Shimmer duration={1} className="text-xs">
{toolActivityLabel(tool)}
</Shimmer>
{arg && <span className="truncate">{arg}</span>}
<ChevronRight className="size-3 shrink-0 opacity-50 transition-transform group-open:rotate-90" />
</summary>
<div className="mt-2 rounded-xl border bg-card p-3 text-xs">
<div className="mb-1 font-medium uppercase tracking-wide text-muted-foreground">Input (streaming)</div>
<pre className="max-h-32 overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed">
{hasInput ? JSON.stringify(p.input, null, 2) : "…"}
</pre>
</div>
</details>
);
}