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:
@@ -8,8 +8,13 @@
|
||||
import ActivityTimeline from './ActivityTimeline.svelte'
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||
import TargetIcon from '@lucide/svelte/icons/target'
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||
import MilestoneIcon from '@lucide/svelte/icons/milestone'
|
||||
import Spinner from './Spinner.svelte'
|
||||
import CircleIcon from '@lucide/svelte/icons/circle'
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
||||
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
||||
import CircleSlashIcon from '@lucide/svelte/icons/circle-slash'
|
||||
import CirclePauseIcon from '@lucide/svelte/icons/circle-pause'
|
||||
|
||||
onMount(() => startWorkspace())
|
||||
|
||||
@@ -55,6 +60,18 @@
|
||||
// Plan collapsed status
|
||||
const planDone = $derived($planSteps.filter((s) => s.status === 'done').length)
|
||||
const planTotal = $derived($planSteps.length)
|
||||
const planPct = $derived(planTotal > 0 ? Math.round((planDone / planTotal) * 100) : 0)
|
||||
|
||||
// When there are no plan steps, the empty state depends on WHY: a task that's
|
||||
// actively planning (or streaming its first turn) is genuinely waiting for one,
|
||||
// but a finished task that never planned (a read-only lookup, a direct answer)
|
||||
// will never get one — a perpetual "Awaiting plan…" there is misleading.
|
||||
const planPhase = $derived.by<'drafting' | 'none' | 'idle'>(() => {
|
||||
const st = $currentTask?.status
|
||||
if (st === 'done' || st === 'failed' || st === 'abandoned') return 'none'
|
||||
if (st === 'planning' || $streaming) return 'drafting'
|
||||
return 'idle'
|
||||
})
|
||||
|
||||
// Activity collapsed status
|
||||
const activityRunning = $derived($activityLog.filter((e) => e.status === 'running').length)
|
||||
@@ -113,44 +130,95 @@
|
||||
{#if planOpen}
|
||||
<div style="height: {heights[1]}px" class="flex flex-col overflow-y-auto">
|
||||
{#if $currentTask?.goal}
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 text-xs">
|
||||
<TargetIcon class="size-3 shrink-0 text-muted-foreground" />
|
||||
<span class="truncate text-muted-foreground">{$currentTask.goal}</span>
|
||||
<div class="flex items-start gap-2 px-3 py-2">
|
||||
<MilestoneIcon class="mt-0.5 size-3 shrink-0 text-primary" />
|
||||
<span class="text-xs leading-snug text-foreground/90">{$currentTask.goal}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if planTotal > 0}
|
||||
<div class="flex items-center justify-between px-3 pb-1 text-[11px] text-muted-foreground">
|
||||
<span>{planDone}/{planTotal} done</span>
|
||||
<div class="px-3 pb-2.5">
|
||||
<div class="mb-1.5 flex items-baseline justify-between text-[11px]">
|
||||
<span class="font-medium text-foreground">{planDone} of {planTotal} done</span>
|
||||
<span class="tabular-nums text-muted-foreground">{planPct}%</span>
|
||||
</div>
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary transition-all duration-500 ease-out" style="width: {planPct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-3 h-1 rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary transition-all duration-500" style="width: {planTotal > 0 ? Math.round((planDone / planTotal) * 100) : 0}%"></div>
|
||||
</div>
|
||||
<ol class="flex flex-col gap-0.5 overflow-y-auto px-3 py-1 text-[11px]">
|
||||
{#each $planSteps as step (step.id)}
|
||||
<li class="flex items-start gap-1.5 {step.status === 'done' ? 'text-muted-foreground line-through decoration-muted-foreground/40' : ''}">
|
||||
<span class="mt-0.5 shrink-0">
|
||||
{#if step.status === 'done'}
|
||||
<div class="size-2 rounded-full bg-success" />
|
||||
{:else if step.status === 'running'}
|
||||
<LoaderCircleIcon class="size-2.5 animate-spin text-primary" />
|
||||
<ol class="flex flex-col overflow-y-auto px-2 pb-2 text-[11px]">
|
||||
{#each $planSteps as step, i (step.id)}
|
||||
{@const isDone = step.status === 'done'}
|
||||
{@const isRunning = step.status === 'running'}
|
||||
<li class="relative flex items-start gap-2.5 rounded-md px-2 py-1.5 transition-colors {isRunning ? 'bg-primary/5' : ''}">
|
||||
{#if i < $planSteps.length - 1}
|
||||
<span class="pointer-events-none absolute bottom-[-2px] left-[13.5px] top-[22px] w-px bg-border" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="relative z-10 mt-px flex size-3.5 shrink-0 items-center justify-center rounded-full bg-background">
|
||||
{#if isDone}
|
||||
<CircleCheckIcon class="size-3.5 text-primary" />
|
||||
{:else if isRunning}
|
||||
<Spinner class="size-3.5 text-primary" />
|
||||
{:else if step.status === 'failed'}
|
||||
<div class="size-2 rounded-full bg-destructive" />
|
||||
<CircleXIcon class="size-3.5 text-destructive" />
|
||||
{:else if step.status === 'blocked'}
|
||||
<CirclePauseIcon class="size-3.5 text-warning" />
|
||||
{:else if step.status === 'skipped' || step.status === 'replaced'}
|
||||
<CircleSlashIcon class="size-3.5 text-muted-foreground" />
|
||||
{:else}
|
||||
<div class="size-2 rounded-full bg-muted-foreground/30" />
|
||||
<CircleIcon class="size-3.5 text-muted-foreground/40" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="leading-tight">{step.title}</span>
|
||||
<span
|
||||
class="min-w-0 flex-1 leading-snug {isDone
|
||||
? 'text-muted-foreground line-through decoration-muted-foreground/40'
|
||||
: isRunning
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
>{step.title}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{:else if planPhase === 'drafting'}
|
||||
<div class="flex flex-col items-center gap-3 px-3 py-6 text-center">
|
||||
<svg viewBox="0 0 140 88" class="h-16 w-auto text-primary" fill="none">
|
||||
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
||||
<circle cx="16" cy="20" r="4.5" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.35;1;0.35" dur="1.3s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<line x1="30" y1="20" x2="124" y2="20" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.15;0.5;0.15" dur="1.3s" repeatCount="indefinite" />
|
||||
</line>
|
||||
<circle cx="16" cy="44" r="4.5" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.35;1;0.35" dur="1.3s" begin="0.25s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<line x1="30" y1="44" x2="102" y2="44" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.15;0.5;0.15" dur="1.3s" begin="0.25s" repeatCount="indefinite" />
|
||||
</line>
|
||||
<circle cx="16" cy="68" r="4.5" fill="currentColor">
|
||||
<animate attributeName="opacity" values="0.35;1;0.35" dur="1.3s" begin="0.5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<line x1="30" y1="68" x2="80" y2="68" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.15;0.5;0.15" dur="1.3s" begin="0.5s" repeatCount="indefinite" />
|
||||
</line>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="text-xs text-muted-foreground">Drafting a plan…</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
|
||||
<div class="flex gap-1">
|
||||
<div class="size-1.5 rounded-full bg-muted-foreground/20" />
|
||||
<div class="size-1.5 rounded-full bg-muted-foreground/30" />
|
||||
<div class="size-1.5 rounded-full bg-muted-foreground/20" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Awaiting plan…</p>
|
||||
<div class="flex flex-col items-center gap-3 px-3 py-6 text-center">
|
||||
<svg viewBox="0 0 140 88" class="h-16 w-auto text-muted-foreground/40" fill="none">
|
||||
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
||||
<circle cx="16" cy="20" r="4.5" fill="currentColor" opacity="0.7" />
|
||||
<line x1="30" y1="20" x2="124" y2="20" opacity="0.35" />
|
||||
<circle cx="16" cy="44" r="4.5" fill="currentColor" opacity="0.45" />
|
||||
<line x1="30" y1="44" x2="102" y2="44" opacity="0.25" />
|
||||
<circle cx="16" cy="68" r="4.5" fill="none" opacity="0.3" />
|
||||
<line x1="30" y1="68" x2="80" y2="68" opacity="0.15" stroke-dasharray="2.5 3.5" />
|
||||
</g>
|
||||
</svg>
|
||||
<p class="max-w-[14rem] text-xs leading-relaxed text-muted-foreground">
|
||||
{planPhase === 'none' ? 'Handled directly — no plan needed' : 'No plan for this task yet'}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -172,10 +240,10 @@
|
||||
onclick={() => (activityOpen = !activityOpen)}
|
||||
>
|
||||
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||
<span>Activity</span>
|
||||
<span>Event log</span>
|
||||
{#if !activityOpen}
|
||||
{#if $streaming && activityRunning > 0}
|
||||
<LoaderCircleIcon class="size-3 animate-spin text-primary" />
|
||||
<Spinner class="size-3 text-primary" />
|
||||
<span class="font-normal normal-case text-primary">{activityRunning} running</span>
|
||||
{:else}
|
||||
<span class="ml-auto font-normal normal-case">{activityCount || '—'} action{activityCount === 1 ? '' : 's'}</span>
|
||||
|
||||
Reference in New Issue
Block a user