Skip to content

refactor(flow-chat): reuse the session surface in both floating chat surfaces - #1775

Merged
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:refactor/floating-chat-reuse-session-surface
Jul 26, 2026
Merged

refactor(flow-chat): reuse the session surface in both floating chat surfaces#1775
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:refactor/floating-chat-reuse-session-surface

Conversation

@bobleer

@bobleer bobleer commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

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 one SessionMenu.

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-uiflow_chat, app layout).

Motivation / Impact

Duplicate chat UIs. Both floating surfaces rendered ModernFlowChatContainer plus 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 + useFlowChatSessions replace both with one component and one store subscription.

Bubble could not create Cowork sessions. Its + dispatched toolbar-create-session with no detail, so detail?.mode was always undefined and 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 PhysicalSize unconverted, while the edge margin in the same helper was already scaled by scaleFactor. On a 2x display the window rendered at half the intended size; on 1x it was correct. resolveToolbarWindowGeometry now treats the authored sizes as logical pixels, converts once, and returns the matching physical min size so callers stop mixing units. Expanded mode is 440x680, matching the bubble.

The bubble trigger often needed several clicks. Three compounding causes, all fixed:

  1. It opened on click, which only fires when pointerdown and pointerup land 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 on pointerdown (click kept for keyboard/assistive activation) and the handler is idempotent.
  2. Mounting ChatPane in the same commit that opened the panel stalled the open transition; measured with a synthetic 400 ms mount block, the panel was still at scale(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.
  3. During the scale-up the backdrop still covered the panel's final rect (measured: ~130 ms at the panel centre, longer once the mount stall is added), so an impatient second click closed what the first one opened. The backdrop is now inert until the panel finishes opening — it still blocks stray clicks from reaching the scene underneath, it just does not close.

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 use visibility rather than relying on pointer-events alone.

Dead code removed. updateToolbarState has no callers anywhere (confirmed with git log -S), so toolbarState never leaves its initial value: the bubble's confirm/reject header controls could never render and toolbar-tool-confirm / toolbar-tool-reject have no listeners. Those controls are gone from the bubble (the reused surface provides them). The toolbar-tool-* events and the ToolbarModeState fields behind them are still referenced by floating window mode's collapsed strip and were left alone — see Reviewer Notes.

Verification

pnpm run type-check:web        # pass
pnpm run lint:web              # pass
pnpm run build:web             # pass
pnpm run theme:color-audit     # pass
pnpm run i18n:audit            # pass, 0 warnings
pnpm run check:repo-hygiene    # pass
pnpm --dir src/web-ui run test:run \
  src/flow_chat/components/toolbar-mode \
  src/app/startup/startupPerformanceContract.test.ts   # 60 passed

All re-run on the current upstream/main base.

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:

  • Backdrop vs panel during the open animation — probing elementFromPoint at the panel's final centre each frame showed backdrop until ~134 ms, then panel.
  • Mount stall — with the block inline the panel was still at scale(0.15) when it ended; deferred by two frames the transition ran to completion on schedule.
  • Trigger hit testing — a real synthesised click on the rewritten structure logged TRIGGER:pointerdown both from the closed state and mid-close-animation, and notably logged no click, which is exactly the failure mode being fixed.
  • Collapsed panel pointer-events resolved to none, 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:

  • The window sizing change (scaleFactor was inferred from screenshot pixel ratios, not read at runtime).
  • ChatPane's first-mount cost inside a 440 px-wide panel — the layout and the SessionMenu dropdown clipping against the panel's overflow: hidden are the parts I would look at first.

Reviewer Notes

New contract test toolbarModeSessionSurface.test.ts runs over both surfaces and asserts they mount ChatPane with showChatInput, carry no private conversation view or composer, and take their session affordances from SessionMenu — so the duplication cannot quietly come back.

toolbarWindowGeometry.test.ts went from 3 cases to 7. Every existing case used scaleFactor: 1, which is why the HiDPI sizing bug was invisible to it; there is now explicit 2x coverage.

Two deliberate scope limits:

  • Floating window mode's collapsed strip still has the dead toolbarState branches (confirm/reject buttons, todo progress). Removing them means deleting ToolbarModeState and updateToolbarState from the context's public API, which felt like a separate change. Happy to fold it in if preferred.
  • toolbar-send-message now has no dispatchers and its listener in AppLayout is 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

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable. (No new strings — the shared menu reuses the existing toolCards.toolbar.* keys; i18n:audit passes.)

Related Issue

Closes #1078

…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.
@bobleer
bobleer merged commit 72c6a69 into GCWing:main Jul 26, 2026
5 checks passed
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.

[feature] 悬浮窗功能缺失了 ,桌宠悬浮按钮的任务信息希望有关闭按钮

1 participant