Show task notifications as synthetic tool calls in chat (#79)

* fix(server): render claude task notifications as synthetic tool calls

* refactor(claude): extract task-notification tool-call mapper

* refactor: drive tool icons from plain_text detail metadata

* refactor(claude): make task-notification parsing schema-first
This commit is contained in:
Mohamed Boudra
2026-02-24 18:15:23 +07:00
committed by GitHub
parent 9d2ec0e9e1
commit 0832b12608
11 changed files with 659 additions and 30 deletions
@@ -7,6 +7,7 @@ const iconMocks = vi.hoisted(() => ({
MicVocal: () => null,
Pencil: () => null,
Search: () => null,
Sparkles: () => null,
SquareTerminal: () => null,
Wrench: () => null,
}));
@@ -48,4 +49,24 @@ describe("tool-call-icon", () => {
expect(icon).toBe(iconMocks.Brain);
});
it("uses custom icon from plain_text detail", () => {
const icon = resolveToolCallIcon("custom_tool", {
type: "plain_text",
label: "Background task completed",
icon: "sparkles",
text: "notification payload",
});
expect(icon).toBe(iconMocks.Sparkles);
});
it("does not special-case skill tool names", () => {
const icon = resolveToolCallIcon("skill", {
type: "plain_text",
label: "Skill output",
});
expect(icon).toBe(iconMocks.Wrench);
});
});
+17 -4
View File
@@ -10,7 +10,7 @@ import {
SquareTerminal,
Wrench,
} from "lucide-react-native";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import type { ToolCallDetail, ToolCallIconName } from "@server/server/agent/agent-sdk-types";
export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>;
@@ -26,9 +26,25 @@ const TOOL_DETAIL_ICONS: Record<ToolCallDetail["type"], ToolCallIconComponent> =
unknown: Wrench,
};
const TOOL_ICON_BY_NAME: Record<ToolCallIconName, ToolCallIconComponent> = {
wrench: Wrench,
square_terminal: SquareTerminal,
eye: Eye,
pencil: Pencil,
search: Search,
bot: Bot,
sparkles: Sparkles,
brain: Brain,
mic_vocal: MicVocal,
};
export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail): ToolCallIconComponent {
const lowerName = toolName.trim().toLowerCase();
if (detail?.type === "plain_text" && detail.icon) {
return TOOL_ICON_BY_NAME[detail.icon];
}
// Thoughts are rendered through ToolCall with unknown detail payloads.
if (lowerName === "thinking" && (!detail || detail.type === "unknown")) {
return Brain;
@@ -39,9 +55,6 @@ export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail):
if (lowerName === "task") {
return Bot;
}
if (lowerName === "skill") {
return Sparkles;
}
if (detail) {
return TOOL_DETAIL_ICONS[detail.type];