From fb26507123063a27892d839599ea8d8cfd19a022 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 3 Apr 2026 15:24:30 -0400 Subject: [PATCH] docs: add ChatPanel unification design spec Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-04-03-chatpanel-design.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-03-chatpanel-design.md diff --git a/docs/superpowers/specs/2026-04-03-chatpanel-design.md b/docs/superpowers/specs/2026-04-03-chatpanel-design.md new file mode 100644 index 0000000..cd7d0a2 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-chatpanel-design.md @@ -0,0 +1,216 @@ +# ChatPanel Unification Design + +**Date:** 2026-04-03 + +## Goal + +Replace the four divergent chat surfaces (ChatView, BriefingView, WorkspaceView, HomeView widget) with a single `ChatPanel` component that encapsulates all chat behaviour — streaming, TTS, PTT, tool calls, thinking blocks, abort — so that fixes and features automatically apply to every context. + +--- + +## Background + +The app currently has four independent chat implementations that have drifted significantly: + +| Surface | File | Gap | +|---|---|---| +| Main chat | `ChatView.vue` | Canonical reference | +| Briefing | `BriefingView.vue` | Had separate TTS impl (now fixed), no PTT, streaming race bug | +| Workspace | `WorkspaceView.vue` | TTS missing until recently, different input wiring | +| Dashboard widget | `HomeView.vue` + `DashboardChatInput.vue` | Separate input component, response rendered manually in parent, no TTS, no PTT | + +Every fix to chat has required touching 3–4 files. This design makes chat a first-class component. + +--- + +## Architecture + +### Component: `ChatPanel.vue` + +A single Vue 3 component that owns the entire chat interaction loop for a given conversation context. Two variants controlled by a `variant` prop: + +- **`full`** — full-height chat: message history, streaming bubble, input bar, all controls +- **`widget`** — compact embedded chat: input bar + compact response area, no history scroll + +Both variants share identical internals: same composables, same store reads, same TTS/PTT/abort logic. + +### Extracted Sub-components + +| Component | Responsibility | +|---|---| +| `ChatInputBar.vue` | Unified input bar: textarea, note picker, PTT mic, send button, abort button | +| `ChatMessageList.vue` | Scrollable message history with auto-scroll, bulk-select (full variant only) | +| `ChatStreamingBubble.vue` | Live streaming content display + thinking block | +| `ChatToolCallList.vue` | Tool call cards, collapsed/expanded state | + +### State Ownership + +`ChatPanel` reads from `useChatStore` directly — it does not accept messages or streaming state as props. This mirrors how all current views work and avoids prop-drilling re-implementation. + +The conversation being displayed is controlled via a `convId` prop. When `convId` is undefined, `ChatPanel` uses `chatStore.currentConversationId`. The parent view sets up the conversation (creates it if needed) and passes the ID down. + +--- + +## Props & Emits Interface + +```typescript +interface ChatPanelProps { + variant: 'full' | 'widget' + convId?: number // which conversation to display; undefined = store current + projectId?: number // workspace: pins RAG scope, passed to sendMessage + briefingMode?: boolean // briefing: hides RAG scope chip, enables briefing-specific send path + placeholder?: string // input placeholder text + autoFocus?: boolean // focus input on mount +} + +interface ChatPanelEmits { + // Emitted when a new conversation is started from the widget (so parent can track convId) + (e: 'conversation-started', convId: number): void +} +``` + +All other behaviour (TTS, PTT, thinking, tool calls, streaming indicator, abort) is always on — not gated by props. The intentional differences between views are expressed only through the props above. + +--- + +## Variant Behaviour + +### `variant="full"` (ChatView, BriefingView, WorkspaceView) + +Layout (top to bottom): +``` +┌────────────────────────────────────────┐ +│ [RAG scope chip / briefing header] │ ← shown unless briefingMode or projectId set +│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ +│ ChatMessageList │ +│ user bubble │ +│ assistant bubble + tool calls │ +│ thinking block (always shown) │ +│ ... │ +│ ChatStreamingBubble (while streaming) │ +│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ +│ ChatInputBar │ +│ [textarea] [note-picker] [mic] [▶] │ +│ [listen toggle] [abort] │ +└────────────────────────────────────────┘ +``` + +### `variant="widget"` (HomeView dashboard) + +Layout (top to bottom, compact): +``` +┌────────────────────────────────────────┐ +│ ChatInputBar (pill style) │ +│ [textarea] [mic] [▶] │ +├────────────────────────────────────────┤ +│ [query text] (after send) │ +│ [streaming / final response text] │ +│ [tool call chips] │ +│ [Continue in Chat →] │ +└────────────────────────────────────────┘ +``` + +The widget variant does NOT show full message history. It shows only the most recent exchange. Once a new conversation is started or the user navigates to `/chat/:id`, the full history is available. + +The `.dashboard-response` section currently in `HomeView.vue` moves inside `ChatPanel` and is rendered when `variant="widget"` and a conversation exists. + +--- + +## TTS / PTT Wiring + +`ChatPanel` instantiates `useStreamingTts` and `useListenMode` internally. These are not passed as props. + +```typescript +// Inside ChatPanel setup() +const listenMode = useListenMode() +const voiceTtsEnabled = computed(() => /* same check as current views */) +const tts = useStreamingTts({ + streamingContent: computed(() => chatStore.streamingContent), + streaming: computed(() => !!chatStore.streaming), + enabled: computed(() => listenMode.value && voiceTtsEnabled.value), +}) +``` + +PTT is handled inside `ChatInputBar` via the existing `useVoiceRecorder` composable (already used in `DashboardChatInput`). On recording stop, the transcribed text is placed in the textarea and auto-submitted. + +--- + +## Per-View Migration + +### ChatView → `` +- Remove: all TTS/PTT/streaming/abort logic, scroll management, input bar template +- Keep: route wiring, conversation list sidebar, bulk-delete UI (sidebar stays in ChatView) +- ChatPanel replaces only the right-hand panel + +### BriefingView → `` +- Remove: streaming watch, TTS, manual scroll, input bar, response persistence workaround +- Keep: history dropdown (today / past briefings), date header +- `briefingMode` hides the RAG scope chip + +### WorkspaceView → `` +- Remove: inline chat input, streaming watch, TTS wiring +- Keep: 3-panel grid layout, task panel, note editor panel +- ChatPanel takes the centre column + +### HomeView → `` +- Remove: `DashboardChatInput` import + usage, `.dashboard-response` section, all manual store wiring (`dashboardConvId`, `dashboardQuery`, `dashboardFinalContent`, `dashboardFinalToolCalls`, `onChatSubmit`) +- Keep: dashboard layout, projects/tasks/events sections +- `DashboardChatInput.vue` deleted + +--- + +## Data Flow + +``` +Parent view + └─ + ├─ reads: useChatStore (messages, streaming, streamingContent, currentConversation) + ├─ ChatMessageList — renders history from store + ├─ ChatStreamingBubble — renders chatStore.streamingContent while streaming + ├─ ChatToolCallList — renders tool calls from streaming + finalized messages + ├─ ChatInputBar + │ ├─ usePtt (mic → textarea → auto-send) + │ └─ emits: submit(content, contextNoteId) + ├─ useStreamingTts (sentence-chunk TTS during streaming) + └─ useListenMode (shared global toggle) +``` + +--- + +## Files Created / Modified + +**Created:** +- `frontend/src/components/ChatPanel.vue` +- `frontend/src/components/ChatInputBar.vue` +- `frontend/src/components/ChatMessageList.vue` +- `frontend/src/components/ChatStreamingBubble.vue` +- (no new composable needed — PTT uses existing `useVoiceRecorder.ts`) + +**Modified:** +- `frontend/src/views/ChatView.vue` — use ChatPanel for the chat area +- `frontend/src/views/BriefingView.vue` — replace chat section with ChatPanel +- `frontend/src/views/WorkspaceView.vue` — replace inline chat with ChatPanel +- `frontend/src/views/HomeView.vue` — replace DashboardChatInput + response section with ChatPanel widget + +**Deleted:** +- `frontend/src/components/DashboardChatInput.vue` + +--- + +## CSS / Styling + +- `ChatPanel` carries its own scoped CSS for both variants +- `ChatInputBar` replicates the pill style currently in `DashboardChatInput` and the flat style in `ChatView` — variant is controlled by a `pill` boolean prop (default false; widget sets it true) +- All existing UI design language tokens (`--color-primary`, `--radius-lg`, Fraunces labels, gradient send button) are preserved + +--- + +## What Does NOT Change + +- Chat store (`useChatStore`) — unchanged +- API client (`client.ts`) — unchanged +- Backend routes — unchanged +- WorkspaceTaskPanel and WorkspaceNoteEditor — unchanged +- Briefing history dropdown and date header — unchanged +- ChatView conversation sidebar and bulk-delete — unchanged +- RAG scope chip logic — moved inside ChatPanel, behaviour identical