feat(mothership): server-persisted unread task indicators via SSE#3549
feat(mothership): server-persisted unread task indicators via SSE#3549waleedlatif1 merged 10 commits intofeat/mothership-copilotfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Introduces a task status event pipeline: a new Redis (or local fallback) Updates chat/task mutations and streaming paths to publish status events ( Written by Cursor Bugbot for commit 59bae28. Configure here. |
Greptile SummaryThis PR introduces server-persisted unread task indicators via Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser as Browser (Sidebar)
participant SSE as /api/mothership/events (SSE)
participant ChatAPI as /api/mothership/chat
participant Redis as Redis Pub/Sub
participant ReadAPI as /api/mothership/chats/read
participant DB as PostgreSQL
Browser->>SSE: GET /api/mothership/events?workspaceId=...
SSE->>DB: check session + workspace permissions
SSE-->>Browser: 200 text/event-stream (connected)
Note over SSE,Redis: taskPubSub.onStatusChanged registered
Browser->>ChatAPI: POST /api/mothership/chat (send message)
ChatAPI->>DB: update conversationId (set stream ID)
ChatAPI->>Redis: publishStatusChanged('started')
Redis-->>SSE: task:status_changed event
SSE-->>Browser: event: task_status (type: started)
Browser->>Browser: invalidateQueries → isActive=true, dot pulses
ChatAPI-->>Browser: SSE stream chunks (content)
Note over Browser: isSending=true, wasSendingRef=true
ChatAPI->>DB: update messages + updatedAt, clear conversationId
ChatAPI->>Redis: publishStatusChanged('completed')
Redis-->>SSE: task:status_changed event
SSE-->>Browser: event: task_status (type: completed)
Browser->>Browser: invalidateQueries → isUnread=true (updatedAt > lastSeenAt)
Note over Browser: isSending→false (stream ended before SSE event)
Browser->>ReadAPI: POST /api/mothership/chats/read (chatId)
Note over Browser: onMutate: optimistic isUnread=false
ReadAPI->>DB: UPDATE lastSeenAt=NOW()
ReadAPI-->>Browser: { success: true }
Browser->>Browser: onSettled: invalidateQueries → isUnread=false confirmed
|
Replace fragile client-side polling + timer-based green flash with server-persisted lastSeenAt semantics, real-time SSE push via Redis pub/sub, and dot overlay UI on the Blimp icon. - Add lastSeenAt column to copilotChats for server-persisted read state - Add Redis/local pub/sub singleton for task status events (started, completed, created, deleted, renamed) - Add SSE endpoint (GET /api/mothership/events) with heartbeat and workspace-scoped filtering - Add mark-read endpoint (POST /api/mothership/chats/read) - Publish SSE events from chat, rename, delete, and auto-title handlers - Add useTaskEvents hook for client-side SSE subscription - Add useMarkTaskRead mutation with optimistic update - Replace timer logic in sidebar with TaskStatus state machine (running/unread/idle) and dot overlay using brand color variables - Mark tasks read on mount and stream completion in home page - Fix security: add userId check to delete WHERE clause - Fix: bump updatedAt on stream completion - Fix: set lastSeenAt on rename to prevent false-positive unread Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
183a530 to
61aac5f
Compare
- Return 404 when delete finds no matching chat (was silent no-op) - Move log after ownership check so it only fires on actual deletion - Publish completed SSE event from stop route so sidebar dot clears on abort Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
|
@cursor review |
Existing rows would have last_seen_at = NULL after migration, causing all past completed tasks to show as unread. Backfill sets last_seen_at to updated_at for all existing rows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
|
@cursor review |
…navigation - Pass updatedAt explicitly alongside lastSeenAt on chat creation so both use the same JS timestamp (DB defaultNow() ran later, causing updatedAt > lastSeenAt → false unread) - Reset wasSendingRef when chatId changes to prevent a stale true from task A triggering a redundant markRead on task B Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
|
@cursor review |
… SSE URL Expose resolvedChatId from useChat so home.tsx can mark-read even when chatId prop stays undefined after replaceState URL update. Also URL-encode workspaceId in EventSource URL as a defensive measure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile @cursor review — addressed both comments: mark-read now uses |
…andling Auto-focus the textarea when the initial home view renders. Also fix sidebar task click to always call onMultiSelectClick so selection state stays consistent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@cursor review |
|
@greptile |
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
Outdated
Show resolved
Hide resolved
Auto-title now sets both updatedAt and lastSeenAt (matching the rename route pattern) to prevent false-positive unread dots. Also move the 'started' SSE event inside the if(updated) guard so it only fires when the DB update actually matched a row. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile fixed auto-title |
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
- Extract createPubSubChannel factory (lib/events/pubsub.ts) to eliminate duplicated Redis/EventEmitter boilerplate between task and MCP pub/sub - Extract createWorkspaceSSE factory (lib/events/sse-endpoint.ts) to share auth, heartbeat, and cleanup logic across SSE endpoints - Fix auto-title race suppressing unread status by removing updatedAt/lastSeenAt from title-only DB update - Fix wheel event listener leak in ResourceTabs (RefCallback cleanup was silently discarded) - Fix getFullSelection() missing taskIds (inconsistent with hasAnySelection) - Deduplicate SSE_RESPONSE_HEADERS to spread from shared SSE_HEADERS - Hoist isSttAvailable to module-level constant to avoid per-render IIFE Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Summary
lastSeenAtDB column — survives reload, multi-tab, multi-device--brand-tertiary-2= running, solid--brand-tertiary= unread, no dot = idleupdatedAtnow bumped on stream completion;lastSeenAtset on rename to prevent false-positive unreadChanges
Schema
lastSeenAtcolumn tocopilotChatstable + migrationServer
lib/copilot/task-events.ts— Redis/local pub/sub singleton for task status eventsapi/mothership/events— SSE endpoint with workspace filtering, heartbeat, connection trackingapi/mothership/chats/read— Mark-read endpoint (setslastSeenAt)Client
hooks/use-task-events.ts— SSE subscription hook that invalidates task queries on eventshooks/queries/tasks.ts—isActive/isUnreadderived state,useMarkTaskReadoptimistic mutationTaskStatusIconwith dot overlay,isCurrentRoutesuppresses dots when viewingTest plan