Files
FabledScribe/docs/superpowers/specs/2026-04-03-chatpanel-design.md
T
2026-04-03 15:24:30 -04:00

9.4 KiB
Raw Blame History

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 34 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

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.

// 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 → <ChatPanel variant="full">

  • 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 → <ChatPanel variant="full" briefingMode />

  • 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 → <ChatPanel variant="full" :projectId="projectId">

  • Remove: inline chat input, streaming watch, TTS wiring
  • Keep: 3-panel grid layout, task panel, note editor panel
  • ChatPanel takes the centre column

HomeView → <ChatPanel variant="widget">

  • 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
  └─ <ChatPanel :convId="convId" variant="full|widget">
       ├─ 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