feat: multi session chat#391
Conversation
Design for a ChatGPT-style multi-session feature in the embed: device-local storage, responsive sidebar/drawer, opt-in flag with default-on in Flowise core. Covers architecture, data model and v1 -> v2 migration, UI surface, data flows, error handling, and out-of-scope items. Also ignore .superpowers/ (visual brainstorming companion artifacts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply pre-commit prettier formatting that wasn't re-staged into the previous commit. No content changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23 tasks across 8 phases covering storage foundations, store layer, config surface, panel UI, mode-specific layouts, Bot.tsx integration, events/cross-tab, and final wire-up. Verification is manual (per spec Decision #13) via a public/debug-sessions.html harness for pure-logic tasks and demo-page recipes for UI tasks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply pre-commit prettier formatting that wasn't re-staged into the previous commit. No content changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two team-facing docs to accompany the spec and plan: - 2026-04-29-multi-session-chat-decisions.md — option-by-option comparison of every fork in the design with chosen options marked. - 2026-04-29-multi-session-chat-rationale.md — goal, assumptions, constraints, and the throughlines that drove the design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply pre-commit prettier formatting that wasn't re-staged. No content changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement the title derivation utility for multi-session chat feature that extracts the first user message as a concise session title, stripping markdown and truncating to 40 characters with ellipsis. Includes a manual verification harness (debug-sessions.html) that validates all 7 test cases pass when opened in the dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… limitation - Add Array.isArray() validation to readMessages() to prevent undefined behavior when localStorage contains corrupted-but-valid JSON (e.g. objects or primitives typed as MessageType[]). - Update inline JS port of readMessages in debug-sessions.html to match. - Document the harness's inline-port limitation and explain why a real test runner (Vitest + Solid Testing Library) is needed (spec Decision #13). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed readMessages import from sessionStorage and uuid v4 import that were included speculatively but never used in the module. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… BotProps additions Remove scope creep from Task 8: - Delete src/features/full/types.ts (118 lines, never imported) - Revert src/features/popup/types.ts to pre-Task-8 state (minimal PopupParams) Surviving Task 8 changes: - src/components/Bot.tsx: MultiSessionConfig type + multiSession field - src/features/bubble/types.ts: sessionPanel field in ChatWindowTheme (valid) Full and popup types were dead code; popup theme wiring will be added in Task 21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ropagates it Task 8 added the type but missed the runtime schema; without this entry, Object.assign(element, props) sets multiSession on the host element but the SolidElement wrapper never forwards it to the underlying component, so ChatRoot's enabled() always evaluates false. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan Task 11. ChatRoot now creates the session store, lays out a side-by-side panel + Bot when multiSession is enabled, and forwards a derived sessionPanel theme. The theme prop on ChatRootProps is widened to unknown for now — Task 21 will tighten this with the actual mode-specific theme types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Partial pull-forward of plan Task 21, scoped to Full only, so the panel UI (Tasks 11-15) can be visually verified before the Bubble + Popup wiring lands. Demo HTML switches to initFull with multiSession enabled and adds host-sizing CSS for flowise-fullchatbot, which has no built-in :host sizing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-task code review caught two issues: 1. createSessionStore was running on every ChatRoot mount, including the multiSession.enabled !== true fallback path. loadOrMigrate persists a v2 index on first read, so users with the feature disabled were having their localStorage migrated unnecessarily. Factored the panel layout into an inner ChatRootEnabled component so the store is constructed only inside the <Show> truthy branch. 2. Removed the unreachable second leg of the panelTheme cascade (anyProps.chatWindow?.sessionPanel) since no mount surface passes a top-level chatWindow prop; left a forward-looking comment about Task 21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan Task 12. Extracts the For-body from SessionPanel into a SessionListItem component that handles the inline rename input (Enter commits, Esc cancels, blur cancels, empty value falls back to the derived title or "New chat") and the inline delete confirmation prompt. Keyboard nav: Enter switches sessions, Delete opens the confirm prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan Task 13. Adds a capWarning signal + dismissCapWarning action to the session store, fired from newChat after the eviction loop on the first overflow per chatflow (gated by the _capWarned localStorage flag). SessionPanel renders the new CapWarningToast component below the "+ New chat" button. Theme key panelTheme.capWarningText overrides the default copy. Note: this commit also picks up pre-existing Prettier reflows in sessionStore.ts left over from prior pre-commit hook runs; behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… mode Plan Task 14. SessionPanel now reads/writes the panelCollapsed flag from localStorage when isFullPage is true, renders a header caret that toggles between expanded (260px) and collapsed (44px) widths, hides the new-chat button, toast, and session list while collapsed, and animates the width change. The caret is gated on isFullPage so bubble/popup modes (when wired) do not render it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan Task 15. SessionPanel takes new isDrawer/drawerOpen/onDrawerClose props; in drawer mode it renders a backdrop overlay plus an absolutely positioned panel taking 75% width when drawerOpen is true, and renders nothing when closed. handleSwitch auto-closes the drawer on session switch. ChatRoot owns the drawerOpen signal and listens for the flowise-toggle-session-drawer custom event so the chat header's new ☰ button can flip it from inside Bot.tsx. Bot.tsx adds a leading ☰ button to the chat header, gated on multiSession.enabled && !isFullPage; clicking it dispatches the toggle event. Visual verification deferred to Task 21 (which wires ChatRoot into Bubble + Popup, the surfaces where drawer mode applies). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…helper Per-task code review flagged that panelBody is a plain function inside SessionPanel, called from two <Show> branches. JSX + signal reads are fine, but lifecycle primitives or new signals inside the helper would attach to the parent component's owner and could leak across branch toggles. Added a comment to make the constraint explicit so future edits don't introduce that hazard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap <Bot> for <ChatRoot> in Bubble.tsx and pass multiSession and theme props so the drawer/session-panel cascade works in bubble mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fresh sessions in store mode have an empty activeMessages array; reading messagesArray[messagesArray.length - 1].action threw before the user sent their first message. Guard with optional chaining on the last element. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a user submits a message in conversation B, switches to conversation A mid-stream, the streaming events were appending tokens to A (the new active session) because every streaming handler read sessionStore.activeMessages() and called sessionStore.actions.upsertMessage(...) — both keyed implicitly to activeChatId. Fix: capture streamingChatId at submit time. All streaming/post-submit writes go through new upsertMessageInSession(chatId, msg, opts) and removeMessageByIdInSession(chatId, id) actions which only update the activeMessages signal when the target chatId is currently active. The user sees A render cleanly while B continues filling in the background; switching back to B shows the full streamed response. Also replaces the single-timer pendingPersist with a per-chatId Map of debounced writes so streaming into a non-active session still persists correctly. flushPending now flushes all pending writes regardless of session. streamingChatId is pinned at the top of handleSubmit (covering both streaming and non-streaming paths and the post-submit file-upload trim), cleared in closeResponse for streaming, cleared at the end of handleSubmit for the non-streaming success path, and cleared in handleError for any failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In store mode, sessionStore.activeMessages() for a fresh session is empty, so messages().length === 1 (the legacy "fresh, only welcome" gate) was never true — starter prompts only appeared after the user sent the first message and the user-message bubble briefly satisfied length === 1. Several other gates that read length === 1 (form input visibility, rating-disabled) also broke. Fix: prepend a synthetic welcome message to messages() in store mode so the rendered list mirrors fallback-mode semantics (welcome + actual messages). The synthetic welcome is render-only; it is not persisted to the store. handleRegenerateResponse, the only handler that writes the entire array back to the store, strips the welcome before calling replaceActiveMessages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Task 23 manual verification matrix derived from spec Section 8, plus deferred-items / known-limitations notes captured during execution. Updates the design spec status from "Design (pre-implementation)" to "Implemented" with a link to the test plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r polish The session panel now derives its core palette from the chat's user-message brand color (theme.chatWindow.userMessage.backgroundColor, default #3B81F6). A single brand color drives: - "+ New chat" button (solid fill, white text, hover darken) - Active row highlight (brand at 14% alpha) - Hover row highlight (brand at 7% alpha) - Panel background hue (brand at 4% tint over white) - Border (brand at 12% alpha) Each remains overridable via theme.chatWindow.sessionPanel.*. Sidebar UI refresh: - "CHATS" header label (uppercase, muted, smaller) replaces "Conversations" - SVG icons replace ☰ / ⟨ unicode characters (panel-collapse and menu glyphs) - New chat button gains a + icon, slight shadow, hover state - Active row gets a 3px brand-colored left accent bar - Rename / delete are now icon buttons with hover affordance and tooltips - Inline rename input gets a brand-colored focus ring - Delete confirm uses "Delete / Cancel" labels instead of "Yes / No" - Drawer backdrop adds a 2px blur and stronger shadow - Sidebar transition smoothed via cubic-bezier Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e item ChatGPT, Claude, and Gemini all render "+ New chat" as another sidebar item — transparent background, hover gives subtle feedback, left-aligned text-with-icon. The previous solid brand-colored button competed with the session list for visual weight. Default is now transparent + brand-tinted hover (left-aligned with a small compose/pencil icon, matching the design language). Embedders who want the prominent solid button can still opt in via panelTheme.newChatButtonColor; when set, we switch to solid styling with the same hover-darken behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SessionPanel renders as a sibling of <Bot>, both children of the ChatRoot wrapper. <Bot> sets the Open Sans + system font stack via the .chatbot-container CSS class, but the panel was inheriting the host element's default (typically Times/system serif), making the typography visibly inconsistent across the panel and chat. Apply the same font stack inline to the ChatRoot wrapper so both children inherit the same family. Kept inline (rather than adding the chatbot-container class) because that class also pulls in CSS vars (--chatbot-container-bg-image / -bg-color) which only resolve inside shadow DOM and would otherwise leave the wrapper with undefined fallbacks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdatedAt Each streamed token calls upsertMessageInSession which bumps the streaming session's updatedAt and creates a new SessionV2 reference. Solid's <For> keys items by reference identity, so the SessionListItem for the streaming session was being unmounted+remounted every token — which reset the local `editing` signal back to false, making it impossible to rename the active session mid-stream. Lift the edit + delete-confirm state up to SessionPanel, keyed by chatId, so the lifetime of those flags is independent of <For>'s row remounts. SessionListItem becomes a controlled component: editing, editingDraft, and confirmingDelete come in as props, with corresponding callbacks for start/change/commit/cancel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In multi-session mode, the chat now renders a transparent header at the top of <Bot> showing only the active session title (left-aligned), replacing the blue title bar + Clear button. Clicking the title opens a menu with three actions: - Star / Unstar — toggles a `starred` flag on the session, optimistically pinning it to a new "Starred" section in the SessionPanel above "Recents" (matching Claude's two-section layout). Within each section, sessions are ordered by updatedAt desc. - Rename — swaps the title into an inline input; Enter commits, Escape cancels. Routes to the same store.actions.renameSession the panel uses, so edits sync everywhere. - Delete — calls store.actions.deleteSession; SessionPanel reflects the removal immediately via the index signal. Bubble/popup modes get a hamburger button on the left of the new header (replacing the previous ☰ unicode glyph with a proper SVG icon). Data layer: - SessionV2 gets an optional `starred?: boolean` field (backward compat: absence === false). - SessionStore exposes `starredSessions` / `recentSessions` memos and a new `toggleStarred(chatId)` action. - SessionListItem gains an `onToggleStar` prop and renders a star toggle in its action row.
Two fixes: 1. Stream-into-other-chats regression: case 'start' in the SSE handler was unconditionally re-pinning streamingChatId to the current activeChatId. handleSubmit already pins it synchronously before the request fires; if the user switches sessions in the gap between fetch firing and the 'start' event arriving, the re-pin would clobber the originating session and bleed all subsequent tokens into the new active. Guard the re-pin behind `streamingChatId === undefined` so handleSubmit's pin wins. The INPROGRESS case was already correctly guarded. 2. Sidebar sectioning: "Starred" was wrapping "Recents" so when no chats were starred the Recents header didn't render at all. Restructure so the Starred section renders only when starred().length > 0, and Recents is always rendered (with a tighter top padding when no Starred precedes it).
…ight Three multi-session-mode UI fixes matching ChatGPT/Claude/Gemini patterns: 1. Bot messages render bare (no background, no border-radius, no padding beyond a small inset). User messages keep their bubble. BotBubble takes a new `bare` prop that strips the chatbot-host-bubble class and the 50px right gutter. 2. Chat content (messages, starter prompts, input row) is constrained to a 760px max-width and centered in the chat surface. The session panel stays at its full width on the left. 3. Header no longer overlays the chat. The legacy single-session header was `absolute top-0` so the chat-view padded itself with `pt-[70px]` to compensate. SessionTitleHeader is rendered in normal block flow, so the chat-view drops to `pt-2` when sessionStore is mounted — first message no longer bleeds into the header.
The header was still using absolute positioning, so the chat-view sat underneath it and the welcome bubble + starter prompts bled into the title area. Switch to position: relative with a fixed 50px height and flex-shrink: 0 so the header is a normal block flex child of the Bot column. Also bumped the chat-view top padding from pt-2 to pt-6 in multi-session mode so the first message has visible breathing room beneath the header.
There was a problem hiding this comment.
Code Review
This pull request introduces a multi-session feature for the chatbot, allowing users to manage multiple conversations. It includes a new session store, storage migration logic, and UI components like the SessionPanel and SessionListItem. I have reviewed the changes and identified two areas for improvement: the send button's background color fallback logic in TextInput.tsx and the missing background color prop for the LoadingBubble in Bot.tsx.
| fallback={ | ||
| <div | ||
| class="w-9 h-9 rounded-[10px] flex items-center justify-center" | ||
| style={{ background: props.sendButtonBackgroundColor ?? props.sendButtonColor ?? '#3B81F6' }} |
There was a problem hiding this comment.
The fallback logic for the send button's background color is a bit confusing. It falls back to props.sendButtonColor, which is intended for the icon color, not the background. This could lead to unexpected styling if sendButtonBackgroundColor is not provided. For clarity and correctness, it would be better to remove the fallback to sendButtonColor and just use the default color.
| style={{ background: props.sendButtonBackgroundColor ?? props.sendButtonColor ?? '#3B81F6' }} | |
| style={{ background: props.sendButtonBackgroundColor ?? '#3B81F6' }} |
| {message.type === 'userMessage' && loading() && index() === messages().length - 1 && <LoadingBubble />} | ||
| {message.type === 'apiMessage' && message.message === '' && loading() && index() === messages().length - 1 && ( | ||
| <LoadingBubble /> | ||
| )} |
There was a problem hiding this comment.
The LoadingBubble component now supports a backgroundColor prop, but it's not being passed here. This will result in a transparent background for the loading bubble, which might be inconsistent with the BotBubble's background, especially in multi-session mode. To ensure visual consistency, you should pass the botMessage background color to the LoadingBubble.
| {message.type === 'userMessage' && loading() && index() === messages().length - 1 && <LoadingBubble />} | |
| {message.type === 'apiMessage' && message.message === '' && loading() && index() === messages().length - 1 && ( | |
| <LoadingBubble /> | |
| )} | |
| {message.type === 'userMessage' && loading() && index() === messages().length - 1 && <LoadingBubble backgroundColor={props.botMessage?.backgroundColor} />} | |
| {message.type === 'apiMessage' && message.message === '' && loading() && index() === messages().length - 1 && ( | |
| <LoadingBubble backgroundColor={props.botMessage?.backgroundColor} /> | |
| )} |
Adding in multi session chat experience:
What's new (multi-session only)