docs(webui): running-conversation indicator design spec
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>`:
|
||||
|
||||
```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 `<RunningDot />` inside the title `<h3>`,
|
||||
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 `<RunningDot />` 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).
|
||||
Reference in New Issue
Block a user