diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx
index 27181166a..b58244619 100644
--- a/packages/app/src/components/tool-call-details.tsx
+++ b/packages/app/src/components/tool-call-details.tsx
@@ -245,6 +245,14 @@ export function ToolCallDetailsContent({
{detail.query}
);
+ } else if (detail?.type === "plain_text") {
+ if (detail.text) {
+ sections.push(
+
+ {detail.text}
+
+ );
+ }
} else if (detail?.type === "unknown") {
const plainInputText =
typeof detail.input === "string" && detail.output === null
diff --git a/packages/app/src/utils/tool-call-icon.ts b/packages/app/src/utils/tool-call-icon.ts
index 10ba09bc0..dbcd44acb 100644
--- a/packages/app/src/utils/tool-call-icon.ts
+++ b/packages/app/src/utils/tool-call-icon.ts
@@ -6,6 +6,7 @@ import {
MicVocal,
Pencil,
Search,
+ Sparkles,
SquareTerminal,
Wrench,
} from "lucide-react-native";
@@ -21,6 +22,7 @@ const TOOL_DETAIL_ICONS: Record =
search: Search,
worktree_setup: SquareTerminal,
sub_agent: Bot,
+ plain_text: Wrench,
unknown: Wrench,
};
@@ -37,6 +39,9 @@ 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];
diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts
index 15900de4e..92d940d64 100644
--- a/packages/server/src/server/agent/agent-sdk-types.ts
+++ b/packages/server/src/server/agent/agent-sdk-types.ts
@@ -160,6 +160,11 @@ export type ToolCallDetail =
summary?: string;
}>;
}
+ | {
+ type: "plain_text";
+ label?: string;
+ text?: string;
+ }
| {
type: "unknown";
input: unknown | null;
diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts
index 64325baf9..1430435c5 100644
--- a/packages/server/src/server/agent/providers/claude-agent.test.ts
+++ b/packages/server/src/server/agent/providers/claude-agent.test.ts
@@ -1460,6 +1460,28 @@ describe("convertClaudeHistoryEntry", () => {
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", () => {
const entry = {
type: "assistant",
diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts
index e99c61b05..74287d453 100644
--- a/packages/server/src/server/agent/providers/claude-agent.ts
+++ b/packages/server/src/server/agent/providers/claude-agent.ts
@@ -1344,9 +1344,19 @@ function isMetadataOnlySdkMessage(message: SDKMessage): boolean {
if (message.type !== "user") {
return false;
}
+ if (isSyntheticUserEntry(message)) {
+ return true;
+ }
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 {
const root = message as unknown as Record;
const messageType = readTrimmedString(root.type);
@@ -3429,6 +3439,9 @@ class ClaudeAgentSession implements AgentSession {
}
break;
case "user": {
+ if (isSyntheticUserEntry(message)) {
+ break;
+ }
if (this.compacting) {
this.compacting = false;
break;
@@ -4380,6 +4393,9 @@ export function convertClaudeHistoryEntry(
if (entry.isCompactSummary) {
return [];
}
+ if (entry.type === "user" && isSyntheticUserEntry(entry)) {
+ return [];
+ }
const message = entry?.message;
if (!message || !("content" in message)) {
@@ -4556,6 +4572,9 @@ async function parseClaudeSessionDescriptor(
if (entry?.isSidechain) {
continue;
}
+ if (entry?.type === "user" && isSyntheticUserEntry(entry)) {
+ continue;
+ }
if (!sessionId && typeof entry.sessionId === "string") {
sessionId = entry.sessionId;
}
diff --git a/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts
index 825374ab2..f526adc7d 100644
--- a/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts
+++ b/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts
@@ -102,6 +102,25 @@ const ClaudeToolDetailPass2Schema = z.union([
toolDetailBranchByName("glob", ToolSearchInputSchema, z.unknown(), (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,
]);
diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts
index 384837139..08ef0d95f 100644
--- a/packages/server/src/shared/messages.ts
+++ b/packages/server/src/shared/messages.ts
@@ -210,6 +210,11 @@ const ToolCallDetailPayloadSchema: z.ZodType = z.discriminatedUn
})
),
}),
+ z.object({
+ type: z.literal('plain_text'),
+ label: z.string().optional(),
+ text: z.string().optional(),
+ }),
z.object({
type: z.literal('unknown'),
input: UnknownValueSchema,
diff --git a/packages/server/src/shared/tool-call-display.ts b/packages/server/src/shared/tool-call-display.ts
index 63568bdf8..ea24da17a 100644
--- a/packages/server/src/shared/tool-call-display.ts
+++ b/packages/server/src/shared/tool-call-display.ts
@@ -91,6 +91,9 @@ export function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCall
displayName = readString(input.detail.subAgentType) ?? "Task";
summary = readString(input.detail.description);
break;
+ case "plain_text":
+ summary = input.detail.label;
+ break;
case "unknown":
break;
}