Skip to content

Commit 5861e55

Browse files
committed
fix(chat): fix scrollbar, overflow, and Agent tool rendering in chat panel
- Remove right padding from chat drawer body so scrollbar sits flush against the drawer edge; add pr-3 inside viewport for content clearance - Suppress horizontal scrollbar with overflow-x-hidden on viewport - Remove redundant ChevronRight arrow from ToolShell (each tool has its own icon); fix truncate+break-words conflict by dropping truncate - Rename tool registration from "Task" to "Agent" to match actual Claude API tool name — the custom TaskToolUI was never matching before - Display subagent_type as label (e.g. CLEAN-CODE-REVIEWER) instead of generic "task:" prefix; fall back to "agent" when no type is specified - Add Agent tool call examples to MockChatPage for dev preview - Add 36 unit tests for ToolShell, parseMcpToolName, langFromPath, and agent label derivation
1 parent 5312683 commit 5861e55

8 files changed

Lines changed: 410 additions & 45 deletions

File tree

apps/web/src/components/RelayRuntimeProvider.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useSessionRelay } from "../hooks/useSessionRelay";
77

88
type ContentPart = { type: "text"; text: string } | { type: "reasoning"; text: string } | ToolCallPart;
99

10-
// Subtask (subagent) child events captured inside a Task tool call. Not assistant-ui
10+
// Subtask (subagent) child events captured inside a Agent tool call. Not assistant-ui
1111
// parts — just a serializable summary rendered by TaskToolUI.
1212
export type SubtaskChild =
1313
| { kind: "text"; text: string }
@@ -39,10 +39,10 @@ type ToolCallPart = {
3939
export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): ThreadMessageLike[] {
4040
const messages: ThreadMessageLike[] = [];
4141
const toolCallMap = new Map<string, ToolCallPart>();
42-
// Nested subtasks: when a subagent spawns another subagent, the inner Task's
42+
// Nested subtasks: when a subagent spawns another subagent, the inner Agent's
4343
// tool_use lives inside the outer Task's children (not in toolCallMap). Map
44-
// any nested tool_use_id we observe → its top-level Task id, so subsequent
45-
// descendant blocks can still be routed to the outermost Task card.
44+
// any nested tool_use_id we observe → its top-level Agent id, so subsequent
45+
// descendant blocks can still be routed to the outermost Agent card.
4646
const subtaskRoot = new Map<string, string>();
4747

4848
function resolveTaskRoot(parentId: string): string | undefined {
@@ -68,17 +68,17 @@ export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): T
6868
function appendSubtaskChild(parentId: string, child: SubtaskChild) {
6969
const rootId = resolveTaskRoot(parentId);
7070
if (!rootId) {
71-
// Parent Task tool_use is unknown. Shouldn't happen in a well-ordered
72-
// stream (block.done for the Task arrives before its subagent's blocks),
71+
// Parent Agent tool_use is unknown. Shouldn't happen in a well-ordered
72+
// stream (block.done for the Agent arrives before its subagent's blocks),
7373
// but log so data loss is diagnosable rather than silent.
7474
console.warn("[convertEvents] subtask block dropped — unknown parent", parentId, child.kind);
7575
return;
7676
}
7777
const tc = toolCallMap.get(rootId)!;
7878
const r = ensureTaskResult(tc);
7979
r.children.push(child);
80-
// If this child is itself a tool_use (potentially another Task), record a
81-
// redirect so its descendants flatten into the same top-level Task card.
80+
// If this child is itself a tool_use (potentially another Agent), record a
81+
// redirect so its descendants flatten into the same top-level Agent card.
8282
if (child.kind === "tool_use") {
8383
subtaskRoot.set(child.id, rootId);
8484
}
@@ -112,7 +112,7 @@ export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): T
112112
}
113113

