Skip to content

Commit 4142c33

Browse files
wesmclkaoclaude
authored
feat: structured tool call metadata and worktree project normalization (#18)
## Summary - Extract and persist structured tool call metadata (`tool_use_id`, `input_json`, `skill_name`, `result_content_length`) from session files into the `tool_calls` table - Pair `tool_result` content lengths back to their originating `tool_call` via `tool_use_id` in the sync engine, then filter empty user carrier messages - Expose structured `tool_calls` in the message API response and enrich frontend tool block rendering with parsed arguments (Bash commands, Task prompts, TaskCreate/Update metadata, Skill names, etc.) - Canonicalize worktree project names during import so archived worktree sessions group with the main repository even when worktree paths no longer exist on disk - Batch tool-call hydration queries to avoid SQLite bind-variable limits on large sessions ## Test plan - [x] All Go tests pass (`CGO_ENABLED=1 go test -tags fts5 ./...`) - [x] All frontend tests pass (380 tests) - [x] Worktree project normalization: online worktree, offline worktree with branch hint, offline worktree without branch - [x] Tool-call batch hydration across batch boundaries (500+25 messages) - [ ] Manual: open a session with tool calls, confirm tool blocks show arguments and no empty user messages appear 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: CL Kao <clkao@datarecce.io> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ab63cbd commit 4142c33

23 files changed

Lines changed: 1849 additions & 88 deletions

frontend/src/lib/api/types/core.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ export interface ProjectInfo {
3535
session_count: number;
3636
}
3737

38+
/** Matches Go ToolCall struct in internal/db/messages.go */
39+
export interface ToolCall {
40+
tool_name: string;
41+
category: string;
42+
tool_use_id?: string;
43+
input_json?: string;
44+
skill_name?: string;
45+
result_content_length?: number;
46+
}
47+
3848
/** Matches Go Message struct in internal/db/messages.go */
3949
export interface Message {
4050
id: number;
@@ -46,6 +56,7 @@ export interface Message {
4656
has_thinking: boolean;
4757
has_tool_use: boolean;
4858
content_length: number;
59+
tool_calls?: ToolCall[];
4960
}
5061

5162
/** Matches Go MinimapEntry struct */

frontend/src/lib/components/content/MessageContent.svelte

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts">
22
import type { Message } from "../../api/types.js";
3-
import { parseContent } from "../../utils/content-parser.js";
3+
import {
4+
parseContent,
5+
enrichSegments,
6+
} from "../../utils/content-parser.js";
47
import { formatTimestamp } from "../../utils/format.js";
58
import ThinkingBlock from "./ThinkingBlock.svelte";
69
import ToolBlock from "./ToolBlock.svelte";
@@ -14,7 +17,12 @@
1417
1518
let { message }: Props = $props();
1619
17-
let segments = $derived(parseContent(message.content));
20+
let segments = $derived(
21+
enrichSegments(
22+
parseContent(message.content, message.has_tool_use),
23+
message.tool_calls,
24+
),
25+
);
1826
1927
let isUser = $derived(message.role === "user");
2028
@@ -58,7 +66,11 @@
5866
<ThinkingBlock content={segment.content} />
5967
{/if}
6068
{:else if segment.type === "tool"}
61-
<ToolBlock content={segment.content} label={segment.label} />
69+
<ToolBlock
70+
content={segment.content}
71+
label={segment.label}
72+
toolCall={segment.toolCall}
73+
/>
6274
{:else if segment.type === "code"}
6375
<CodeBlock content={segment.content} language={segment.label} />
6476
{:else}

frontend/src/lib/components/content/ToolBlock.svelte

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,90 @@
11
<script lang="ts">
2+
import type { ToolCall } from "../../api/types.js";
3+
24
interface Props {
35
content: string;
46
label?: string;
7+
toolCall?: ToolCall;
58
}
69
7-
let { content, label }: Props = $props();
10+
let { content, label, toolCall }: Props = $props();
811
let collapsed: boolean = $state(true);
912
1013
let previewLine = $derived(
1114
content.split("\n")[0]?.slice(0, 100) ?? "",
1215
);
16+
17+
/** Parsed input parameters from structured tool call data */
18+
let inputParams = $derived.by(() => {
19+
if (!toolCall?.input_json) return null;
20+
try {
21+
return JSON.parse(toolCall.input_json);
22+
} catch {
23+
return null;
24+
}
25+
});
26+
27+
/** For Task tool calls, extract key metadata fields */
28+
let taskMeta = $derived.by(() => {
29+
if (toolCall?.tool_name !== "Task" || !inputParams)
30+
return null;
31+
const meta: { label: string; value: string }[] = [];
32+
if (inputParams.subagent_type) {
33+
meta.push({
34+
label: "type",
35+
value: inputParams.subagent_type,
36+
});
37+
}
38+
if (inputParams.description) {
39+
meta.push({
40+
label: "description",
41+
value: inputParams.description,
42+
});
43+
}
44+
return meta.length ? meta : null;
45+
});
46+
47+
/** For TaskCreate, show subject and description */
48+
let taskCreateMeta = $derived.by(() => {
49+
if (toolCall?.tool_name !== "TaskCreate" || !inputParams)
50+
return null;
51+
const meta: { label: string; value: string }[] = [];
52+
if (inputParams.subject) {
53+
meta.push({ label: "subject", value: inputParams.subject });
54+
}
55+
if (inputParams.description) {
56+
meta.push({ label: "description", value: inputParams.description });
57+
}
58+
return meta.length ? meta : null;
59+
});
60+
61+
/** For TaskUpdate, show taskId and status */
62+
let taskUpdateMeta = $derived.by(() => {
63+
if (toolCall?.tool_name !== "TaskUpdate" || !inputParams)
64+
return null;
65+
const meta: { label: string; value: string }[] = [];
66+
if (inputParams.taskId) {
67+
meta.push({ label: "task", value: `#${inputParams.taskId}` });
68+
}
69+
if (inputParams.status) {
70+
meta.push({ label: "status", value: inputParams.status });
71+
}
72+
if (inputParams.subject) {
73+
meta.push({ label: "subject", value: inputParams.subject });
74+
}
75+
return meta.length ? meta : null;
76+
});
77+
78+
/** Combined metadata for any tool type */
79+
let metaTags = $derived(
80+
taskMeta ?? taskCreateMeta ?? taskUpdateMeta ?? null,
81+
);
82+
83+
let taskPrompt = $derived(
84+
toolCall?.tool_name === "Task"
85+
? inputParams?.prompt ?? null
86+
: null,
87+
);
1388
</script>
1489

1590
<div class="tool-block">
@@ -28,7 +103,21 @@
28103
{/if}
29104
</button>
30105
{#if !collapsed}
31-
<pre class="tool-content">{content}</pre>
106+
{#if metaTags}
107+
<div class="tool-meta">
108+
{#each metaTags as { label: metaLabel, value }}
109+
<span class="meta-tag">
110+
<span class="meta-label">{metaLabel}:</span>
111+
{value}
112+
</span>
113+
{/each}
114+
</div>
115+
{/if}
116+
{#if taskPrompt}
117+
<pre class="tool-content">{taskPrompt}</pre>
118+
{:else if content}
119+
<pre class="tool-content">{content}</pre>
120+
{/if}
32121
{/if}
33122
</div>
34123

@@ -90,6 +179,28 @@
90179
min-width: 0;
91180
}
92181
182+
.tool-meta {
183+
display: flex;
184+
flex-wrap: wrap;
185+
gap: 6px;
186+
padding: 6px 14px;
187+
border-top: 1px solid var(--border-muted);
188+
}
189+
190+
.meta-tag {
191+
font-family: var(--font-mono);
192+
font-size: 11px;
193+
color: var(--text-muted);
194+
background: var(--bg-inset);
195+
padding: 2px 6px;
196+
border-radius: var(--radius-sm);
197+
}
198+
199+
.meta-label {
200+
color: var(--text-secondary);
201+
font-weight: 500;
202+
}
203+
93204
.tool-content {
94205
padding: 8px 14px 10px;
95206
font-family: var(--font-mono);

frontend/src/lib/components/content/ToolCallGroup.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts">
22
import type { Message } from "../../api/types.js";
3-
import { parseContent } from "../../utils/content-parser.js";
3+
import {
4+
parseContent,
5+
enrichSegments,
6+
} from "../../utils/content-parser.js";
47
import { formatTimestamp } from "../../utils/format.js";
58
import ToolBlock from "./ToolBlock.svelte";
69
@@ -13,7 +16,10 @@
1316
1417
let toolSegments = $derived(
1518
messages.flatMap((m) =>
16-
parseContent(m.content).filter((s) => s.type === "tool"),
19+
enrichSegments(
20+
parseContent(m.content, m.has_tool_use),
21+
m.tool_calls,
22+
).filter((s) => s.type === "tool"),
1723
),
1824
);
1925
@@ -78,6 +84,7 @@
7884
<ToolBlock
7985
content={segment.content}
8086
label={segment.label}
87+
toolCall={segment.toolCall}
8188
/>
8289
{/each}
8390
</div>

0 commit comments

Comments
 (0)