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:
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user