group activity entries by plan step + remove inline renderers from chat
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

- Tool calls in Activity timeline are now tagged with current plan step
- Indented entries show which step they belong to
- Step tracking via update_plan_step(status=running) tool calls
- Removed inline tool renderers from chat (health summary, fleet snapshot, etc.)
  — all tool output now visible only in sidebar Activity timeline
This commit is contained in:
2026-07-14 12:38:02 +02:00
parent c8c7705046
commit 44720b7b30
4 changed files with 21 additions and 19 deletions

View File

@@ -59,7 +59,7 @@
{/if}
<button
type="button"
class="flex w-full items-start gap-2 px-3 py-1.5 text-left text-xs hover:bg-muted/30 {hasDetail ? 'cursor-pointer' : 'cursor-default'}"
class="flex w-full items-start gap-2 {entry.indent ? 'pl-7' : 'px-3'} py-1.5 text-left text-xs hover:bg-muted/30 {hasDetail ? 'cursor-pointer' : 'cursor-default'}"
onclick={() => hasDetail && toggle(entry.id)}
>
<!-- status icon -->

View File

@@ -13,6 +13,8 @@ export interface ActivityEntry {
detail?: string
timestamp: number
toolName?: string
stepSeq?: number
indent?: boolean
status: 'running' | 'done' | 'failed'
}
@@ -39,11 +41,23 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
})
}
// Tool calls (from messages)
// Tool calls (from messages). Tag each tool with the plan step that's
// currently running when it fires.
let currentStepSeq = 0
let entryIdx = 0
for (let mi = 0; mi < $msgs.length; mi++) {
for (const t of $msgs[mi].tools) {
// Track current step from update_plan_step calls
if (t.type === 'tool_use' && t.name === 'update_plan_step') {
const s = (t.args as any)?.seq as number | undefined
const status = (t.args as any)?.status as string | undefined
if (s && status === 'running') currentStepSeq = s
} else if (t.name === 'set_goal' || t.name === 'propose_plan' || t.name === 'complete_task') {
currentStepSeq = 0
}
const label = toolActivityLabel(t)
const stepTag = currentStepSeq > 0 ? currentStepSeq : undefined
if (t.type === 'tool_use') {
entries.push({
id: t.id ?? `tool_${mi}_${entryIdx++}`,
@@ -51,10 +65,11 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
description: label,
timestamp: now - ($msgs.length - mi) * 1000,
toolName: t.name,
stepSeq: stepTag,
indent: stepTag != null,
status: 'running'
})
} else if (t.type === 'tool_result') {
// Find and update matching tool_use entry
const running = entries.find((e) =>
e.type === 'tool_running' && e.id === t.id && e.status === 'running'
)
@@ -76,6 +91,8 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
detail: !t.error ? (typeof t.result === 'string' ? t.result.slice(0, 200) : '') : undefined,
timestamp: now - ($msgs.length - mi) * 1000,
toolName: t.name,
stepSeq: stepTag,
indent: stepTag != null,
status: t.error ? 'failed' : 'done'
})
}

View File

@@ -6,7 +6,6 @@
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
import InlineApproval from '$lib/components/InlineApproval.svelte'
import AgentIndicator from '$lib/components/AgentIndicator.svelte'
import { getToolRenderer } from '$lib/tool-renderers'
import { Button } from '$lib/components/ui/button'
import { Textarea } from '$lib/components/ui/textarea'
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up'
@@ -45,14 +44,6 @@
scrolledUp = false
}
const MAX_INLINE_CARDS = 5
function getInlineTools(msg: { tools: any[] }): any[] {
const matched = msg.tools.filter((t) => getToolRenderer(t))
if (matched.length <= MAX_INLINE_CARDS) return matched
return matched.slice(0, MAX_INLINE_CARDS)
}
// Resizable right rail (session graph). Persisted so it survives reloads.
const RAIL_MIN = 260
const RAIL_MAX = 620
@@ -147,12 +138,6 @@
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap">{msg.text}</div>
{:else}
<div class="flex w-full flex-col gap-2">
{#each getInlineTools(msg) as tool (tool.id)}
{@const renderer = getToolRenderer(tool)}
{#if renderer}
<renderer.component {tool} />
{/if}
{/each}
{#if msg.text}
<div class="prose-chat max-w-none text-sm leading-relaxed">
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->