Handle skill tool details and skip synthetic timeline events

This commit is contained in:
Mohamed Boudra
2026-02-23 18:06:38 +07:00
parent ff4f2eeb56
commit cbf5621a73
8 changed files with 86 additions and 0 deletions
@@ -245,6 +245,14 @@ export function ToolCallDetailsContent({
<Text selectable style={styles.scrollText}>{detail.query}</Text> <Text selectable style={styles.scrollText}>{detail.query}</Text>
</View> </View>
); );
} else if (detail?.type === "plain_text") {
if (detail.text) {
sections.push(
<View key="plain-text" style={styles.plainTextSection}>
<Text selectable style={styles.plainText}>{detail.text}</Text>
</View>
);
}
} else if (detail?.type === "unknown") { } else if (detail?.type === "unknown") {
const plainInputText = const plainInputText =
typeof detail.input === "string" && detail.output === null typeof detail.input === "string" && detail.output === null
+5
View File
@@ -6,6 +6,7 @@ import {
MicVocal, MicVocal,
Pencil, Pencil,
Search, Search,
Sparkles,
SquareTerminal, SquareTerminal,
Wrench, Wrench,
} from "lucide-react-native"; } from "lucide-react-native";
@@ -21,6 +22,7 @@ const TOOL_DETAIL_ICONS: Record<ToolCallDetail["type"], ToolCallIconComponent> =
search: Search, search: Search,
worktree_setup: SquareTerminal, worktree_setup: SquareTerminal,
sub_agent: Bot, sub_agent: Bot,
plain_text: Wrench,
unknown: Wrench, unknown: Wrench,
}; };
@@ -37,6 +39,9 @@ export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail):
if (lowerName === "task") { if (lowerName === "task") {
return Bot; return Bot;
} }
if (lowerName === "skill") {
return Sparkles;
}
if (detail) { if (detail) {
return TOOL_DETAIL_ICONS[detail.type]; return TOOL_DETAIL_ICONS[detail.type];
@@ -160,6 +160,11 @@ export type ToolCallDetail =
summary?: string; summary?: string;
}>; }>;
} }
| {
type: "plain_text";
label?: string;
text?: string;
}
| { | {
type: "unknown"; type: "unknown";
input: unknown | null; input: unknown | null;
@@ -1460,6 +1460,28 @@ describe("convertClaudeHistoryEntry", () => {
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
test("skips synthetic user entries", () => {
const entry = {
type: "user",
isSynthetic: true,
message: {
role: "user",
content: [
{
type: "text",
text: "Base directory for this skill: /tmp/skill",
},
],
},
};
const mapBlocks = vi.fn().mockReturnValue([]);
const result = convertClaudeHistoryEntry(entry, mapBlocks);
expect(result).toEqual([]);
expect(mapBlocks).not.toHaveBeenCalled();
});
test("passes thinking blocks to mapBlocks for assistant entries", () => { test("passes thinking blocks to mapBlocks for assistant entries", () => {
const entry = { const entry = {
type: "assistant", type: "assistant",
@@ -1344,9 +1344,19 @@ function isMetadataOnlySdkMessage(message: SDKMessage): boolean {
if (message.type !== "user") { if (message.type !== "user") {
return false; return false;
} }
if (isSyntheticUserEntry(message)) {
return true;
}
return isTaskNotificationUserContent(message.message?.content); return isTaskNotificationUserContent(message.message?.content);
} }
function isSyntheticUserEntry(entry: unknown): boolean {
if (!entry || typeof entry !== "object") {
return false;
}
return (entry as { isSynthetic?: unknown }).isSynthetic === true;
}
export function readEventIdentifiers(message: SDKMessage): EventIdentifiers { export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
const root = message as unknown as Record<string, unknown>; const root = message as unknown as Record<string, unknown>;
const messageType = readTrimmedString(root.type); const messageType = readTrimmedString(root.type);
@@ -3429,6 +3439,9 @@ class ClaudeAgentSession implements AgentSession {
} }
break; break;
case "user": { case "user": {
if (isSyntheticUserEntry(message)) {
break;
}
if (this.compacting) { if (this.compacting) {
this.compacting = false; this.compacting = false;
break; break;
@@ -4380,6 +4393,9 @@ export function convertClaudeHistoryEntry(
if (entry.isCompactSummary) { if (entry.isCompactSummary) {
return []; return [];
} }
if (entry.type === "user" && isSyntheticUserEntry(entry)) {
return [];
}
const message = entry?.message; const message = entry?.message;
if (!message || !("content" in message)) { if (!message || !("content" in message)) {
@@ -4556,6 +4572,9 @@ async function parseClaudeSessionDescriptor(
if (entry?.isSidechain) { if (entry?.isSidechain) {
continue; continue;
} }
if (entry?.type === "user" && isSyntheticUserEntry(entry)) {
continue;
}
if (!sessionId && typeof entry.sessionId === "string") { if (!sessionId && typeof entry.sessionId === "string") {
sessionId = entry.sessionId; sessionId = entry.sessionId;
} }
@@ -102,6 +102,25 @@ const ClaudeToolDetailPass2Schema = z.union([
toolDetailBranchByName("glob", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByName("glob", ToolSearchInputSchema, z.unknown(), (input) =>
toSearchToolDetail(input) toSearchToolDetail(input)
), ),
toolDetailBranchByName(
"Skill",
z.object({ skill: z.string() }).passthrough(),
z.union([
z.object({ output: z.string() }).passthrough().transform((value) => value.output),
z.string(),
]).nullable(),
(input, output) => {
const skillName = input?.skill;
if (!skillName) {
return undefined;
}
return {
type: "plain_text" as const,
label: skillName,
...(output ? { text: output } : {}),
} satisfies ToolCallDetail;
}
),
ClaudeSpeakToolDetailSchema, ClaudeSpeakToolDetailSchema,
]); ]);
+5
View File
@@ -210,6 +210,11 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUn
}) })
), ),
}), }),
z.object({
type: z.literal('plain_text'),
label: z.string().optional(),
text: z.string().optional(),
}),
z.object({ z.object({
type: z.literal('unknown'), type: z.literal('unknown'),
input: UnknownValueSchema, input: UnknownValueSchema,
@@ -91,6 +91,9 @@ export function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCall
displayName = readString(input.detail.subAgentType) ?? "Task"; displayName = readString(input.detail.subAgentType) ?? "Task";
summary = readString(input.detail.description); summary = readString(input.detail.description);
break; break;
case "plain_text":
summary = input.detail.label;
break;
case "unknown": case "unknown":
break; break;
} }