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:
@@ -209,12 +209,22 @@ func sqlcEventToGen(ev sqlcgen.Event) gen.Event {
|
||||
// writeSSE writes a single Event as an SSE message. Returns false if the
|
||||
// write failed (client disconnected). flusher may be nil (io.Pipe path,
|
||||
// which has no separate flush step).
|
||||
//
|
||||
// We deliberately DO NOT set the SSE `event:` name field, even though every
|
||||
// event has a type. A named SSE event is only delivered to a matching
|
||||
// addEventListener(type) handler, NOT to EventSource.onmessage — and the whole
|
||||
// frontend (stores/events.ts and every page that reads liveEvents) consumes the
|
||||
// stream via onmessage, reading the type from the JSON payload's `type` field.
|
||||
// Emitting `event: <type>` silently routed every event away from onmessage, so
|
||||
// the live stream delivered nothing to the UI. Leaving the name off sends all
|
||||
// events to onmessage; the type is already in `data`, and new event types need
|
||||
// zero client changes. `id:` is kept for Last-Event-ID reconnection.
|
||||
func writeSSE(w ioWriter, flusher http.Flusher, ev sqlcgen.Event) bool {
|
||||
data, err := json.Marshal(sqlcEventToGen(ev))
|
||||
if err != nil {
|
||||
return true // skip un-serializable events
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "id: %d\nevent: %s\ndata: %s\n\n", ev.ID, ev.Type, data)
|
||||
_, err = fmt.Fprintf(w, "id: %d\ndata: %s\n\n", ev.ID, data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user