Skip to content

Commit 9245876

Browse files
committed
refactor(chat): add toolName extraction and optimize file exploration heuristics
- Extract `toolName` from payloads for more precise identification of tool actions - Refactor exploration card logic to use specific tool sets for read and search actions - Enhance heading generation and output previews for better clarity in UI - Use cleaner parsing and JSON processing for tool details and file paths
1 parent d5a3909 commit 9245876

6 files changed

Lines changed: 418 additions & 102 deletions

File tree

apps/server/src/provider/Layers/ClaudeAdapter.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,16 +482,82 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
482482
const commandValue = input.command ?? input.cmd;
483483
const command = typeof commandValue === "string" ? commandValue : undefined;
484484
if (command && command.trim().length > 0) {
485-
return `${toolName}: ${command.trim().slice(0, 400)}`;
485+
return command.trim().slice(0, 400);
486486
}
487487

488+
const friendly = friendlyToolSummary(toolName, input);
489+
if (friendly) return friendly;
490+
488491
const serialized = JSON.stringify(input);
489492
if (serialized.length <= 400) {
490493
return `${toolName}: ${serialized}`;
491494
}
492495
return `${toolName}: ${serialized.slice(0, 397)}...`;
493496
}
494497

498+
function friendlyToolSummary(toolName: string, input: Record<string, unknown>): string | undefined {
499+
const normalized = toolName.toLowerCase();
500+
501+
if (normalized === "read") {
502+
const filePath = trimStr(input.file_path ?? input.filePath ?? input.path);
503+
return filePath ? shortenPath(filePath) : undefined;
504+
}
505+
506+
if (normalized === "edit") {
507+
const filePath = trimStr(input.file_path ?? input.filePath ?? input.path);
508+
return filePath ? shortenPath(filePath) : undefined;
509+
}
510+
511+
if (normalized === "write") {
512+
const filePath = trimStr(input.file_path ?? input.filePath ?? input.path);
513+
return filePath ? shortenPath(filePath) : undefined;
514+
}
515+
516+
if (normalized === "grep") {
517+
const pattern = trimStr(input.pattern);
518+
const path = trimStr(input.path);
519+
if (pattern && path) return `"${pattern}" in ${shortenPath(path)}`;
520+
if (pattern) return `"${pattern}"`;
521+
return undefined;
522+
}
523+
524+
if (normalized === "glob") {
525+
const pattern = trimStr(input.pattern);
526+
const path = trimStr(input.path);
527+
if (pattern && path) return `${pattern} in ${shortenPath(path)}`;
528+
if (pattern) return pattern;
529+
return undefined;
530+
}
531+
532+
if (normalized === "bash") {
533+
const cmd = trimStr(input.command ?? input.cmd ?? input.script);
534+
return cmd ? cmd.slice(0, 400) : undefined;
535+
}
536+
537+
if (normalized === "webfetch" || normalized === "web_fetch") {
538+
return trimStr(input.url) ?? undefined;
539+
}
540+
541+
if (normalized === "websearch" || normalized === "web_search") {
542+
const query = trimStr(input.query ?? input.q);
543+
return query ? `"${query}"` : undefined;
544+
}
545+
546+
return undefined;
547+
}
548+
549+
function trimStr(value: unknown): string | null {
550+
if (typeof value !== "string") return null;
551+
const trimmed = value.trim();
552+
return trimmed.length > 0 ? trimmed : null;
553+
}
554+
555+
function shortenPath(fullPath: string): string {
556+
const parts = fullPath.split("/");
557+
if (parts.length <= 3) return fullPath;
558+
return parts.slice(-3).join("/");
559+
}
560+
495561
function titleForTool(itemType: CanonicalItemType, toolName?: string): string {
496562
switch (itemType) {
497563
case "command_execution":
@@ -509,12 +575,18 @@ function titleForTool(itemType: CanonicalItemType, toolName?: string): string {
509575
case "image_view":
510576
return "Image view";
511577
case "dynamic_tool_call":
512-
return "Tool call";
578+
return toolName ? capitalizeToolName(toolName) : "Tool call";
513579
default:
514580
return "Item";
515581
}
516582
}
517583

584+
function capitalizeToolName(toolName: string): string {
585+
const trimmed = toolName.trim();
586+
if (trimmed.length === 0) return "Tool call";
587+
return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`;
588+
}
589+
518590
function titleForReadTool(toolName?: string): string {
519591
if (!toolName) return "Read";
520592
const normalized = toolName.toLowerCase();

apps/web/src/components/chat/AgentGroupCard.tsx

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
2-
import { memo, useState } from "react";
1+
import { BotIcon } from "lucide-react";
2+
import { memo } from "react";
33
import {
44
formatTokenCount,
55
formatToolUseCount,
@@ -120,13 +120,7 @@ const AgentTaskRow = memo(function AgentTaskRow(props: { task: AgentTaskSummary
120120

121121
export const AgentGroupCard = memo(function AgentGroupCard(props: AgentGroupCardProps) {
122122
const { agentGroup, label, isLive } = props;
123-
const [expanded, setExpanded] = useState(false);
124123
const tasks = agentGroup.tasks;
125-
const allSettled = tasks.every(
126-
(t) => t.status === "completed" || t.status === "failed" || t.status === "stopped",
127-
);
128-
129-
const ExpandIcon = expanded ? ChevronDownIcon : ChevronRightIcon;
130124

131125
return (
132126
<div
@@ -136,44 +130,18 @@ export const AgentGroupCard = memo(function AgentGroupCard(props: AgentGroupCard
136130
isLive ? "border-l-violet-400/40" : "border-l-violet-400/20",
137131
)}
138132
>
139-
<button
140-
type="button"
141-
aria-expanded={expanded}
142-
className="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors duration-100 hover:bg-muted/20"
143-
onClick={() => setExpanded((prev) => !prev)}
144-
>
145-
<ExpandIcon className="size-3 shrink-0 text-muted-foreground/50" />
146-
<span className="flex size-4 shrink-0 items-center justify-center">
147-
{allSettled ? (
148-
<span className="size-2 rounded-full bg-emerald-500" aria-hidden="true" />
149-
) : (
150-
<span className="size-2 animate-pulse rounded-full bg-amber-400" aria-hidden="true" />
151-
)}
133+
<div className="flex items-center gap-2 px-3 py-1.5">
134+
<BotIcon className="size-3.5 shrink-0 text-violet-400/60" />
135+
<span className="min-w-0 flex-1 truncate text-[11px] leading-5 text-foreground/80">
136+
{label}
152137
</span>
153-
<Tooltip>
154-
<TooltipTrigger
155-
render={
156-
<span className="min-w-0 flex-1 truncate text-[11px] leading-5 text-foreground/80">
157-
{label}
158-
</span>
159-
}
160-
/>
161-
<TooltipPopup
162-
side="top"
163-
className="max-w-lg break-words whitespace-pre-wrap leading-tight"
164-
>
165-
{label}
166-
</TooltipPopup>
167-
</Tooltip>
168-
</button>
138+
</div>
169139

170-
{expanded && (
171-
<div className="space-y-1 border-t border-border/20 px-2 py-1.5">
172-
{tasks.map((task) => (
173-
<AgentTaskRow key={task.taskId} task={task} />
174-
))}
175-
</div>
176-
)}
140+
<div className="space-y-1 border-t border-border/20 px-2 py-1.5">
141+
{tasks.map((task) => (
142+
<AgentTaskRow key={task.taskId} task={task} />
143+
))}
144+
</div>
177145
</div>
178146
);
179147
});
Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
import {
2-
CheckIcon,
3-
ChevronDownIcon,
4-
ChevronRightIcon,
5-
CircleXIcon,
6-
TerminalIcon,
7-
} from "lucide-react";
8-
import { memo, useState } from "react";
1+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, CircleXIcon, TerminalIcon } from "lucide-react";
2+
import { memo, useLayoutEffect, useMemo, useRef, useState } from "react";
93
import { cn } from "~/lib/utils";
4+
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
105
import type { WorkLogEntry } from "../../session-logic";
116

127
interface CommandExecutionCardProps {
@@ -16,13 +11,36 @@ interface CommandExecutionCardProps {
1611

1712
type CommandStatus = "running" | "error" | "success";
1813

14+
const PREVIEW_MAX_HEIGHT = "120px";
15+
1916
function deriveCommandStatus(entry: WorkLogEntry, isLive: boolean): CommandStatus {
2017
if (isLive && !entry.detail && entry.exitCode === undefined) return "running";
2118
if (entry.tone === "error" || (entry.exitCode !== undefined && entry.exitCode !== 0))
2219
return "error";
2320
return "success";
2421
}
2522

23+
const DETAIL_COMMAND_PREFIX_RE = /^(?:Bash|Shell|Sh):\s*/i;
24+
25+
function deriveCommandAndOutput(entry: WorkLogEntry): {
26+
displayCommand: string | null;
27+
output: string | null;
28+
} {
29+
if (entry.command) {
30+
return { displayCommand: entry.command, output: entry.detail ?? null };
31+
}
32+
if (entry.detail) {
33+
const firstNewline = entry.detail.indexOf("\n");
34+
const firstLine = firstNewline === -1 ? entry.detail : entry.detail.slice(0, firstNewline);
35+
if (DETAIL_COMMAND_PREFIX_RE.test(firstLine)) {
36+
const cmd = firstLine.replace(DETAIL_COMMAND_PREFIX_RE, "").trim();
37+
const rest = firstNewline === -1 ? null : entry.detail.slice(firstNewline + 1).trim() || null;
38+
return { displayCommand: cmd || null, output: rest };
39+
}
40+
}
41+
return { displayCommand: null, output: entry.detail ?? null };
42+
}
43+
2644
const STATUS_ACCENT: Record<CommandStatus, string> = {
2745
running: "border-l-amber-400/40",
2846
error: "border-l-rose-400/40",
@@ -63,11 +81,20 @@ export const CommandExecutionCard = memo(function CommandExecutionCard(
6381
) {
6482
const { entry, isLive } = props;
6583
const [expanded, setExpanded] = useState(false);
84+
const [previewOverflows, setPreviewOverflows] = useState(false);
85+
const previewRef = useRef<HTMLDivElement>(null);
6686

6787
const status = deriveCommandStatus(entry, isLive);
68-
const hasOutput = !!entry.detail;
88+
const { displayCommand, output } = useMemo(() => deriveCommandAndOutput(entry), [entry]);
6989

70-
const ToggleIcon = expanded ? ChevronDownIcon : ChevronRightIcon;
90+
useLayoutEffect(() => {
91+
const el = previewRef.current;
92+
if (!el || expanded) return;
93+
setPreviewOverflows(el.scrollHeight > el.clientHeight + 1);
94+
}, [expanded, output]);
95+
96+
const hasMoreContent = expanded || previewOverflows;
97+
const ExpandIcon = expanded ? ChevronUpIcon : ChevronDownIcon;
7198

7299
return (
73100
<div
@@ -77,40 +104,61 @@ export const CommandExecutionCard = memo(function CommandExecutionCard(
77104
STATUS_ACCENT[status],
78105
)}
79106
>
80-
<button
81-
type="button"
82-
onClick={() => setExpanded((prev) => !prev)}
83-
className="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors duration-100 hover:bg-muted/20"
84-
>
85-
<ToggleIcon className="size-3 shrink-0 text-muted-foreground/50" />
86-
<TerminalIcon className="size-3.5 shrink-0 text-muted-foreground/60" />
87-
<span className="text-[11px] font-medium text-muted-foreground/60">Shell</span>
88-
<span className="min-w-0 flex-1" />
89-
<CommandStatusBadge entry={entry} status={status} />
90-
</button>
91-
92-
<div className="border-t border-border/20 px-3 py-1.5">
93-
{entry.command ? (
94-
<p className="font-mono text-[11px] leading-5 text-foreground/75">
95-
<span className="text-muted-foreground/50">$ </span>
96-
{entry.command}
97-
</p>
98-
) : (
99-
<p className="text-[11px] leading-5 text-muted-foreground/60">{entry.label}</p>
100-
)}
101-
</div>
102-
103-
{expanded && (
104-
<div className="border-t border-border/20 px-3 py-2">
105-
{hasOutput ? (
106-
<pre className="max-h-[300px] overflow-y-auto whitespace-pre-wrap break-words font-mono text-[10px] leading-4 text-muted-foreground/55">
107-
{entry.detail}
108-
</pre>
107+
<Tooltip>
108+
<TooltipTrigger render={<div className="flex items-center gap-2 px-3 py-1.5" />}>
109+
<TerminalIcon className="size-3.5 shrink-0 text-muted-foreground/60" />
110+
{displayCommand ? (
111+
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-foreground/80">
112+
{displayCommand}
113+
</span>
109114
) : (
110-
<p className="text-[10px] italic text-muted-foreground/35">No output</p>
115+
<span className="min-w-0 flex-1 text-[11px] text-muted-foreground/60">
116+
{entry.label}
117+
</span>
111118
)}
119+
<CommandStatusBadge entry={entry} status={status} />
120+
</TooltipTrigger>
121+
{displayCommand && (
122+
<TooltipPopup side="top" className="max-w-lg">
123+
<p className="break-all font-mono text-xs">{displayCommand}</p>
124+
</TooltipPopup>
125+
)}
126+
</Tooltip>
127+
128+
{output && !expanded && (
129+
<div
130+
ref={previewRef}
131+
className="relative overflow-hidden border-t border-border/20"
132+
style={{ maxHeight: PREVIEW_MAX_HEIGHT }}
133+
>
134+
<pre className="whitespace-pre-wrap break-words px-3 py-1.5 font-mono text-[10px] leading-4 text-muted-foreground/55">
135+
{output}
136+
</pre>
137+
{previewOverflows && (
138+
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t from-card/90 to-transparent" />
139+
)}
140+
</div>
141+
)}
142+
143+
{output && expanded && (
144+
<div className="border-t border-border/20">
145+
<pre className="max-h-[500px] overflow-y-auto whitespace-pre-wrap break-words px-3 py-1.5 font-mono text-[10px] leading-4 text-muted-foreground/55">
146+
{output}
147+
</pre>
112148
</div>
113149
)}
150+
151+
{hasMoreContent && (
152+
<button
153+
type="button"
154+
data-scroll-anchor-ignore
155+
className="flex w-full items-center justify-center gap-1.5 border-t border-border/30 py-1.5 text-[10px] text-muted-foreground/50 transition-colors hover:bg-muted/20 hover:text-muted-foreground/70"
156+
onClick={() => setExpanded((prev) => !prev)}
157+
>
158+
<ExpandIcon className="size-3" />
159+
<span>{expanded ? "Hide output" : "Show full output"}</span>
160+
</button>
161+
)}
114162
</div>
115163
);
116164
});

0 commit comments

Comments
 (0)