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:
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { activityLog, type ActivityEntry } from '$lib/stores/activity'
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
||||
import Spinner from './Spinner.svelte'
|
||||
import CircleDotIcon from '@lucide/svelte/icons/circle-dot'
|
||||
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
||||
import TargetIcon from '@lucide/svelte/icons/target'
|
||||
import MilestoneIcon from '@lucide/svelte/icons/milestone'
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
|
||||
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
||||
import HelpCircleIcon from '@lucide/svelte/icons/help-circle'
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
function typeIcon(type: ActivityEntry['type']) {
|
||||
switch (type) {
|
||||
case 'goal': return TargetIcon
|
||||
case 'goal': return MilestoneIcon
|
||||
case 'plan': return ListTodoIcon
|
||||
case 'step_running': case 'step_done': case 'step_failed':
|
||||
case 'tool_running': case 'tool_done': case 'tool_error':
|
||||
@@ -36,29 +36,55 @@
|
||||
}
|
||||
|
||||
function statusColor(status: ActivityEntry['status']) {
|
||||
if (status === 'running') return 'text-primary'
|
||||
if (status === 'failed') return 'text-destructive'
|
||||
return 'text-success'
|
||||
return 'text-primary'
|
||||
}
|
||||
|
||||
// Tool results often arrive as a JSON string — pretty-print it when it
|
||||
// parses, otherwise fall back to the raw text rather than hiding it.
|
||||
function prettyPrint(raw: string): string {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(raw), null, 2)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(ts: number): string | null {
|
||||
if (!ts) return null
|
||||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{#if $activityLog.length === 0}
|
||||
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div class="size-1 rounded-full bg-muted-foreground/20 animate-pulse" style="animation-delay:0s" />
|
||||
<div class="size-1 rounded-full bg-muted-foreground/30 animate-pulse" style="animation-delay:0.15s" />
|
||||
<div class="size-1 rounded-full bg-muted-foreground/20 animate-pulse" style="animation-delay:0.3s" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Waiting for activity…</p>
|
||||
<div class="flex flex-col items-center gap-3 px-3 py-8 text-center">
|
||||
<svg viewBox="0 0 64 110" class="h-20 w-auto text-muted-foreground/40" fill="none">
|
||||
<line x1="32" y1="8" x2="32" y2="102" stroke="currentColor" stroke-width="1" stroke-dasharray="2.5 4" opacity="0.35" />
|
||||
<circle cx="32" cy="22" r="4" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="32" cy="55" r="4" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<animate attributeName="r" values="4;11;4" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.6;0;0.6" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="32" cy="88" r="4" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
<p class="max-w-[12rem] text-xs leading-relaxed text-muted-foreground">Waiting for activity…</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col py-1">
|
||||
{#each $activityLog as entry, i (entry.id)}
|
||||
{@const isLast = i === $activityLog.length - 1}
|
||||
{@const hasDetail = !!entry.detail}
|
||||
{@const icon = typeIcon(entry.type)}
|
||||
{@const isOpen = expanded.has(entry.id)}
|
||||
{@const time = formatTime(entry.timestamp)}
|
||||
<div class="relative">
|
||||
<!-- connector line -->
|
||||
{#if !isLast}
|
||||
@@ -66,39 +92,56 @@
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
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)}
|
||||
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 cursor-pointer"
|
||||
onclick={() => toggle(entry.id)}
|
||||
>
|
||||
<!-- status icon -->
|
||||
<span class="relative mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full {statusColor(entry.status)}">
|
||||
{#if entry.status === 'running'}
|
||||
<LoaderCircleIcon class="size-3.5 animate-spin" />
|
||||
<Spinner class="size-3.5" />
|
||||
{:else if entry.status === 'failed'}
|
||||
<CircleXIcon class="size-3.5" />
|
||||
{:else if icon}
|
||||
<svelte:component this={icon} class="size-3" />
|
||||
{:else}
|
||||
<CircleCheckIcon class="size-3" />
|
||||
<CircleDotIcon class="size-3" />
|
||||
{/if}
|
||||
</span>
|
||||
<!-- description -->
|
||||
<span class="min-w-0 flex-1 leading-snug {entry.status === 'done' ? 'text-muted-foreground' : ''}">
|
||||
{entry.description}
|
||||
</span>
|
||||
{#if hasDetail}
|
||||
<span class="mt-0.5 shrink-0 text-muted-foreground">
|
||||
{#if expanded.has(entry.id)}
|
||||
<ChevronDownIcon class="size-3" />
|
||||
{:else}
|
||||
<ChevronRightIcon class="size-3" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="mt-0.5 shrink-0 text-muted-foreground">
|
||||
{#if isOpen}
|
||||
<ChevronDownIcon class="size-3" />
|
||||
{:else}
|
||||
<ChevronRightIcon class="size-3" />
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
<!-- detail (collapsed) -->
|
||||
{#if hasDetail && expanded.has(entry.id)}
|
||||
<div class="pl-8 pr-3 pb-1">
|
||||
<pre class="whitespace-pre-wrap break-all rounded bg-muted/50 p-2 font-mono text-[10px] text-muted-foreground">{entry.detail}</pre>
|
||||
<!-- detail -->
|
||||
{#if isOpen}
|
||||
<div class="flex flex-col gap-1.5 pl-8 pr-3 pb-2">
|
||||
<div class="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||
<span class="capitalize">{entry.status}</span>
|
||||
{#if time}<span aria-hidden="true">·</span><span>{time}</span>{/if}
|
||||
{#if entry.toolName}<span aria-hidden="true">·</span><code class="font-mono">{entry.toolName}</code>{/if}
|
||||
</div>
|
||||
{#if entry.args}
|
||||
<div>
|
||||
<p class="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">Called with</p>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 font-mono text-[10px] leading-relaxed text-muted-foreground">{prettyPrint(entry.args)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.detail}
|
||||
<div>
|
||||
{#if entry.args}<p class="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">{entry.status === 'failed' ? 'Error' : 'Result'}</p>{/if}
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 font-mono text-[10px] leading-relaxed text-muted-foreground">{prettyPrint(entry.detail)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !entry.args && !entry.detail}
|
||||
<p class="text-[10px] text-muted-foreground/70">No further detail for this step.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user