Skip to content

feat: multi session chat#391

Draft
chloebyun-wd wants to merge 66 commits intomainfrom
feat/multi-session-chat
Draft

feat: multi session chat#391
chloebyun-wd wants to merge 66 commits intomainfrom
feat/multi-session-chat

Conversation

@chloebyun-wd
Copy link
Copy Markdown
Contributor

@chloebyun-wd chloebyun-wd commented May 5, 2026

Adding in multi session chat experience:

Chatbot.initFull({
            chatflowid: 'agent1',    // or 'support', 'salesbot', etc.
            apiHost: window.location.origin,
            multiSession: { enabled: true }  // this is the new config to enable the new experience
        })

What's new (multi-session only)

  • Session panel + title header — sidebar in full-page mode, drawer in bubble/popup, transparent ChatGPT-style header with Star/Rename/Delete menu;
  • Sessions store — new/switch/upsert/rename/delete + toggleStarred, per-chatId debounced persistence, cross-tab storage-event sync with active-stream protection, quotaPanic signal for unrecoverable storage failures, cap-eviction with capWarning toast.
  • Storage layer — v1→v2 in-place migration that preserves legacy lead, orphan reconciliation, defensive safeParse with logging.
  • Bot.tsx hardening — streamingChatId pin so tokens never bleed across mid-stream session switches, best-effort abortMessageQuery on switch, empty-placeholder cleanup, clearChatOnReload guarded against the v2 index, SSE body uses pinned chatId.
  • New TextInput / SendButton variant — two-row layout with rounded send pill and UpArrowIcon, while non-multi keeps main's exact single-row layout.
  • Latest-user-message scroll anchor — pins the user's message near the top per turn (matches ChatGPT). Non-multi keeps main's plain scroll-to-bottom.
Screenshot 2026-05-05 at 3 21 54 PM Screenshot 2026-05-05 at 3 21 21 PM Screenshot 2026-05-05 at 3 21 38 PM

chloebyun-wd and others added 30 commits April 29, 2026 14:56
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>
chloebyun-wd and others added 28 commits May 1, 2026 13:47
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.
@chloebyun-wd chloebyun-wd changed the title feat:multi session chat feat: multi session chat May 5, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
style={{ background: props.sendButtonBackgroundColor ?? props.sendButtonColor ?? '#3B81F6' }}
style={{ background: props.sendButtonBackgroundColor ?? '#3B81F6' }}

Comment thread src/components/Bot.tsx
Comment on lines +2928 to +2931
{message.type === 'userMessage' && loading() && index() === messages().length - 1 && <LoadingBubble />}
{message.type === 'apiMessage' && message.message === '' && loading() && index() === messages().length - 1 && (
<LoadingBubble />
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
{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} />
)}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant