feat(ui,app): composer rewrite (slice 10, issue #440)#508
Conversation
…ith ui_kit - composer DockCard: dark mode uses surface-raised + drop shadow to match ui_kit floating feel; light mode keeps ring + shadow-raised - shell dark tokens: switch from cold gray (#212121/#181818) to warm coffee (#221f1c/#1a1714) so halo blends with composer surface - titlebar macOS: right half transparent so page halo shows through both titlebar and main continuously - main content: drop opaque bg-bg-base layers so page-level halo reaches composer surface - composer width: home and session both 720px / 2xl:920px (was home 720, session 720→920) - delete cold-gray-era 1px border-t divider in layout.tsx
Move PromptStore/HistoryStore types to prompt-input/store-types.ts and DOM <-> Parts bidirectional serialization (createPill, isNormalizedEditor, renderPartsToEditor, parseEditorToParts) to prompt-input/editor-serialize.ts. Pure functions, no Solid reactivity. Pairs with editor-dom (cursor primitives). First step of splitting prompt-input.tsx (1602 lines) into business-domain modules.
…ratives.ts Move 10 imperative side-effect helpers (scrollCursorIntoView, queueScroll, clearEditor, setEditorText, focusEditorEnd, currentCursor, restoreFocus, renderEditorWithCursor, getCaretState, escBlur) into a single factory. All operate via editorRef/scrollRef getters; no Solid reactivity introduced. PromptInput now destructures the helpers it needs from createEditorImperatives.
Move activeFileTab, recent, and openComment (with its commentInReview helper) into prompt-input/comment-routing.ts. The factory consumes useSessionLayout/ useFile/useComments/useSync internally, exposing only what PromptInput needs. This logic was inside the composer historically but routes clicks into the review side panel — it's not composer-internal.
Move two persisted history stores, historyComments, applyHistoryComments, applyHistoryPrompt, addToHistory, and navigateHistory into prompt-input/history-navigation.ts. History is a self-contained subsystem: persisted entries (normal + shell), apply logic with comment synchronization, and navigation. setCursorPosition imports directly from editor-dom (no detour through editor-imperatives). Adopt PromptStore type from store-types in the main createStore call.
…nput.ts Move composing/IME signal, handleBlur, composition handlers, reconcile, the prompt -> DOM reconcile effect, the carry-over effect, handleInput, and addPart into createEditorInput factory. For now atOnInput/slashOnInput/closePopover/resetHistoryNavigation are passed in as direct closures from PromptInput. The next commit extracts popover-controllers and converts these to a popovers() ref forward to break the addPart <-> popover-handlers cycle. Pure functions (parseEditorToParts, isNormalizedEditor, createPill, createTextFragment, setRangeEdge) are imported directly from editor- serialize/editor-dom rather than threaded through deps.
Move @ file popover, / command popover (with their useFilteredList instances), slash auto-scroll effect, the promptProbe testing hook (with its promptEnabled() guard preserved), and selectPopoverActive into createPopoverControllers. Resolve the addPart <-> popover-handlers cycle with a mutable popoversRef: PromptInput creates editorInput first (passing popoversAccess that throws if not yet initialized), then creates popovers with editorInput.addPart, then assigns popoversRef. editorInput's handleInput now calls deps.popovers() .atOnInput / .slashOnInput. The throw is intentional — silent fallback would drop @/slash input events if the assembly order ever regressed. slashPopoverRef typed nullable (assigned by PromptPopover on mount).
Move handleKeyDown (the 169-line key dispatch switch) into createPromptKeydownHandler factory. The handler routes events to popover, history, shell-mode, file-pick, and submit/abort subsystems based on the key combination. PromptInput passes 17 deps to the factory, all of which were already local closures in the handler — preserves behavior verbatim.
Move renderModelControl and renderVariantControl into PromptModelControl and PromptVariantControl components in prompt-input/model-controls.tsx. The variants memo moves into PromptVariantControl since it's only consumed there. variantOpen state stays in PromptInput (paired with the close-on-disable effect) and is passed as open/onOpenChange props — splitting that effect across files would double the disable subscription. PromptVariantControl exposes onOpenChange (no separate onClose prop, since the underlying Select doesn't have one); PromptInput's onOpenChange handler calls restoreFocus when the popover closes.
- prompt-input.tsx: drop ContentPart, useLayout, Popover imports and the layout/tabs/view destructured locals — all moved into split modules - prompt-input.tsx: drop historyComments from createHistoryNavigation destructure (only addToHistory and navigateHistory are consumed) - comment-routing.ts: stop returning activeFileTab from createCommentRouting (only used internally by the recent memo, was never consumed externally in the original file either)
#440) Home composer wrapper used max-w-[720px] 2xl:max-w-[920px] inside an outer max-w-200 (= 800px) cap. The cap shadowed the 2xl branch — at 2xl the popover was held to 800px even though the inner intent was 920px. Drop the outer cap and tighten the inner wrapper to max-w-[640px] (matches the skill chips max-width directly below). Home composer now sits in a 640px column that visually rhymes with the chip group, while session-internal composer keeps the existing 720 / 2xl 920 responsive width — narrower home is the design intent (hero stage vs working strip).
Trim the picker popover to the design intent for slice 10:
- Width 340 → 280 (covers ~95% of popular provider model names without
truncation; verified empirically across 256 models, see brainstorm width
calculator).
- Drop the autofocus search bar inside the menu — the ⌘K full-screen picker
already covers high-frequency search; the popover stays for low-frequency
quick-switch within ≤ 5 visible models per group.
- Drop the "+ add provider" / sliders icon-button action and the unpaid
"add more provider" footer CTA. Provider management is a low-frequency
one-shot action and belongs in settings (⌘,), not in the high-frequency
quick-switch surface.
- Add provider icon to each model row (left of name) — design preview shows
it, ModelTooltip alone is not enough at-a-glance.
- Remove now-unused imports (useDialog, useProviders, IconButton, Button,
Show) and the dialog-bound dismiss kinds ("manage" / "provider"); keep
popularProviders for sortGroupsBy and Tooltip for the per-row
ModelTooltip wrapper.
Group/row spacing (group-gap 12, row-gap 2, header 12px / 400) is unchanged
because the underlying List component already aligns with dev's contract;
no overrides needed.
…#440) Append a thinking-level section to the bottom of the model picker popover, separated from provider groups by a 1px border-weaker divider + pt-3 to visually mark the conceptual jump (gap = same layer; line = cross layer). The section is data-driven against model.variant: it renders only when the current model exposes at least one variant. The trigger row shows the section label "思考档位" / "Thinking" and the current variant translated via translateVariant (default → "默认" / "Default"). Clicking the trigger opens a nested Kobalte popover to the right (placement="right-start"), listing ["default", ...variant.list()] as a single-select. Selecting "default" calls model.variant.set(undefined); selecting any other value calls .set(rawName), so SDK-side reasoningEffort plumbing is unchanged. Why nested Kobalte popover (vs inline segmented or absolute div): - Inline segmented bunches a low-frequency adjustment ("调档") into the high-frequency main path ("换模型"); ⌘K already covers high-frequency search, so the picker stays for ≤5 quick-switch — the bottom section shouldn't compete with model rows for visual budget. - The flyout placement makes it easy to keep the trigger row visible alongside the option being selected, and Kobalte handles the focus / outside-click contract correctly when nested inside the outer popover. - Manual absolute-positioned div would be clipped by the outer Kobalte.Content overflow-hidden; nested popover routes through portal. i18n: add "dialog.model.variant" key in en.ts ("Thinking") and zh.ts ("思考档位"). Variant labels reuse the existing variant-label.ts mapping (none/minimal/low/medium/high/xhigh/max + default), no new translations needed. The ⌘. variant cycle command keybind is unchanged — keyboard users keep quick cycling without opening the menu.
…(slice 10, issue #440) The variant (thinking level) selection moved into the model picker popover in the previous commit. This commit removes the now-redundant standalone PromptVariantControl button next to the model trigger, and folds the current variant into the model trigger as an inline indicator ("· {variant i18n}"), so the bottom bar shows one combined picker instead of two adjacent dropdowns. model-controls.tsx - Delete PromptVariantControl entirely (Select-based standalone control) - PromptModelControl trigger: render "· {translateVariant(current)}" after the model name when model.variant.current() is truthy. Default (undefined) hides the inline tag — matches the picker popover trigger contract: no tag when off, raw-translated label when on. - Drop unused createMemo, Select imports - Update file header comment prompt-input.tsx - Remove PromptVariantControl from the named import - Delete the [variantOpen, setVariantOpen] signal — Select component owned its own open state from inside model-controls; the parent signal was the controlled-mode escape hatch that no longer has a control to drive - The actionReady close-on-disable createEffect keeps closePopover() and setStore("draggingType", null), drops setVariantOpen(false) - Delete the <PromptVariantControl> JSX block The ⌘. variant cycle command (model.variant.cycle keybind) is unchanged in command.tsx; users keep keyboard cycling without a button to click.
Render permission inside the composer DockCard instead of a separate dock above it, so the permission and input states visually share the same shell. SessionPermissionDock becomes SessionPermissionContent (no DockPrompt wrapper); the existing data-component="dock-prompt"[data-kind=permission] attribute is preserved so the slot CSS still applies. Adds min-h-[140px] to the composer DockCard for the empty-input minimum.
…#440) The composer drifted from STANDARDS.md and picker.css contract on a few details. Rather than tuning each magic number, plug the composer into the existing contracts so the values come from one place. - attach: Button + size-7 override → IconButton (L23: ghost icon button is one fixed 24×24 token; using IconButton retires five className overrides and the unused Button import) - model picker content: add data-picker-content="" and drop inline radius + shadow + bg (L34 / picker.css: surface-base + radius-md + ring-base + shadow-floating + padding 4 come from the contract) - model picker trigger: drop px-1.5 (let Button default 0/8 padding stand; the previous 6px override pinched the icon + name + chev row) - provider icon in trigger: drop opacity-40 + group-hover:opacity-100 + will-change/translateZ hacks (preview spec is rest fg-base, not faded) - composer-bar gap: gap-3 → gap-2 (L33 explicitly locks composer-bar gap to 8) - composer-bar left gap: gap-2 → gap-1 (preview composer-left gap is 4)
Collapse the prior 24/28/32 mixed control system into one tier at 30px with a 26px dense sub-layer. Industry alignment per shadcn/Material 3/ Carbon/Radix/Primer: icon-only buttons match text-button height as a square version (30x30), icon SVG remains independent on 16/20/24. - L23 icon-button: 30x30 square (titlebar-icon 32x30 non-square retained) - L25 button / L26 input / L34 picker / L35 sidebar row: 30 - Send button: 30 round - L42 sub-tab pill, L34 chip, filetree row, collapsible arrow, diff-hunk line-number icon, file-search inner buttons, titlebar-internal pills (worktree badge, project dir): 26 - session-turn-changes-action: rewritten with flex centering, drops the line-height: 24 vertical-center hack - DockCard: drop min-h-[140px] so the empty composer card no longer stretches on the home screen - IconButton call sites: drop redundant size-6 / w-8 h-7 overrides so the default 30x30 takes effect
session-row 三点 / sort trigger / workspace 三点 / workspace 新建会话 四个 trailing icon button 从默认 30 降到密集层 26,贴齐 DESIGN.md 新增的 Nesting rule:30 高行内的 trailing 操作降到 26,宿主行保持 30,给 按钮留可见呼吸,避免 sort 跟首条会话粘连、菜单按钮跟行边缘贴死。
… width - picker.css: clear nested List 0/12 wrapper inside picker context (DESIGN.md L30 popover spec is container 4 + item 0/8; List was adding a third 12px layer making the highlight float 24px from the edge) - picker.css: drop sticky group labels in picker (groups are short, sticky caused mouse-wheel-scrolled items to be overlapped by the label) - picker.css: add 4px gap between header and first item now that sticky no longer floats the label - model-picker.tsx: 280 -> 240 width, freed by the padding fix - model-picker.tsx: thinking-level row stays visible even when the model has no variants (trigger disabled, label "default") so the section doesn't disappear depending on model
The send/stop button uses theme-aware tokens (--fg-strong / --bg-base) which mirror across light and dark, breaking the muscle-memory contract for the primary action button. Lock the colors so light and dark look visually identical: - idle/ready: brand orange + white (was: brand orange + bg-base, which in dark theme rendered an unreadable dark-page-color icon on orange) - stopping: ink #1A1613 + white (was: --fg-strong + --bg-base, which in dark theme inverted to bone-white bg + dark icon, mirroring light) Also enlarge stop-square SVG from 8x8 (21% of button) to 15x15 (40% of button) to match Claude.ai / ChatGPT proportions; the prior glyph was hard to read against a 30px circular button. Theme-locked raw color values are an intentional call-site exception for affordance buttons and will be added to DESIGN.md deviations.
Manual buttons in the thinking-level secondary popover painted the browser default :focus ring on mouse open because they bypassed the picker contract that scopes the ring to :focus-visible. Tagging Content with data-picker-content and items with data-picker-item / data-selected routes them through picker.css so mouse-click no longer shows the ring, keyboard tab still does, and selected/hover states match the rest of the picker family.
Collapsed widget height is 36 per DESIGN.md (Composer · dock · model menu): 30 chev IconButton centered in a 36 row gives 3px breathing top/bottom and distinguishes a widget header from a plain control row. Pre-L34 the dock was a standalone rounded card and 78 over-padded it. Three coupled fixes: - Min height 78 -> 36 (two callsites: full() memo and DockSegment max-height floor) so the collapsed header matches the spec. - Progress span and TextReveal: leading-none + items-center. text-13-regular inherits line-height-large (150% = 19.5px line-box); CJK glyphs sit in the upper portion of the em-box, so flex items-center on the line-box centered the box but not the glyphs. Collapsing the line-box to the em-box lets the glyphs land on the optical centerline. - TodoList bottom padding pb-11 -> pb-3. The 44px tail was scaffolding for the scroll-stuck gradient overlay (now removed); 12px is enough breathing for the last item without leaving a visible empty band on full lists.
text-fg-on-brand is locked to #ffffff for use on the brand-orange surface and does not invert in dark mode. The context-usage tooltip uses the default tooltip surface (background: var(--fg-strong); color: var(--bg-cream)), both tokens auto-invert by scheme. Forcing white text on top of that produced white-on-light in dark mode (tooltip bg becomes light cream). Drop the override so spans inherit the tooltip's natural color.
Todo / Revert / Followup all hand-rolled the toggle row (label + chev IconButton). They drifted: Todo used h-9 (36, matching DESIGN.md L305); Revert and Followup used py-2 + a 20px h-5 filler when collapsed, totalling 46-66px. The spec calls for 36 across all three widgets. - Extract DockWidgetHeader (pl-3 pr-2 h-9 + chev slot + role=button + Enter/Space toggle). Caller owns label content and chev props (rotation, data attrs, click handlers). - Switch all three widgets onto it. Drop the h-5 collapsed filler in Revert and Followup so collapsed height is 36 per spec. - Pull leading-none onto label/preview spans inside the header so the text-13 line-box (line-height-large = 19.5px) collapses to the em-box; flex items-center then visually centers CJK glyphs. - Polish expanded list rows: drop per-row py-1 (rows take their height from Button-small), tighten container pb-7 -> pb-3 to match Todo, and switch the inline restore button on Revert from secondary to ghost so a stack of rows reads as messages with quiet inline actions, not a stack of buttons. DESIGN.md doc debt: capture the 36-row contract and the leading-none optical-centering note so the next widget doesn't redrift.
PawWork wants submit-during-busy to land in the followup dock first, then auto-send when the current turn ends; "send now" steers mid-stream as an opt-in confirmation. The upstream "steer" mode (immediate submit-during- busy with no queue UI) was the default, and a sticky migration kept flipping any stored "queue" value back to "steer", so the followup dock never appeared when a session was busy. Flip the default to "queue", drop the queue->steer migration, and replace it with a one-shot steer->queue pass to clear stored "steer" values from prior installs (no settings UI ever surfaced this choice, so a stored "steer" is always a migration artifact, not an explicit user preference). The followup setting getter and setter no longer rewrite the value. The "send now" button on the followup dock already calls the same prompt endpoint that steer mode uses; sendFollowup's actionReady guard checks status-known, not idle, so manual confirmation can fire mid-stream and the backend handles the insert.
Followup and Revert duplicated their collapse spring + height observer
+ turn/off/value derivations. Extract them into useDockCollapse so both
docks share one animation contract. Todo keeps its own wiring because
it layers a dockProgress fade on top of the base collapse value.
Also pin DockWidgetHeader at h-[36px] (was h-9). This app sets root
font-size to 13px via `html { font: var(--type-body) }` in theme.css,
so Tailwind's rem-based h-9 resolves to 2.25rem * 13 = 29.25px - short
of the 30px IconButton, which made the chev overflow and clip against
the segment's overflow-y: hidden. DESIGN.md L305 commits to absolute
36px, so absolute-pixel utilities are the honest mapping.
Wrapper around the chev keeps `flex items-center` as a defensive null:
the inline-flex IconButton in a block parent could in theory pull a
line-box descender into the wrapper height; flex eliminates that path.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR refactors the composer and prompt-input system by extracting editor, routing, and interaction logic into dedicated modules, replaces the DockTray component with a new DockCard/DockSegment/DockWidgetHeader architecture with animated collapse, moves model selection from dialog-based to integrated popover-based UI, and harmonizes component sizing across the design system (32px→30px buttons, 24px→26px icons). ChangesPrompt Input Refactoring & Dock UI Modernization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request refactors the composer region by modularizing the PromptInput logic and introducing a new DockCard architecture for session widgets. It also implements a draft carryover feature and unifies UI dimensions across various components. Feedback suggests using reactive accessors for resize observation to follow SolidJS best practices, moving module-level UI state to a more local context to avoid singleton issues, and refining the draft carryover logic to prevent the loss of rich content pills.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (8)
packages/ui/src/components/picker.test.ts (1)
54-79: ⚡ Quick winAdd assertions for the new nested picker/list rules.
picker.cssnow adds[data-component="list"],[data-slot="list-header"], and[data-slot="list-group"]rules inside[data-picker-content], but this suite still only pins the older content contract. A few source-text assertions for those selectors would keep the new picker behavior from regressing silently.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/picker.test.ts` around lines 54 - 79, Update the tests in picker.test.ts that target the "[data-picker-content]" block to also assert presence of the new nested rules: check that the sliced CSS block for "[data-picker-content] {" contains the selectors and/or declarations for [data-component="list"], [data-slot="list-header"], and [data-slot="list-group"]; add a few assertions mirroring the existing pattern (find contentIdx via css.indexOf("[data-picker-content] {"), slice the block, then expect(block).toContain(...) for each of those three selectors or their key declarations) so the new nested picker/list rules are covered.packages/app/src/components/prompt-input/popover-controllers.ts (1)
129-147: 💤 Low value
promptProbe.selectruns outside thepromptEnabled()guard.
promptProbe.set/promptProbe.clearare wrapped inif (promptEnabled())(lines 170–182), butpromptProbe.select(cmd.id)on line 132 is invoked unconditionally on every selection. If the probe relies on state set inside the guard, this could either no-op or store data into a probe that isn’t supposed to be active in production builds. Consider mirroring the guard:Suggested fix
- promptProbe.select(cmd.id) + if (promptEnabled()) promptProbe.select(cmd.id)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/components/prompt-input/popover-controllers.ts` around lines 129 - 147, promptProbe.select(cmd.id) is being called unconditionally even though other probe calls (promptProbe.set / promptProbe.clear) are guarded by promptEnabled(); wrap the select call in the same promptEnabled() check so the probe is only used when probes are enabled. Locate the handleSlashSelect function and add a conditional around promptProbe.select (and any other probe interactions) using promptEnabled() before calling promptProbe.select(cmd.id), mirroring the existing guards around promptProbe.set and promptProbe.clear.packages/app/src/pages/session/use-session-commands.tsx (1)
272-276: 💤 Low value
openModelPicker()may leaveexternalOpen=trueorphaned when no popover is mounted.Because the open state lives in a module-level signal in
model-picker.tsx, invokingmodel.choosewhile noModelSelectorPopoveris mounted (e.g. command palette fired before the composer renders, or before a session is active) will leaveexternalOpenstuck attrue, causing the popover to spring open the next time it mounts. Consider clearing the flag with a fallback timeout, or only setting it after asserting a trigger is mounted (e.g. via a registry of mounted instances).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/session/use-session-commands.tsx` around lines 272 - 276, The call in chooseModel that invokes openModelPicker can leave the module-level signal externalOpen in model-picker.tsx stuck true if no ModelSelectorPopover is mounted; update openModelPicker (or add a helper in model-picker.tsx) to either (a) only set externalOpen after confirming a mounted popover instance via a lightweight registry/flag maintained by ModelSelectorPopover on mount/unmount, or (b) set externalOpen but schedule a fallback clear (e.g., short timeout) to reset it if no popover registers as mounted; modify chooseModel only if needed to call the new safe API so externalOpen cannot become orphaned.packages/app/src/components/prompt-input/keydown.ts (1)
184-190: 💤 Low valueInconsistent control flow between
atandslashpopover branches.The
atbranch ends withevent.preventDefault(); return, while theslashbranch falls through to the sharedevent.preventDefault(); returntwo lines below. This works today, but it's easy to break when adding a third popover kind. Mirroring theatbranch keeps the two paths visually symmetric:Suggested cleanup
- if (store.popover === "slash") { - slashOnKeyDown(event) - } - event.preventDefault() - return + if (store.popover === "slash") { + slashOnKeyDown(event) + event.preventDefault() + return + } + event.preventDefault() + return🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/components/prompt-input/keydown.ts` around lines 184 - 190, The control flow for the popover handling is asymmetric: when store.popover === "at" the code calls the handler then does event.preventDefault(); return, but the "slash" branch calls slashOnKeyDown(event) and relies on the shared event.preventDefault()/return below; make the branches symmetric by adding event.preventDefault(); return immediately after slashOnKeyDown(event) so both store.popover === "at" and store.popover === "slash" explicitly call their handler and then call event.preventDefault() and return.packages/app/src/components/prompt-input/editor-imperatives.ts (1)
10-32: 💤 Low value
insetis captured by value, not as an accessor.
editorRef,scrollRef,prompt,platformare reactive/lazy, butinset: numberis read once atcreateEditorImperativesinvocation. If the call site ever derivesinsetfrom a responsive layout (e.g. dock height changing with breakpoints),scrollCursorIntoViewon line 63 will use a stale value. Consider typing it asinset: () => numberto match the rest of the deps surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/components/prompt-input/editor-imperatives.ts` around lines 10 - 32, The inset value is captured by value causing stale reads; change the EditorImperativesDeps type to declare inset: () => number (make it an accessor like editorRef/scrollRef), update createEditorImperatives to destructure and use the accessor (call inset() wherever the numeric inset is needed — e.g., inside scrollCursorIntoView and any other references), and update all call sites that construct EditorImperativesDeps to pass a function returning the current inset instead of a number so the imperative functions always read the latest value.packages/app/src/components/prompt-input/model-picker.tsx (1)
22-26: 💤 Low valueModule-level signal couples all
ModelSelectorPopoverinstances.
externalOpenis declared at module scope, so callingopenModelPicker()will open every mountedModelSelectorPopoversimultaneously. Today there is effectively a single instance, but if the composer ever renders more than one (e.g. side-by-side sessions, the new hybrid composer shell, or a preview panel) this becomes a stealth bug. Consider scoping the open command to an instance, e.g. by registering the popover into a small registry on mount and exposingopenModelPicker()as a dispatcher to the focused/last-mounted instance.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/components/prompt-input/model-picker.tsx` around lines 22 - 26, The module-level signal externalOpen and its setter setExternalOpen are causing every ModelSelectorPopover to open when openModelPicker() is called; change this to instance-scoped control by registering each ModelSelectorPopover on mount (e.g., maintain a small registry of instances or their open handlers) and have openModelPicker() dispatch to the focused/last-mounted instance’s open handler instead of toggling a global externalOpen; update ModelSelectorPopover to register/unregister itself and expose an instance method or callback for opening, and make openModelPicker() look up and call that instance handler.packages/app/src/pages/session/composer/session-revert-dock.tsx (1)
17-19: 💤 Low valueConsider using
createSignalinstead ofcreateStorefor a single field.The
storeobject contains only one field (collapsed) and is updated independently by thetogglefunction. Per coding guidelines,createStoreis preferred when signals represent coupled object state with shared batch updates, but for a single independent field,createSignaladds less boilerplate.♻️ Refactor to createSignal
- const [store, setStore] = createStore({ - collapsed: true, - }) + const [collapsed, setCollapsed] = createSignal(true) - const toggle = () => setStore("collapsed", (value) => !value) + const toggle = () => setCollapsed((value) => !value)Then update references from
store.collapsedtocollapsed()throughout the component.Based on learnings: in this repo, prefer
createStoreover multiplecreateSignalcalls only when the signals represent coupled object state that is updated together in at least one shared batch-update site.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/session/composer/session-revert-dock.tsx` around lines 17 - 19, The component currently uses createStore for a single boolean field (store.collapsed) updated by toggle; replace createStore with createSignal to reduce boilerplate: replace const [store, setStore] = createStore({ collapsed: true }) with const [collapsed, setCollapsed] = createSignal(true), update the toggle handler (toggle) to call setCollapsed(prev => !prev), and update all usages of store.collapsed to collapsed() throughout the SessionRevertDock component (and any imports if needed) to ensure behavior remains identical.packages/app/src/pages/session/composer/session-followup-dock.tsx (1)
17-19: 💤 Low valueConsider using
createSignalinstead ofcreateStorefor a single field.The
storeobject contains only one field (collapsed) and is updated independently by thetogglefunction. Per coding guidelines,createStoreis preferred when signals represent coupled object state with shared batch updates, but for a single independent field,createSignaladds less boilerplate.♻️ Refactor to createSignal
- const [store, setStore] = createStore({ - collapsed: false, - }) + const [collapsed, setCollapsed] = createSignal(false) - const toggle = () => setStore("collapsed", (value) => !value) + const toggle = () => setCollapsed((value) => !value)Then update references from
store.collapsedtocollapsed()throughout the component.Based on learnings: in this repo, prefer
createStoreover multiplecreateSignalcalls only when the signals represent coupled object state that is updated together in at least one shared batch-update site.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/session/composer/session-followup-dock.tsx` around lines 17 - 19, The component currently uses createStore({ collapsed: false }) with store and setStore even though only a single independent field is tracked; replace this with createSignal(false) (rename to collapsed and setCollapsed) and update the toggle handler (currently calling setStore) to call setCollapsed(prev => !prev) (or setCollapsed(!collapsed())). Also update all usages of store.collapsed to collapsed() and remove the deprecated setStore/store references and imports so the component uses the new collapsed signal everywhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/components/prompt-input/editor-serialize.ts`:
- Around line 129-136: The block-separator code after visit() is adding an extra
newline for empty blocks because visit() can already append "\n" (e.g., for
<br>), so change the separator logic in the loop over editor.childNodes to only
append "\n" when the block is a block element (isBlock) AND the serialization
buffer does not already end with a newline (check buffer.endsWith("\n")). Update
the branch that uses isBlock and index to guard the buffer += "\n" with this
ends-with check so blocks that already contributed a trailing newline (visit())
don't get a second one; references: editor.childNodes, visit(), buffer, isBlock.
In `@packages/app/src/components/prompt-input/keydown.ts`:
- Around line 224-232: The current keydown handler contains a redundant branch:
both branches call handleSubmit(event) making the stopping() check dead code;
change the logic in the Enter-without-Shift path so it either removes the
stopping() conditional entirely and directly calls handleSubmit(event), or
(preferred) explicitly ignore submits while a stop is in progress by doing: if
(stopping()) return; else handleSubmit(event). Update the handler containing
stopping() and handleSubmit to reflect the chosen behavior (ignore or simplify)
so the intent is clear.
In `@packages/app/src/components/prompt-input/model-picker.tsx`:
- Around line 105-141: The nested Kobalte.Portal inside ThinkingLevelSection
causes clicks inside the inner picker to be treated as "outside" by the outer
popover's onPointerDownOutside, closing the model picker; update the outer
popover's onPointerDownOutside handler (the one that calls close("outside")) to
first check the event target against the inner picker's DOM (use
event.target.closest with the existing data attribute like data-picker-content
or a new shared data attribute) and return early if the target is inside the
inner picker, so pointerDownInside interactions in the portal won't dismiss the
parent; alternatively render the nested picker inline, but prefer adding the DOM
guard in onPointerDownOutside to preserve portal usage.
In `@packages/app/src/components/prompt-input/popover-controllers.ts`:
- Around line 117-127: The generated custom command IDs collide because id is
built only from cmd.name; update the ID generation in the custom mapping (the
const custom = sync.data.command.map(...) block that sets id:
`custom.${cmd.name}`) to include cmd.source as well (e.g., incorporate
cmd.source into the id alongside cmd.name) so each command gets a unique id
across sources and stops being deduped by useFilteredList.
In `@packages/app/src/components/prompt-input/send-button.tsx`:
- Around line 23-25: The merge currently drops string-typed styles because it
only spreads when typeof props.style === "object"; update the SendButton style
handling in send-button.tsx (the code that builds the style prop for the
SendButton component) to preserve string values instead of discarding them —
e.g. change the merge so it uses props.style directly when it's a string (or
passes props.style through when not an object) rather than always replacing with
{}; ensure the logic still merges object styles correctly (keep the existing
object spread for JSX.CSSProperties) and handle the string branch by returning
props.style (or otherwise forwarding it) so callers that pass a string value for
style are not silently ignored.
In `@packages/app/src/pages/session/composer/session-composer-region.tsx`:
- Around line 130-149: The composer UI is being hidden behind prompt.ready(),
which prevents users from responding to permissionRequest() that can arrive
while prompt hydration is pending; update the rendering logic in the
showComposer() branch (the fallback that currently checks prompt.ready()) to
first check for an active permissionRequest() and render the permission request
UI immediately (instead of the handoff/loading DockCard), while still keeping
the existing rolled()/SessionRevertDock and handoff loading as fallbacks; apply
the same change to the other similar block referenced (lines ~179-236) so
permissionRequest() is never blocked by prompt.ready().
In `@packages/ui/src/components/dock-card.tsx`:
- Around line 9-12: The class merging logic incorrectly treats a space-separated
split.class as a single key (e.g., "foo bar") — remove the manual merge and let
SolidJS handle combining `class` and `classList` natively: delete the
`[split.class ?? ""]: !!split.class` entry and the custom merge in `classList`
for the components using `split` (refer to the `classList` usage around
`split.class` in DockCard, DockSegment, and DockSegmentForm) so the element
receives `class={...}` and `classList={...}` separately instead of constructing
a single spaced-key map.
---
Nitpick comments:
In `@packages/app/src/components/prompt-input/editor-imperatives.ts`:
- Around line 10-32: The inset value is captured by value causing stale reads;
change the EditorImperativesDeps type to declare inset: () => number (make it an
accessor like editorRef/scrollRef), update createEditorImperatives to
destructure and use the accessor (call inset() wherever the numeric inset is
needed — e.g., inside scrollCursorIntoView and any other references), and update
all call sites that construct EditorImperativesDeps to pass a function returning
the current inset instead of a number so the imperative functions always read
the latest value.
In `@packages/app/src/components/prompt-input/keydown.ts`:
- Around line 184-190: The control flow for the popover handling is asymmetric:
when store.popover === "at" the code calls the handler then does
event.preventDefault(); return, but the "slash" branch calls
slashOnKeyDown(event) and relies on the shared event.preventDefault()/return
below; make the branches symmetric by adding event.preventDefault(); return
immediately after slashOnKeyDown(event) so both store.popover === "at" and
store.popover === "slash" explicitly call their handler and then call
event.preventDefault() and return.
In `@packages/app/src/components/prompt-input/model-picker.tsx`:
- Around line 22-26: The module-level signal externalOpen and its setter
setExternalOpen are causing every ModelSelectorPopover to open when
openModelPicker() is called; change this to instance-scoped control by
registering each ModelSelectorPopover on mount (e.g., maintain a small registry
of instances or their open handlers) and have openModelPicker() dispatch to the
focused/last-mounted instance’s open handler instead of toggling a global
externalOpen; update ModelSelectorPopover to register/unregister itself and
expose an instance method or callback for opening, and make openModelPicker()
look up and call that instance handler.
In `@packages/app/src/components/prompt-input/popover-controllers.ts`:
- Around line 129-147: promptProbe.select(cmd.id) is being called
unconditionally even though other probe calls (promptProbe.set /
promptProbe.clear) are guarded by promptEnabled(); wrap the select call in the
same promptEnabled() check so the probe is only used when probes are enabled.
Locate the handleSlashSelect function and add a conditional around
promptProbe.select (and any other probe interactions) using promptEnabled()
before calling promptProbe.select(cmd.id), mirroring the existing guards around
promptProbe.set and promptProbe.clear.
In `@packages/app/src/pages/session/composer/session-followup-dock.tsx`:
- Around line 17-19: The component currently uses createStore({ collapsed: false
}) with store and setStore even though only a single independent field is
tracked; replace this with createSignal(false) (rename to collapsed and
setCollapsed) and update the toggle handler (currently calling setStore) to call
setCollapsed(prev => !prev) (or setCollapsed(!collapsed())). Also update all
usages of store.collapsed to collapsed() and remove the deprecated
setStore/store references and imports so the component uses the new collapsed
signal everywhere.
In `@packages/app/src/pages/session/composer/session-revert-dock.tsx`:
- Around line 17-19: The component currently uses createStore for a single
boolean field (store.collapsed) updated by toggle; replace createStore with
createSignal to reduce boilerplate: replace const [store, setStore] =
createStore({ collapsed: true }) with const [collapsed, setCollapsed] =
createSignal(true), update the toggle handler (toggle) to call setCollapsed(prev
=> !prev), and update all usages of store.collapsed to collapsed() throughout
the SessionRevertDock component (and any imports if needed) to ensure behavior
remains identical.
In `@packages/app/src/pages/session/use-session-commands.tsx`:
- Around line 272-276: The call in chooseModel that invokes openModelPicker can
leave the module-level signal externalOpen in model-picker.tsx stuck true if no
ModelSelectorPopover is mounted; update openModelPicker (or add a helper in
model-picker.tsx) to either (a) only set externalOpen after confirming a mounted
popover instance via a lightweight registry/flag maintained by
ModelSelectorPopover on mount/unmount, or (b) set externalOpen but schedule a
fallback clear (e.g., short timeout) to reset it if no popover registers as
mounted; modify chooseModel only if needed to call the new safe API so
externalOpen cannot become orphaned.
In `@packages/ui/src/components/picker.test.ts`:
- Around line 54-79: Update the tests in picker.test.ts that target the
"[data-picker-content]" block to also assert presence of the new nested rules:
check that the sliced CSS block for "[data-picker-content] {" contains the
selectors and/or declarations for [data-component="list"],
[data-slot="list-header"], and [data-slot="list-group"]; add a few assertions
mirroring the existing pattern (find contentIdx via
css.indexOf("[data-picker-content] {"), slice the block, then
expect(block).toContain(...) for each of those three selectors or their key
declarations) so the new nested picker/list rules are covered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7ea7426c-0c51-4ef0-b16c-0f3322b00d8d
📒 Files selected for processing (69)
packages/app/e2e/composer/composer-slice-10.spec.tspackages/app/src/components/dialog-manage-models.tsxpackages/app/src/components/dialog-select-model-unpaid.tsxpackages/app/src/components/file-tree.tsxpackages/app/src/components/prompt-input.tsxpackages/app/src/components/prompt-input/comment-routing.tspackages/app/src/components/prompt-input/context-items.tsxpackages/app/src/components/prompt-input/draft-carryover.test.tspackages/app/src/components/prompt-input/draft-carryover.tspackages/app/src/components/prompt-input/editor-imperatives.tspackages/app/src/components/prompt-input/editor-input.tspackages/app/src/components/prompt-input/editor-serialize.tspackages/app/src/components/prompt-input/history-navigation.tspackages/app/src/components/prompt-input/keydown.tspackages/app/src/components/prompt-input/model-controls.tsxpackages/app/src/components/prompt-input/model-picker.tsxpackages/app/src/components/prompt-input/popover-controllers.tspackages/app/src/components/prompt-input/send-button.tsxpackages/app/src/components/prompt-input/store-types.tspackages/app/src/components/session-context-usage.tsxpackages/app/src/components/session/session-header.tsxpackages/app/src/components/session/session-new-view.tsxpackages/app/src/components/session/session-status-connections.tsxpackages/app/src/components/settings-keybinds.tsxpackages/app/src/components/status-popover-body.tsxpackages/app/src/components/status-popover.tsxpackages/app/src/components/titlebar.tsxpackages/app/src/context/settings.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/index.csspackages/app/src/pages/layout.tsxpackages/app/src/pages/layout/pawwork-sidebar.tsxpackages/app/src/pages/layout/pawwork-worktree-badge.tsxpackages/app/src/pages/layout/sidebar-items.tsxpackages/app/src/pages/layout/sidebar-workspace.tsxpackages/app/src/pages/session/composer/dock-widget-header.tsxpackages/app/src/pages/session/composer/session-composer-region.tsxpackages/app/src/pages/session/composer/session-followup-dock.tsxpackages/app/src/pages/session/composer/session-permission-dock.tsxpackages/app/src/pages/session/composer/session-revert-dock.tsxpackages/app/src/pages/session/composer/session-todo-dock.tsxpackages/app/src/pages/session/composer/use-dock-collapse.tspackages/app/src/pages/session/file-tabs.tsxpackages/app/src/pages/session/session-main-view.tsxpackages/app/src/pages/session/session-side-panel.tsxpackages/app/src/pages/session/use-session-commands.tsxpackages/ui/src/components/accordion.csspackages/ui/src/components/button.csspackages/ui/src/components/collapsible.csspackages/ui/src/components/command-palette.csspackages/ui/src/components/dock-card.csspackages/ui/src/components/dock-card.tsxpackages/ui/src/components/file-search.tsxpackages/ui/src/components/file.csspackages/ui/src/components/icon-button.csspackages/ui/src/components/icon.tsxpackages/ui/src/components/line-comment-styles.tspackages/ui/src/components/list.csspackages/ui/src/components/menu.csspackages/ui/src/components/message-part.csspackages/ui/src/components/picker.csspackages/ui/src/components/picker.test.tspackages/ui/src/components/popover.csspackages/ui/src/components/session-review.tsxpackages/ui/src/components/session-turn.csspackages/ui/src/components/tabs.csspackages/ui/src/components/text-field.csspackages/ui/src/styles/index.css
💤 Files with no reviewable changes (1)
- packages/app/src/components/dialog-select-model-unpaid.tsx
The two action buttons live next to each other but had drifted apart: SendButton was h-[30px] / size-4 (icon), ContextUsage was size-7 / rounded-xl! (button) with a hardcoded ProgressCircle size 16. In this 13px-rem app, Tailwind size-N resolves to N * 0.25rem * 13 = N * 3.25px, so size-4 = 13 (not the 16 the design intended) and size-7 = 22.75 (not 28). The pair rendered as a 30 circle with a 13 icon next to a 22.75 circle with a 16 ring - drift in both outer and inner. Pin both ends to absolute pixels so the chrome contract is honest: 30 outer + 16 inner indicator + 7+7 padding, both buttons mirrored. Same systemic root cause as the dock header h-9 fix: Tailwind unit-spacing is not the 4pt grid this codebase's design contracts assume. Tracked for the slice 10 docs PR.
Commit 81aa587 (joined-card dock refactor) hardcoded dockProgress={1} on SessionTodoDock and removed the parent-side spring that drove it, so the dock popped in the moment props.state.dock() flipped true instead of sliding/fading in from 0. Restore the mount animation: - session-composer-region: re-introduce a useSpring tracking dockOpen, keep the segment mounted while it animates out (dockMounted memo), pass the spring value through as dockProgress. - session-todo-dock: multiply dock() into the max-height expression so the segment slides 0 → collapsed-height (or 0 → full when mounting in expanded state), in addition to the existing opacity drive that was already wired. Composer-dock spring options now appear in three places (Todo collapse, useDockCollapse for Followup/Revert, and segment mount). Extract them into a single DOCK_MOTION constant in composer/motion.ts so the contract is referenceable, not scattered. Pinning the value (0.3s, bounce:0) is the de facto standard across composer springs; hoisting to a global SPRING_TOKENS tier ladder and wiring spring through prefers-reduced-motion are tracked in #34.
…e2e assertion CI typecheck failure: createPopoverControllers returns at.active / at.setActive / slash.active / slash.setActive verbatim from useFilteredList, which exposes solid-list's createList signals as Accessor<string | null> / Setter<string | null>. The factory return interface declared them as string | undefined, which is not assignable. Consumer (prompt-input.tsx) already wraps reads with ?? undefined, confirming the null was always there and the interface was overstated. Pin the interface to string | null so the types match runtime. E2E failure: session.spec.ts asserted [data-component="prompt-variant- control"] is visible, but variant selection has been folded into the model picker popover (see prompt-input/model-controls.tsx top-of-file note). The separate variant trigger no longer renders. Drop the stale assertion.
Critical:
- dock-card: stop concatenating split.class into a single classList key.
"foo bar" was becoming a literal token and tripping
Element.classList.toggle (DOMException). Solid merges class+classList
natively when both are passed; restored to that idiom for DockCard,
DockSegment, and DockSegmentForm.
Major:
- editor-serialize: skip the block-separator newline when visit() has
already contributed one (e.g. an empty <div><br></div>). Fixes
multiline draft round-trips that were inflating "foo\n\nbar" into
"foo\n\n\nbar".
- model-picker: nested ThinkingLevel popover renders into Kobalte.Portal,
so its content sits outside the outer Content's DOM subtree. The
outer popover's onPointerDownOutside fired on every inner-popover
click and dismissed the model picker. Both popovers carry
data-picker-content; outer now ignores the dismiss when
event.target.closest("[data-picker-content]") matches a nested
picker.
- session-composer-region: permission requests can arrive while
prompt.ready() is still false (e.g. switching into a session whose
prompt is hydrating). The hydrate fallback used to show only the
loading card, leaving the user no way to approve or deny. The
fallback now renders SessionPermissionContent when a request is
pending and falls through to the loading card otherwise.
- send-button: SendButtonProps declared style?: JSX.CSSProperties |
string but the merge silently dropped the string branch. No caller
passes a string today, so narrow the prop type to JSX.CSSProperties
to match actual behavior instead of inventing a string-merge code
path.
Minor:
- keydown: the Enter-without-Shift branch had if (stopping())
handleSubmit / else handleSubmit — both arms identical, so the
conditional was dead. Inlined to a single handleSubmit call. The
stopping accessor stays in scope for the keydown probe.
- popover-controllers: include cmd.source in the custom slash command
id so workspace + user configs sharing a command name don't collapse
into one entry under useFilteredList.
Medium (gemini):
- use-dock-collapse: contentRef is now a signal driven by the JSX ref
callback, and createResizeObserver consumes that signal directly
rather than running inside a createEffect. Idiomatic and tracks
ref re-assignment correctly.
Push back:
- model-picker module-level externalOpen / openModelPicker(): the
signal is a deliberate imperative API consumed by use-session-commands
and the prompt-input keybind; multi-composer support would require
reshaping the call surface, not just the state. YAGNI for the single
composer this app has today; tracked under #34.
- editor-input draft carryover preserves text only: pre-existing
intentional design from PR #185 (commit ea01514 "carry plain-text
draft across directory switch"). Cross-workspace pill resolution
needs workspace-aware path semantics — out of slice 10 scope.
The trigger span used max-w-[7rem] (= 91px in this 13px-rem app), which truncated common longer names like "DeepSeek V4 Pro" or "Claude Sonnet 4.6" while leaving no min, so neighbor controls (workspace chip, + button) jumped by the full content-width swing whenever the active model changed. Switch to absolute pixels with both bounds: min-w-[80px] gives a stable floor that swallows the 38-65px range covering common names like "GPT-5.5", "Kimi K2.6", "GLM-5.1" with a small natural gutter, without inflating them visibly. max-w-[180px] cleanly fits all 17-character display names with truncate as the safety net beyond. The neighbor-shift budget tightens from ~140px to ~100px while short names stop looking padded. The @max-[28rem]/composer:max-w-0 collapse rule (icon-only mode in narrow composers) is preserved.
The composer-slice-10 spec asserted boundingBox 32px but the actual SendButton renders h-[30px] w-[30px] and DESIGN.md commits to the 30 dense tier. The assertion slipped through CI because it lacks the @smoke tag the smoke job filters on; it would have failed the moment someone ran the full e2e shard. Caught in external code review. Push back deferred: openModelPicker() module-level externalOpen as P3 follow-up — same reasoning as the prior round. The single-composer path cleans externalOpen on onOpenChange(false), and a multi-composer fix needs a mounted registry / focus-aware dispatch redesign rather than a state shape tweak. Tracked under issue #34.
Root cause: - PR #508 made DockCard clip overflow. Prompt popovers were still rendered inside the prompt composer card, so slash commands and @mention file suggestions could be visually clipped even when their trigger/state logic was correct. - During manual Electron verification, several early fix attempts were tested against the wrong renderer source because the worktree had symlinked node_modules resolving @opencode-ai/app back to the main checkout. That made the first debug rounds look like the fix code was ineffective when it had not actually loaded in the tested app. Fix: - Keep PromptPopover anchored inside PromptInput so the existing geometry and keyboard behavior stay intact. - Give only the prompt composer DockCard a local overflow-visible opt-out, rather than changing the global DockCard contract. - Preserve the original max-h-80 / above-input visual boundary and add pointer/z-index handling so slash and @mention rows remain visible, hoverable, and selectable. - Add regression E2E coverage for session slash commands, home slash commands, and @mention file suggestions. The tests assert viewport gap, topmost element hit-testing, and hover paint so this does not regress silently. Review follow-up: - Gemini's old Portal / ResizeObserver positioning thread was resolved as obsolete after the implementation was simplified back to the local overflow opt-out. - CodeRabbit's inline ref-cast comment was addressed by passing props.inputRef directly. - CodeRabbit's direct positioning-constant nit applied to the removed fixed-position implementation and is obsolete on the final head. Verification: - CI on PR #525: all checks passing, including typecheck, unit shards, e2e-artifacts, smoke-macos-arm64, CodeQL, CodeRabbit, lint-commits, and review checks. - Review threads: all resolved. - Local verification after final amend: bun --cwd packages/app typecheck; bun --cwd packages/app test:e2e -- e2e/prompt/prompt-slash-open.spec.ts; git diff --check. - Manual Electron verification by AstroHan: home /, home @, session /, session @, and multi-segment prompt states all behaved normally. Follow-up: - The @mention/file popover visual design may need a separate DESIGN.md alignment pass. That is intentionally left out of this hotfix so #524 stays scoped to visibility/interactivity. Closes #524.
Summary
Slice 10 of #440 rewrites the composer dock surface for the new design system: control heights unified to the 30 dense tier, picker-contract alignment for model/variant/thinking-level popovers, the three dock widgets (Todo / Followup / Revert) sharing one geometry + collapse contract, and Followup behavior shifted from steer-on-submit to queue-by-default.
Highlights
DockWidgetHeader+useDockCollapsefor header geometry and collapse animation; Todo keeps a privatedockProgressfade layer on toptext-fg-on-brand立即发送button on each queued item explicitly steers mid-streamh-[36px]absolute pixels: this app sets root font-size to 13px viahtml { font: var(--type-body) }in theme.css, which makes Tailwindh-9resolve to 2.25rem × 13 = 29.25px — short of the 30px IconButton, causing the chev to overflow and clip against the segment'soverflow-y: hidden. DESIGN.md L305 commits to absolute 36px, so absolute-pixel utilities are the honest mapping.Test plan
Followup debt
DESIGN.md updates accumulated this slice will land in a separate docs PR after manual testing closes. Notable systemic finding: this app's root font-size is 13px, so Tailwind unit-spacing is 3.25px per unit, not the 4px assumed by DESIGN.md / STANDARDS. DESIGN.md absolute-pixel contracts (
36px,30px,8px, etc.) need either a codebase-wide root font-size fix or a sweep migrating to[Npx]arbitrary-value utilities.Summary by CodeRabbit
New Features
Bug Fixes
Style
Tests