Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions frontend/src/lib/api/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export interface ProjectInfo {
session_count: number;
}

/** Matches Go ToolResultEvent struct in internal/db/messages.go */
export interface ToolResultEvent {
tool_use_id?: string;
agent_id?: string;
subagent_session_id?: string;
source: string;
status: string;
content: string;
content_length: number;
timestamp?: string;
event_index: number;
}

/** Matches Go ToolCall struct in internal/db/messages.go */
export interface ToolCall {
tool_name: string;
Expand All @@ -51,6 +64,7 @@ export interface ToolCall {
result_content_length?: number;
result_content?: string;
subagent_session_id?: string;
result_events?: ToolResultEvent[];
}

/** Matches Go Message struct in internal/db/messages.go */
Expand Down
108 changes: 107 additions & 1 deletion frontend/src/lib/components/content/ToolBlock.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
let { content, label, toolCall, highlightQuery = "", isCurrentHighlight = false }: Props = $props();
let userCollapsed: boolean = $state(true);
let userOutputCollapsed: boolean = $state(true);
let userHistoryCollapsed: boolean = $state(true);
let userOverride: boolean = $state(false);
let userOutputOverride: boolean = $state(false);
let userHistoryOverride: boolean = $state(false);
let searchExpandedInput: boolean = $state(false);
let searchExpandedOutput: boolean = $state(false);
let searchExpandedHistory: boolean = $state(false);
let prevQuery: string = "";

// Auto-expand when a search match exists in input or output
Expand All @@ -41,14 +44,19 @@
const inputText = (
taskPrompt ?? content ?? fallbackContent ?? ""
).toLowerCase();
const historyText = (
toolCall?.result_events?.map((event) => event.content).join("\n\n") ?? ""
).toLowerCase();
const outputText = (
toolCall?.result_content ?? ""
[toolCall?.result_content ?? "", historyText].filter(Boolean).join("\n\n")
).toLowerCase();
searchExpandedInput = inputText.includes(q);
searchExpandedOutput = outputText.includes(q);
searchExpandedHistory = historyText.includes(q);
if (hq !== prevQuery) {
userOverride = false;
userOutputOverride = false;
userHistoryOverride = false;
prevQuery = hq;
}
});
Expand All @@ -63,6 +71,11 @@
: searchExpandedOutput ? false
: userOutputCollapsed,
);
let historyCollapsed = $derived(
userHistoryOverride ? userHistoryCollapsed
: searchExpandedHistory ? false
: userHistoryCollapsed,
);

let outputPreviewLine = $derived.by(() => {
const rc = toolCall?.result_content;
Expand All @@ -71,6 +84,14 @@
return (nl === -1 ? rc : rc.slice(0, nl)).slice(0, 100);
});

let resultEvents = $derived(toolCall?.result_events ?? []);

let historyPreviewLine = $derived.by(() => {
const last = resultEvents[resultEvents.length - 1];
if (!last) return "";
return `${last.status}: ${last.content.split("\n")[0]}`.slice(0, 100);
});

