feat(tasks): task board UI — chat window becomes Tasks + card grid

Reframes the chat surface as tasks:
- New Tasks.svelte: a card grid of tasks, each showing status (Running /
  Needs input / Done / Failed), the goal as title, the outcome summary, and
  relative time; filterable by status with live counts; delete on hover;
  "New task" and per-card click open the conversation.
- Board updates LIVE off the events stream (goal.set / task.status /
  question.*) via an explicit liveEvents.subscribe with a debounced refetch —
  scanning all events newer than the last seen, since entity.touched bursts
  bury task events below index 0.
- App shell: primary nav "Chat" → "Tasks" (board is now the home route),
  "New chat" → "New task", conversation header gets a Tasks / Conversation
  breadcrumb. Removed the superseded Sessions page.
- api.ts Session type carries the task fields (goal/status/outcome/summary).

Also fixes a pre-existing SSE bug that blocked ALL live updates app-wide:
writeSSE emitted `event: <type>`, which EventSource only delivers to
addEventListener(type) handlers — but stores/events.ts (and every page reading
liveEvents) consumes via onmessage, which never fires for named events. So the
live stream delivered nothing to the UI. Dropped the event-name line; the type
is already in the JSON payload, and new event types now need zero client
changes. SSE test still green (it parses data: lines).

Verified in the browser against the live stack: the board renders 50 tasks
with correct status buckets; a goal-driven task appears and flips to a Done
card with its summary in real time without a reload; Events page confirms the
stream now delivers to onmessage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:22:27 +02:00
parent 014e5c74e0
commit 413bf54daf
5 changed files with 251 additions and 185 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Chat from './pages/Chat.svelte'
import Sessions from './pages/Sessions.svelte'
import Tasks from './pages/Tasks.svelte'
import Overview from './pages/Overview.svelte'
import Entities from './pages/Entities.svelte'
import Events from './pages/Events.svelte'
@@ -23,6 +23,7 @@
import { Separator } from '$lib/components/ui/separator'
import { Toaster } from '$lib/components/ui/sonner'
import PlusIcon from '@lucide/svelte/icons/plus'
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
import MessageSquareIcon from '@lucide/svelte/icons/message-square'
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard'
import DatabaseIcon from '@lucide/svelte/icons/database'
@@ -36,7 +37,7 @@
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text'
import TrendingUpIcon from '@lucide/svelte/icons/trending-up'
let page = $state('chat')
let page = $state('tasks')
let routeParam = $state('')
let drawerOpen = $state(false)
@@ -45,9 +46,9 @@
onMount(() => {
function sync() {
const path = location.hash.slice(2) || 'chat'
const path = location.hash.slice(2) || 'tasks'
const [head, ...rest] = path.split('/')
page = head || 'chat'
page = head || 'tasks'
routeParam = rest.join('/')
}
sync()
@@ -106,12 +107,12 @@
<Sidebar.MenuButton
class="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
onclick={() => { newChat(); navigate('chat') }}
tooltipContent="New chat"
tooltipContent="New task"
>
{#snippet child({ props })}
<button {...props}>
<PlusIcon />
<span>New chat</span>
<span>New task</span>
</button>
{/snippet}
</Sidebar.MenuButton>
@@ -123,11 +124,11 @@
<Sidebar.Group>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={page === 'chat'} onclick={() => navigate('chat')} tooltipContent="Chat">
<Sidebar.MenuButton isActive={page === 'tasks' || page === 'chat'} onclick={() => navigate('tasks')} tooltipContent="Tasks">
{#snippet child({ props })}
<button {...props}>
<MessageSquareIcon />
<span>Chat</span>
<ListTodoIcon />
<span>Tasks</span>
</button>
{/snippet}
</Sidebar.MenuButton>
@@ -167,7 +168,13 @@
<header class="flex h-(--header-height) shrink-0 items-center gap-1 border-b px-4 lg:gap-2 lg:px-6">
<Sidebar.Trigger class="-ms-1" />
<Separator orientation="vertical" class="mx-2 data-[orientation=vertical]:h-4" />
<span class="text-base font-medium capitalize">{page === 'entity' ? routeParam : page}</span>
{#if page === 'chat'}
<button type="button" class="text-sm text-muted-foreground hover:text-foreground" onclick={() => navigate('tasks')}>Tasks</button>
<span class="text-muted-foreground">/</span>
<span class="text-base font-medium">Conversation</span>
{:else}
<span class="text-base font-medium capitalize">{page === 'entity' ? routeParam : page}</span>
{/if}
<div class="ms-auto flex items-center gap-2.5">
{#if $summary}
<div class="hidden items-center gap-2.5 text-xs text-muted-foreground sm:flex">
@@ -195,6 +202,8 @@
<main class="min-h-0 flex-1 overflow-hidden">
{#if page === 'overview'}
<Overview />
{:else if page === 'tasks'}
<Tasks />
{:else if page === 'entities'}
<Entities />
{:else if page === 'graph'}
@@ -207,8 +216,6 @@
<Signals />
{:else if page === 'events'}
<Events />
{:else if page === 'sessions'}
<Sessions />
{:else if page === 'agent'}
<Agent />
{:else if page === 'knowledge'}