diff --git a/docs/superpowers/specs/2026-08-12-running-conversation-indicator-design.md b/docs/superpowers/specs/2026-08-12-running-conversation-indicator-design.md new file mode 100644 index 0000000..7a56e46 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-running-conversation-indicator-design.md @@ -0,0 +1,100 @@ +# Running-Conversation Indicator — Design + +Date: 2026-08-12 +Status: Approved (approach A: wire existing infrastructure) + +## Goal + +A conversation whose run is executing gives no visual signal today. Add a +lightweight pulsing "running" indicator in **two places**: + +1. **Thread list rows** — a dot next to the title of any thread with an active + run (including runs started by other tabs or scheduled tasks). +2. **Global header** — a dot next to the logo/wordmark whenever the *currently + open* conversation is running, shown in **all views** (chat, Skills, Memory, + Schedule) so switching away doesn't hide that work is in flight. + +Decisions locked in during brainstorming: + +- **Style:** lightweight pulsing dot only — no banner text, no extra stop + button (the composer's stop affordance already exists). +- **Approach:** wire up the already-built (but currently unconsumed) running + state infrastructure. No backend changes. + +## Existing infrastructure (why no backend work is needed) + +- `useChat.ts` already calls `setThreadRunning(threadId, isRunLoading)` on its + own run lifecycle — a zero-latency local store + (`useThreads.ts:99-126`, covered by `useThreads.running.test.ts`). +- `useBusyThreadIds()` (`useThreads.ts:78-97`) polls + `/api/conversations?status=busy&limit=100` on a 10s heartbeat, covering runs + started out-of-band (other tabs, scheduled tasks). +- Neither hook is consumed by any UI today — this feature is wiring + rendering. + +## Components + +### 1. `useRunningThreadIds()` — new hook in `src/app/hooks/useThreads.ts` + +Merges the two sources into one `ReadonlySet`: + +```ts +const local = useLocalRunningThreadIds(); +const busy = useBusyThreadIds(); +return useMemo(() => new Set([...busy, ...local]), [busy, local]); +``` + +Single call site per consumer; the merge is the only new logic. SWR dedupes +the heartbeat request across multiple mounts, so calling this hook from both +ThreadList and page.tsx is safe. + +### 2. `RunningDot` — new presentational component + +`src/app/components/RunningDot.tsx`. Standard ping pattern: + +- outer `span.relative.flex.size-2` with `role="status"`, i18n `aria-label` + and `title`; +- inner `animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-500 opacity-75`; +- inner solid `relative inline-flex size-2 rounded-full bg-blue-500`. + +Blue matches the existing `STATUS_COLORS.busy` in ThreadList. The component +takes the translated label as a prop so callers use their own i18n domain. + +### 3. Thread list row (`src/app/components/ThreadList.tsx`) + +- Call `useRunningThreadIds()` once at the top of `ThreadList`. +- In `renderThreadCard`, render `` inside the title `

`, + after the pin icon and before the title text, when + `runningIds.has(thread.id)`. + +### 4. Global header (`src/app/[locale]/page.tsx`) + +- Call `useRunningThreadIds()` in `HomePageInner`. +- Render `` in the header's left cluster (after the logo/wordmark + block) when `threadId && runningIds.has(threadId)`. +- No view gating: the dot shows in every view while the open thread runs. + +## i18n + +One new message per domain, both locales: + +- `threadList.running` — e.g. en "Running", zh "正在执行" (used by the row dot). +- `header.running` — same text (used by the header dot). + +## Error handling + +Nothing new. A failed busy heartbeat yields an empty busy set (dot hides until +the next successful poll); the local store still covers this client's own runs, +so the common case never regresses on a heartbeat failure. + +## Testing + +- Extend the pattern of `useThreads.running.test.ts` with a unit test for + `useRunningThreadIds`: local-only, busy-only, and merged membership. +- Existing suites (`useThreads.running.test.ts`, tsc, eslint) must stay green. + +## Out of scope + +- Banner/text prompts, header stop button. +- Push/SSE real-time busy events (10s heartbeat is sufficient). +- Per-row indicators for `interrupted`/`error` status (already surfaced via the + "Requiring Attention" group and status filter).