refactor(flow-chat): reuse the session surface in both floating chat surfaces - #1775
Merged
bobleer merged 1 commit intoJul 26, 2026
Merged
Conversation
…surfaces Floating window mode and the floating chat bubble each carried their own conversation view and composer. Both were strict feature subsets of the main window session UI (a single-line input instead of ChatInput: no model selector, attachments, @-mentions, slash commands, permission modes, voice input or pending queue) and both had to be updated by hand whenever the real chat UI changed. Both now render ChatPane, the same component the session scene mounts, so they inherit the full composer and cannot drift behind it again. Shared session menu The two headers had also diverged: floating window mode opened a menu from "+" and showed a static title, while the bubble made the title a session switcher and had "+" create a session immediately. The bubble's "+" also dispatched toolbar-create-session without a detail, so it always fell through to the code branch and could never start a Cowork session. SessionMenu and useFlowChatSessions extract that into one component and one store subscription used by both headers. Window sizing Toolbar window sizes were passed to PhysicalSize unconverted while the edge margin in the same helper was already scaled by scaleFactor, so the window was half the intended size on a 2x display and full size on 1x. resolveToolbarWindowGeometry now treats the authored sizes as logical pixels and converts once, and returns the matching physical min size so callers stop mixing units. Expanded mode is 440x680, matching the bubble. Trigger reliability The bubble's trigger opened on click, but a click only fires when pointerdown and pointerup land on the same element, and this trigger hides itself as part of opening; a re-render from a streaming session was enough to swallow the press. It now opens on pointerdown (click is kept for keyboard and assistive activation) and the handler is idempotent. The collapsed panel also overlaps the trigger completely, and whether it swallowed the press was decided only by source order between `.bitfun-fmc > *` and `.bitfun-fmc__panel` (identical specificity). Pointer events are now declared per child, stacking is explicit (backdrop 0, panel 1, trigger 2), and hidden elements use visibility rather than relying on pointer-events alone. Mounting ChatPane in the same commit that opened the panel also stalled the open transition, and during the scale-up the backdrop still covered the panel's final rect, so an impatient second click closed what the first one opened. The surface now mounts two frames later and the backdrop only closes once the panel has finished opening. Removed along the way: dead confirm/reject wiring in the bubble header (updateToolbarState has no callers, so toolbarState never leaves its initial value and toolbar-tool-confirm / toolbar-tool-reject have no listeners), the now-unused toolbar-send-message dispatches, and several unreferenced keyframes and rules.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Floating window mode and the floating chat bubble each carried their own conversation view and composer. Both were strict feature subsets of the main window session UI, and both had to be updated by hand whenever the real chat UI changed.
Both now render
ChatPane— the same component the session scene mounts — so they inherit the full composer and cannot drift behind it again. The two headers, which had also diverged from each other, now share oneSessionMenu.Net -386 lines while gaining features on both surfaces.
Type and Areas
Type: Refactor, plus three bug fixes found while doing it (regression fix, UI/UX).
Areas: web UI (
src/web-ui—flow_chat, app layout).Motivation / Impact
Duplicate chat UIs. Both floating surfaces rendered
ModernFlowChatContainerplus a single-line<input>. Users lost the model selector, attachments,@-mentions, slash commands, permission modes, voice input and the pending queue whenever they worked from a floating surface. Both now use the real composer.Divergent headers. Floating window mode opened a menu from
+and showed a static title; the bubble made the title a session switcher and had+create a session immediately.SessionMenu+useFlowChatSessionsreplace both with one component and one store subscription.Bubble could not create Cowork sessions. Its
+dispatchedtoolbar-create-sessionwith nodetail, sodetail?.modewas alwaysundefinedand it fell through to the code branch. The shared menu always passes{ mode }.Toolbar window was the wrong size on HiDPI displays. Sizes went to
PhysicalSizeunconverted, while the edge margin in the same helper was already scaled byscaleFactor. On a 2x display the window rendered at half the intended size; on 1x it was correct.resolveToolbarWindowGeometrynow treats the authored sizes as logical pixels, converts once, and returns the matching physical min size so callers stop mixing units. Expanded mode is440x680, matching the bubble.The bubble trigger often needed several clicks. Three compounding causes, all fixed:
click, which only fires whenpointerdownandpointerupland on the same element — and this trigger hides itself as part of opening, so a re-render from a streaming session was enough to swallow the press. It now opens onpointerdown(clickkept for keyboard/assistive activation) and the handler is idempotent.ChatPanein the same commit that opened the panel stalled the open transition; measured with a synthetic 400 ms mount block, the panel was still atscale(0.15)when the block ended and took ~820 ms to reach full size instead of 420 ms. The surface now mounts two frames later, so the transition is already running on the compositor.The collapsed panel also overlaps the trigger completely, and whether it swallowed the press was decided only by source order between
.bitfun-fmc > *and.bitfun-fmc__panel(identical specificity,0-1-0). That happened to resolve correctly today, but moving either rule would have killed the button outright. Pointer events are now declared per child, stacking is explicit (backdrop 0, panel 1, trigger 2), and hidden elements usevisibilityrather than relying onpointer-eventsalone.Dead code removed.
updateToolbarStatehas no callers anywhere (confirmed withgit log -S), sotoolbarStatenever leaves its initial value: the bubble's confirm/reject header controls could never render andtoolbar-tool-confirm/toolbar-tool-rejecthave no listeners. Those controls are gone from the bubble (the reused surface provides them). Thetoolbar-tool-*events and theToolbarModeStatefields behind them are still referenced by floating window mode's collapsed strip and were left alone — see Reviewer Notes.Verification
All re-run on the current
upstream/mainbase.Behavioural claims above were measured in a browser against a faithful replica of the real DOM and CSS (same rules, same source order), not asserted from reading the code:
elementFromPointat the panel's final centre each frame showedbackdropuntil ~134 ms, thenpanel.scale(0.15)when it ended; deferred by two frames the transition ran to completion on schedule.TRIGGER:pointerdownboth from the closed state and mid-close-animation, and notably logged noclick, which is exactly the failure mode being fixed.pointer-eventsresolved tonone, confirming the specificity tie currently falls the right way.Not verified in the running desktop app. I could not exercise these surfaces end to end — the browser-only dev server has no Tauri backend, so it cannot reach a scene where either surface renders. Two things are worth a manual pass before merge:
scaleFactorwas inferred from screenshot pixel ratios, not read at runtime).ChatPane's first-mount cost inside a 440 px-wide panel — the layout and theSessionMenudropdown clipping against the panel'soverflow: hiddenare the parts I would look at first.Reviewer Notes
New contract test
toolbarModeSessionSurface.test.tsruns over both surfaces and asserts they mountChatPanewithshowChatInput, carry no private conversation view or composer, and take their session affordances fromSessionMenu— so the duplication cannot quietly come back.toolbarWindowGeometry.test.tswent from 3 cases to 7. Every existing case usedscaleFactor: 1, which is why the HiDPI sizing bug was invisible to it; there is now explicit 2x coverage.Two deliberate scope limits:
toolbarStatebranches (confirm/reject buttons, todo progress). Removing them means deletingToolbarModeStateandupdateToolbarStatefrom the context's public API, which felt like a separate change. Happy to fold it in if preferred.toolbar-send-messagenow has no dispatchers and its listener inAppLayoutis dead. Left in place for the same reason.One side effect worth flagging: the sizing fix also makes the collapsed toolbar strip DPI-consistent, so on a 2x display it becomes 700 logical px wide instead of the 350 it renders at today (1x displays are unchanged). That matches the authored constant, but it is a visible change — say the word and I will retune the constant.
Checklist
toolCards.toolbar.*keys;i18n:auditpasses.)Related Issue
Closes #1078