/** Parsed input parameters from structured tool call data */
let inputParams = $derived.by(() => {
if (!toolCall?.input_json) return null;
Expand Down Expand Up @@ -247,6 +268,51 @@
<pre class="tool-content output-content" use:applyHighlight={{ q: highlightQuery, current: isCurrentHighlight, content: toolCall.result_content }}>{@html escapeHTML(toolCall.result_content)}</pre>
{/if}
{/if}
{#if resultEvents.length > 0}
<button
class="history-header"
onclick={(e) => {
e.stopPropagation();
const sel = window.getSelection();
if (sel && sel.toString().length > 0) return;
userHistoryCollapsed = !userHistoryCollapsed;
userHistoryOverride = true;
}}
>
<span class="tool-chevron" class:open={!historyCollapsed}>
&#9656;
</span>
<span class="output-label">history</span>
{#if historyCollapsed && historyPreviewLine}
<span class="tool-preview">{historyPreviewLine}</span>
{/if}
</button>
{#if !historyCollapsed}
<div class="result-history">
{#each resultEvents as event (event.event_index)}
<div class="result-event">
<div class="result-event-meta">
<span class="meta-tag">
<span class="meta-label">status:</span>
{event.status}
</span>
<span class="meta-tag">
<span class="meta-label">source:</span>
{event.source}
</span>
{#if event.agent_id}
<span class="meta-tag">
<span class="meta-label">agent:</span>
{event.agent_id}
</span>
{/if}
</div>
<pre class="tool-content output-content history-content" use:applyHighlight={{ q: highlightQuery, current: isCurrentHighlight, content: event.content }}>{@html escapeHTML(event.content)}</pre>
</div>
{/each}
</div>
{/if}
{/if}
{/if}
{#if subagentSessionId}
<SubagentInline sessionId={subagentSessionId} />
Expand Down Expand Up @@ -364,6 +430,26 @@
color: var(--text-primary);
}

.history-header {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
width: 100%;
text-align: left;
font-size: 12px;
color: var(--text-secondary);
min-width: 0;
border-top: 1px solid var(--border-muted);
transition: background 0.1s;
user-select: text;
}

.history-header:hover {
background: var(--bg-surface-hover);
color: var(--text-primary);
}

.output-label {
font-family: var(--font-mono);
font-weight: 500;
Expand All @@ -377,4 +463,24 @@
max-height: 300px;
overflow-y: auto;
}

.result-history {
border-top: 1px solid var(--border-muted);
}

.result-event + .result-event {
border-top: 1px solid var(--border-muted);
}

.result-event-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px 14px 0;
}

.history-content {
border-top: 0;
margin-top: 0;
}
</style>
71 changes: 71 additions & 0 deletions frontend/src/lib/components/content/ToolBlock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,77 @@ describe("ToolBlock output section", () => {
expect(preview).not.toBeNull();
expect(preview!.textContent).toBe("first line");
});

it("renders history after expanding the tool block when result_events are set", async () => {
const toolCall: ToolCall = {
tool_name: "wait",
category: "Other",
result_content: "latest summary",
result_events: [
{
source: "wait_output",
status: "completed",
content: "Finished successfully",
content_length: 21,
agent_id: "agent-1",
event_index: 0,
},
],
};
component = mount(ToolBlock, {
target: document.body,
props: { content: "some input", toolCall },
});
await tick();

expect(document.querySelector(".history-header")).toBeNull();

document.querySelector<HTMLButtonElement>(".tool-header")!.click();
await tick();

expect(document.querySelector(".history-header")).not.toBeNull();
});

it("expands event history and shows chronological event content", async () => {
const toolCall: ToolCall = {
tool_name: "wait",
category: "Other",
result_content: "agent-a:\nFirst finished\n\nagent-b:\nSecond finished",
result_events: [
{
source: "wait_output",
status: "completed",
content: "First finished",
content_length: 14,
agent_id: "agent-a",
event_index: 0,
},
{
source: "subagent_notification",
status: "completed",
content: "Second finished",
content_length: 15,
agent_id: "agent-b",
event_index: 1,
},
],
};
component = mount(ToolBlock, {
target: document.body,
props: { content: "some input", toolCall },
});
await tick();

document.querySelector<HTMLButtonElement>(".tool-header")!.click();
await tick();
document.querySelector<HTMLButtonElement>(".history-header")!.click();
await tick();

const historyEntries = Array.from(document.querySelectorAll(".history-content"));
expect(historyEntries).toHaveLength(2);
expect(historyEntries[0].textContent).toBe("First finished");
expect(historyEntries[1].textContent).toBe("Second finished");
});
});

describe("ToolBlock fallback content", () => {
Expand Down
2 changes: 1 addition & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
// formatting changes). Old databases with a lower user_version
// trigger a non-destructive re-sync (mtime reset + skip cache
// clear) so existing session data is preserved.
const dataVersion = 6
const dataVersion = 8
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be 7?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I bet that's an artifact of it doing v2 against the v1 pass where it initially bumped to 7. Good catch!


//go:embed schema.sql
var schemaSQL string
Expand Down
Loading