Files
PaperClipAI/ui/src/components/task-chat/TaskChatToolCard.tsx
T
DottaandPaperclip cd4c4ed205 fix(ui): stabilize task loading and live feeds (#13095)
Coordinate initial conversation reveal, preserve message identity and reading anchors during live updates, and bound transcript reads with recoverable retries. Cover desktop/mobile navigation and rich task loading with actual-route browser tests.

Verified all Linux CI gates, 5,575 local UI tests, eight layout browser scenarios, and recorded native Codex walkthroughs. Greptile: 5/5; all review findings resolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-09 10:00:37 -05:00

138 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useTaskChatExpansion } from "./expansion-state";
import { cn } from "@/lib/utils";
import {
Check,
ChevronRight,
Loader2,
ShieldCheck,
ShieldX,
X,
} from "lucide-react";
import type { TaskChatToolItem } from "./task-chat-model";
import { toolTaxonomy } from "./tool-taxonomy";
const STATUS_ICON = {
pending: { Icon: Loader2, spin: false, tone: "text-muted-foreground" },
in_progress: {
Icon: Loader2,
spin: true,
tone: "text-(--status-agent-running)",
},
completed: { Icon: Check, spin: false, tone: "text-muted-foreground" },
failed: { Icon: X, spin: false, tone: "text-destructive" },
interrupted: { Icon: X, spin: false, tone: "text-muted-foreground" },
} as const;
/**
* Tool invocation as a flat activity row (v7): [glyph] [name] [mono target] …
* [hover chevron] [status]. No card chrome — activity is metadata, not
* content. Clicking toggles a compact result/change summary (left-rail
* inset). Full diff bodies stay out of the activity feed.
*/
export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) {
const { Icon, spin, tone } = STATUS_ICON[item.status];
const RowIcon = toolTaxonomy(item.rawName ?? item.name).icon;
const [showDetail, setShowDetail] = useTaskChatExpansion(item.id, false);
const expandable = Boolean(item.target || item.detail || item.diff);
return (
<div data-testid="task-chat-tool-card" className="flex min-w-0 max-w-full flex-col text-xs">
<button
type="button"
onClick={expandable ? () => setShowDetail((v) => !v) : undefined}
aria-expanded={expandable ? showDetail : undefined}
className={cn(
"group/tool -mx-1.5 flex min-h-6 w-full min-w-0 max-w-full items-center gap-2 overflow-hidden rounded-sm px-1.5 py-1 text-left leading-none text-muted-foreground",
expandable
? "cursor-pointer transition-colors hover:bg-muted/60 hover:text-foreground"
: "cursor-default",
)}
>
<span className="flex w-5 shrink-0 items-center justify-center">
<RowIcon
className="h-3.5 w-3.5 shrink-0"
aria-hidden
data-testid="task-chat-tool-icon"
/>
</span>
<span className="flex min-w-0 flex-1 items-baseline gap-2 overflow-hidden">
<span className="shrink-0 font-medium leading-4">{item.name}</span>
{item.target ? (
<span className="task-chat-collapsed-line-fade min-w-0 flex-1 font-mono text-(length:--text-micro) leading-4">
{item.target}
</span>
) : null}
</span>
<span className="ml-auto flex shrink-0 items-center gap-2">
{item.decision ? (
<span
className={cn(
"flex items-center gap-1 text-(length:--text-micro)",
item.decision === "allowed"
? "text-(--status-task-icon-done)"
: "text-destructive",
)}
>
{item.decision === "allowed" ? (
<ShieldCheck className="h-3 w-3" />
) : (
<ShieldX className="h-3 w-3" />
)}
{item.decision}
</span>
) : null}
{expandable ? (
<ChevronRight
className={cn(
"h-3 w-3 opacity-0 transition-[opacity,transform] group-hover/tool:opacity-80",
showDetail ? "rotate-90" : null,
)}
aria-hidden
/>
) : null}
<Icon
className={cn("h-3.5 w-3.5", tone, spin && "animate-spin")}
aria-hidden
/>
{item.status === "interrupted" ? (
<span className="text-(length:--text-micro) text-muted-foreground">
Interrupted
</span>
) : null}
</span>
</button>
{item.target && showDetail ? (
<div
className="task-chat-expanded-line-wrap ml-1.5 mt-0.5 min-w-0 max-w-full overflow-hidden border-l-2 border-border py-1 pl-2.5 font-mono text-(length:--text-micro) leading-relaxed text-muted-foreground"
data-testid="task-chat-tool-target-detail"
>
{item.target}
</div>
) : null}
{item.detail && showDetail ? (
<pre className="task-chat-expanded-line-wrap ml-1.5 mt-0.5 min-w-0 max-w-full overflow-hidden whitespace-pre-wrap border-l-2 border-border py-1 pl-2.5 font-mono text-(length:--text-micro) leading-relaxed text-muted-foreground">
{item.detail}
</pre>
) : null}
{item.diff && showDetail ? (
<div
className="ml-1.5 mt-0.5 border-l-2 border-border py-1 pl-2.5"
data-testid="task-chat-tool-change-summary"
>
<div className="flex min-w-0 items-center gap-2 text-(length:--text-micro) text-muted-foreground">
<span className="shrink-0">Changed</span>
{item.diff.path ? (
<span className="min-w-0 truncate font-mono text-foreground">
{item.diff.path}
</span>
) : null}
<span className="ml-auto shrink-0 font-mono">
+{item.diff.added} −{item.diff.removed}
</span>
</div>
</div>
) : null}
</div>
);
}