114114
// If a block belongs to a subtask (parent_id set), route it into the parent
115-
// Task tool card's `children` instead of the main turn. Returns true if handled.
115+
// Agent tool card's `children` instead of the main turn. Returns true if handled.
116116
function routeSubtaskBlock(block: { type: string; [k: string]: any }): boolean {
117117
const parentId: string | undefined = block.parent_id;
118118
if (!parentId) return false;
@@ -164,8 +164,8 @@ export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): T
164164
if (block.type === "tool_result") {
165165
const tc = toolCallMap.get(block.tool_use_id);
166166
if (tc) {
167-
// For Task tool: stamp the subagent's final text into result.text (keep children/meta)
168-
if (tc.toolName === "Task") {
167+
// For Agent tool: stamp the subagent's final text into result.text (keep children/meta)
168+
if (tc.toolName === "Agent") {
169169
const r = ensureTaskResult(tc);
170170
r.text = block.output;
171171
r.error = block.error;
@@ -235,7 +235,7 @@ export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): T
235235
continue;
236236
}
237237

238-
// ── Subtask lifecycle (attach meta to parent Task tool card) ──
238+
// ── Subtask lifecycle (attach meta to parent Agent tool card) ──
239239
if (event.type === "subtask.start" || event.type === "subtask.progress" || event.type === "subtask.end") {
240240
const tc = toolCallMap.get(event.tool_use_id);
241241
if (tc) {
@@ -251,7 +251,7 @@ export function convertEvents(events: RelayEvent[], agentStatus: AgentStatus): T
251251
if (event.duration_ms != null) r.meta.duration_ms = event.duration_ms;
252252
} else {
253253
// subtask.end — the canonical final text arrives separately via the
254-
// outer Task's tool_result. Only fall back to `summary` for non-success
254+
// outer Agent's tool_result. Only fall back to `summary` for non-success
255255
// terminations (failed/stopped) where no tool_result will follow.
256256
r.meta.status = event.status;
257257
if (event.tokens != null) r.meta.tokens = event.tokens;

apps/web/src/components/TaskDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ export function TaskDetail({ taskId, onClose, onRefresh, onAgentClick: _onAgentC
355355
</div>
356356

357357
{/* Chat panel body */}
358-
<div className="flex flex-col flex-1 min-h-0 p-4">
358+
<div className="flex flex-col flex-1 min-h-0 pl-4 pb-4">
359359
<ChatPanel
360360
taskId={taskId}
361361
agentId={task.assigned_to}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface AgentThreadProps {
1616
export const AgentThread: FC<AgentThreadProps> = ({ taskDone }) => {
1717
return (
1818
<ThreadPrimitive.Root className="aui-root aui-thread-root flex h-full flex-col">
19-
<ThreadPrimitive.Viewport className="aui-thread-viewport flex flex-1 flex-col gap-4 overflow-y-auto scroll-smooth px-1">
19+
<ThreadPrimitive.Viewport className="aui-thread-viewport flex flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden scroll-smooth pr-3">
2020
<AuiIf condition={(s) => s.thread.isEmpty}>
2121
<div className="flex items-center justify-center h-full">
2222
<p className="text-sm text-content-tertiary">{taskDone ? "No activity recorded." : "Waiting for agent activity..."}</p>

apps/web/src/components/chat/tool-uis.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { cn } from "@/lib/utils";
2727
import type { SubtaskChild, TaskToolResult } from "../RelayRuntimeProvider";
2828

2929
// ─── Shared shell ───────────────────────────────────────────────────────────
30-
// Compact collapsible row: [chevron] [status icon] [tool-icon] LABEL summary…
30+
// Compact collapsible row: [status icon] [tool-icon] LABEL summary…
3131
// Built directly on Collapsible primitive so the trigger row is fully custom.
3232

3333
interface ToolShellProps {
@@ -54,14 +54,13 @@ const ToolShell: FC<ToolShellProps> = ({ icon, label, summary, status, children
5454
return (
5555
<Collapsible className={cn("group/tool w-full", isCancelled && "opacity-60")}>
5656
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded px-1.5 py-1.5 text-left text-xs transition-colors hover:bg-muted/40">
57-
<ChevronRight className="size-3 shrink-0 text-content-tertiary transition-transform group-data-[state=open]/tool:rotate-90" />
5857
<StatusIcon status={status} />
5958
<span className="shrink-0 text-content-tertiary">{icon}</span>
6059
<span className="shrink-0 font-mono text-[10px] font-semibold uppercase tracking-wide text-content-secondary">{label}</span>
61-
<span className={cn("min-w-0 flex-1 truncate font-mono text-content-primary", isCancelled && "line-through")}>{summary}</span>
60+
<span className={cn("min-w-0 flex-1 font-mono text-content-primary break-words", isCancelled && "line-through")}>{summary}</span>
6261
</CollapsibleTrigger>
6362
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
64-
<div className="pl-6 pr-1.5 pt-2 pb-2">
63+
<div className="pl-6 pr-1.5 pt-2 pb-2 overflow-x-hidden">
6564
{errorText && (
6665
<div className="mb-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[11px] text-destructive">{errorText}</div>
6766
)}
@@ -539,20 +538,16 @@ const SubtaskChildren: FC<{ items: SubtaskChild[] }> = ({ items: children }) =>
539538
};
540539

541540
export const TaskToolUI = makeAssistantToolUI<TaskArgs, TaskToolResultShape | string>({
542-
toolName: "Task",
541+
toolName: "Agent",
543542
render: ({ args, result, status }) => {
544543
const r = coerceTaskResult(result);
545544
const metaParts: string[] = [];
546545
if (r.meta?.tokens != null) metaParts.push(`${r.meta.tokens} tok`);
547546
if (r.meta?.duration_ms != null) metaParts.push(`${Math.round(r.meta.duration_ms / 1000)}s`);
548547
if (r.meta?.last_tool) metaParts.push(r.meta.last_tool);
548+
const agentLabel = args?.subagent_type || "agent";
549549
return (
550-
<ToolShell
551-
icon={<Brain className="size-3.5" />}
552-
label={args?.subagent_type ? `task:${args.subagent_type}` : "task"}
553-
status={status}
554-
summary={args?.description}
555-
>
550+
<ToolShell icon={<Brain className="size-3.5" />} label={agentLabel} status={status} summary={args?.description}>
556551
<div className="mb-1 text-[11px] text-content-tertiary">prompt:</div>
557552
<Mono>{args?.prompt}</Mono>
558553
{r.children && r.children.length > 0 && <SubtaskChildren items={r.children} />}

apps/web/src/routes/MockChatPage.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,36 @@ const MOCK_MESSAGES: ThreadMessageLike[] = [
157157
],
158158
},
159159
},
160+
{
161+
type: "tool-call",
162+
toolCallId: "tc-task-typed",
163+
toolName: "Agent",
164+
args: {
165+
description: "Review changed code for quality",
166+
prompt:
167+
"Review the Edit to packages/cli/src/commands/assign.ts for logic errors, naming, and security issues. Report PASS or REVISE with specific line-level feedback.",
168+
subagent_type: "clean-code-reviewer",
169+
},
170+
result: {
171+
text: "PASS — the dry-run branch is clean. One minor note: consider extracting the preview URL to a constant.",
172+
children: [],
173+
meta: { tokens: 1840, duration_ms: 12400, last_tool: "Read" },
174+
},
175+
},
176+
{
177+
type: "tool-call",
178+
toolCallId: "tc-task-generic",
179+
toolName: "Agent",
180+
args: {
181+
description: "Search for all assign-related test files",
182+
prompt: "Find all test files that reference the assign command or assign endpoint. Report file paths and what each tests.",
183+
},
184+
result: {
185+
text: "Found 2 test files:\n- `tests/assign.test.ts` — unit tests for assignCmd\n- `tests/api/assign.test.ts` — integration tests for /assign endpoint",
186+
children: [],
187+
meta: { tokens: 920, duration_ms: 4200, last_tool: "Glob" },
188+
},
189+
},
160190
{
161191
type: "tool-call",
162192
toolCallId: "tc-plan",

0 commit comments

Comments
 (0)