feat(web): redesign task rail — Plan/Event log polish, entity graph resize fix

- Plan/Activity panels: humanize step titles, richer icons, empty states
  matching Scope's illustration style, pretty-printed expandable detail
- Activity log renamed to Event log; every step now expandable
- Chat: middle-truncate header title, remove redundant task-list rail and
  header stat cluster (duplicated in the sidebar), simplify markdown styling
- Fix --font-mono actually being a monospace font (was aliased to DM Sans)
- Replace rotating loader-circle spinner with a smoother fading-blade Spinner
- SessionGraph entity detail panel: resizable and self-clamping against its
  live container size (was overflowing into sibling sections), close button
- Dev launch config: fetch bearer token from the running api container so
  `npm run dev` works against the local compose stack without a hardcoded
  secret in a tracked file

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 08:22:08 +02:00
parent 3d99282897
commit 6a8efd22bb
16 changed files with 372 additions and 217 deletions

View File

@@ -12,6 +12,7 @@ export interface ActivityEntry {
'approval'
description: string
detail?: string
args?: string
timestamp: number
toolName?: string
stepSeq?: number
@@ -19,6 +20,26 @@ export interface ActivityEntry {
status: 'running' | 'done' | 'failed'
}
// Detail text is kept full-length (not hard-truncated to a preview snippet)
// so the expanded view has something worth pretty-printing — capped only as
// a safety net against pathological payloads (a full fleet dump, etc).
const DETAIL_MAX = 8000
function summarizeArgs(args: unknown): string | undefined {
if (!args || typeof args !== 'object' || Array.isArray(args)) return undefined
if (Object.keys(args).length === 0) return undefined
try {
return JSON.stringify(args)
} catch {
return undefined
}
}
function stringifyResult(result: unknown): string {
const s = typeof result === 'string' ? result : JSON.stringify(result ?? '')
return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… truncated` : s
}
export const activityLog = derived([messages, planSteps, currentTask], ([$msgs, $steps, $task]) => {
const entries: ActivityEntry[] = []
const now = Date.now()
@@ -64,6 +85,7 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
id: t.id ?? `tool_${mi}_${entryIdx++}`,
type: 'tool_running',
description: label,
args: summarizeArgs(t.args),
timestamp: now - ($msgs.length - mi) * 1000,
toolName: t.name,
stepSeq: stepTag,
@@ -77,19 +99,25 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
if (running && t.error) {
running.type = 'tool_error'
running.status = 'failed'
running.description = `${t.name}: ${t.error.slice(0, 80)}`
running.description = `${label}: ${t.error.slice(0, 80)}`
running.detail = t.error
} else if (running) {
running.type = 'tool_done'
running.status = 'done'
running.detail = typeof t.result === 'string'
? t.result.slice(0, 200)
: JSON.stringify(t.result ?? '').slice(0, 200)
running.detail = stringifyResult(t.result)
} else {
// Historical/persisted tool calls arrive as one merged record (args
// + result on the same object, see mergeToolCalls in chat.ts) rather
// than a separate tool_use/tool_result pair — there's never a
// "running" entry to attach to, so this branch has to build the
// full entry itself. It used to fall back to the raw tool name
// (e.g. "get_entity") instead of the humanized label here.
entries.push({
id: t.id ?? `tool_${mi}_${entryIdx++}`,
type: t.error ? 'tool_error' : 'tool_done',
description: t.error ? `${t.name}: ${t.error.slice(0, 80)}` : t.name,
detail: !t.error ? (typeof t.result === 'string' ? t.result.slice(0, 200) : '') : undefined,
description: t.error ? `${label}: ${t.error.slice(0, 80)}` : label,
detail: t.error ? t.error : stringifyResult(t.result),
args: summarizeArgs(t.args),
timestamp: now - ($msgs.length - mi) * 1000,
toolName: t.name,
stepSeq: stepTag,
@@ -181,6 +209,8 @@ function toolActivityLabel(t: ToolCallResult): string {
case 'complete_task': return 'Complete task'
case 'ping_service': return 'Check service'
case 'ask_operator': return 'Ask operator'
default: return t.name
// Unmapped tool (new/uncommon) — humanize the raw name rather than
// showing it verbatim, e.g. "revoke_execution" -> "Revoke execution".
default: return t.name.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
}
}