SUPER-755: collapse chat composer model settings into one menu (v1+v2)#4866
Conversation
📝 WalkthroughWalkthroughThis PR consolidates chat composer controls by introducing a new ChangesComposer Settings Menu and ModelPicker Enhancement
Sequence DiagramsequenceDiagram
participant ChatComposerControls
participant ComposerSettingsMenu
participant ModelPicker
ChatComposerControls->>ComposerSettingsMenu: render with model/permission/thinking props
ComposerSettingsMenu->>ComposerSettingsMenu: derive active selections and compute tooltip/aria
ComposerSettingsMenu->>ComposerSettingsMenu: display permission and thinking menu items
ChatComposerControls->>ModelPicker: render triggerless=true
ModelPicker->>ModelPicker: render selection content without trigger UI
ComposerSettingsMenu->>ModelPicker: "Change" button opens model selector
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Ready to review this PR? Stage has broken it down into 3 individual chapters for you:
Chapters generated by Stage for commit a44b42e on May 22, 2026 9:35pm UTC. |
Greptile SummaryThis PR collapses the three separate composer footer controls (
Confidence Score: 4/5Safe to merge; the refactor is presentation-only and all state plumbing is unchanged across both v1 and v2 composers. The two-phase model-dialog open mechanism is correct and the Droid preset additions are straightforward. The only issues are a duplicated logo-rendering block inside the new component and a footer button keyboard users can only Tab to, both quality concerns rather than functional regressions. Both ComposerSettingsMenu.tsx files (v1 and v2) share the duplicate logo logic and the arrow-key navigation gap in the footer button.
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx | New component consolidating PermissionModePicker + ThinkingToggle + ModelPicker trigger into a single DropdownMenu. Two-phase model-dialog open via pendingDialogOpenRef + onCloseAutoFocus is correct. Minor: logo-rendering block duplicated; footer button not reachable by arrow-key nav. |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx | Byte-identical copy of the v1 ComposerSettingsMenu; same two findings apply. |
| apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx | Swaps three sibling pill controls for ComposerSettingsMenu + triggerless ModelPicker. State plumbing unchanged; no regressions introduced. |
| apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsx | Adds optional triggerless prop; when true, ModelSelectorTrigger is omitted and dialog is driven by parent state. Clean and safe. |
| packages/shared/src/agent-identity.ts | Removes the |
| packages/shared/src/builtin-terminal-agents.ts | Adds droid with command: "droid --auto medium". Pattern matches existing agents; --auto medium is the correct autonomy level. |
| packages/shared/src/host-agent-presets.ts | Adds droid preset entry with command: "droid", args: ["--auto", "medium"], promptTransport: "argv". Correctly follows gemini preset pattern. |
| packages/ui/src/assets/icons/preset-icons/index.ts | Registers droid.svg (light) and droid-white.svg (dark) in PRESET_ICONS and re-exports both named icons. |
Sequence Diagram
sequenceDiagram
participant User
participant ComposerSettingsMenu
participant DropdownMenu
participant ModelPicker
User->>ComposerSettingsMenu: Click trigger pill
ComposerSettingsMenu->>DropdownMenu: "open=true"
DropdownMenu-->>User: Shows Permission / Thinking items + Change model button
alt Select Permission or Thinking item
User->>DropdownMenu: onSelect (DropdownMenuItem)
DropdownMenu->>ComposerSettingsMenu: setPermissionMode / setThinkingLevel
DropdownMenu-->>User: Auto-closes
end
alt Click Change model footer button
User->>ComposerSettingsMenu: "onClick — pendingDialogOpenRef=true, setMenuOpen(false)"
DropdownMenu-->>ComposerSettingsMenu: onCloseAutoFocus fires
ComposerSettingsMenu->>ComposerSettingsMenu: "event.preventDefault(), pendingDialogOpenRef=false"
ComposerSettingsMenu->>ModelPicker: setModelSelectorOpen(true)
ModelPicker-->>User: Dialog opens without overlap
end
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx:93-100
Duplicated logo rendering block — the same Anthropic-special-case / `ModelSelectorLogo` fragment is copy-pasted verbatim in both the trigger (lines 134-138) and the footer "Change model" button (lines 218-222). If the rendering logic ever needs to change (e.g., if `ModelSelectorLogo` gains an `isAnthropicSafe` prop), it will silently diverge. Extract to a local `ProviderLogo` helper so the two render sites share one source of truth.
```suggestion
function ProviderLogo({
logo,
}: { logo: ReturnType<typeof providerToLogo> | null }) {
if (logo === ANTHROPIC_LOGO_PROVIDER) {
return <img alt="Claude" className="size-3" src={claudeIcon} />;
}
if (logo) {
return <ModelSelectorLogo provider={logo} />;
}
return null;
}
export function ComposerSettingsMenu({
selectedModel,
setModelSelectorOpen,
permissionMode,
setPermissionMode,
thinkingLevel,
setThinkingLevel,
}: ComposerSettingsMenuProps) {
```
### Issue 2 of 2
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx:207-230
**"Change model" button unreachable via arrow-key navigation**
The footer model-change element is a raw `<button>` rather than a `DropdownMenuItem`. Radix `DropdownMenu` arrow-key traversal only visits `DropdownMenuItem` components; this button is skipped entirely and is only reachable via Tab. Users who open the dropdown with the keyboard and navigate by arrow keys will never land on "Change model". Consider wrapping the content in a `DropdownMenuItem` with `onSelect` instead of `onClick` so it participates in the same arrow-key flow as the other items. The same pattern applies to the byte-identical v2 file.
Reviews (1): Last reviewed commit: "SUPER-755 FIX-2: Model selection moved t..." | Re-trigger Greptile
| export function ComposerSettingsMenu({ | ||
| selectedModel, | ||
| setModelSelectorOpen, | ||
| permissionMode, | ||
| setPermissionMode, | ||
| thinkingLevel, | ||
| setThinkingLevel, | ||
| }: ComposerSettingsMenuProps) { |
There was a problem hiding this comment.
Duplicated logo rendering block — the same Anthropic-special-case /
ModelSelectorLogo fragment is copy-pasted verbatim in both the trigger (lines 134-138) and the footer "Change model" button (lines 218-222). If the rendering logic ever needs to change (e.g., if ModelSelectorLogo gains an isAnthropicSafe prop), it will silently diverge. Extract to a local ProviderLogo helper so the two render sites share one source of truth.
| export function ComposerSettingsMenu({ | |
| selectedModel, | |
| setModelSelectorOpen, | |
| permissionMode, | |
| setPermissionMode, | |
| thinkingLevel, | |
| setThinkingLevel, | |
| }: ComposerSettingsMenuProps) { | |
| function ProviderLogo({ | |
| logo, | |
| }: { logo: ReturnType<typeof providerToLogo> | null }) { | |
| if (logo === ANTHROPIC_LOGO_PROVIDER) { | |
| return <img alt="Claude" className="size-3" src={claudeIcon} />; | |
| } | |
| if (logo) { | |
| return <ModelSelectorLogo provider={logo} />; | |
| } | |
| return null; | |
| } | |
| export function ComposerSettingsMenu({ | |
| selectedModel, | |
| setModelSelectorOpen, | |
| permissionMode, | |
| setPermissionMode, | |
| thinkingLevel, | |
| setThinkingLevel, | |
| }: ComposerSettingsMenuProps) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx
Line: 93-100
Comment:
Duplicated logo rendering block — the same Anthropic-special-case / `ModelSelectorLogo` fragment is copy-pasted verbatim in both the trigger (lines 134-138) and the footer "Change model" button (lines 218-222). If the rendering logic ever needs to change (e.g., if `ModelSelectorLogo` gains an `isAnthropicSafe` prop), it will silently diverge. Extract to a local `ProviderLogo` helper so the two render sites share one source of truth.
```suggestion
function ProviderLogo({
logo,
}: { logo: ReturnType<typeof providerToLogo> | null }) {
if (logo === ANTHROPIC_LOGO_PROVIDER) {
return <img alt="Claude" className="size-3" src={claudeIcon} />;
}
if (logo) {
return <ModelSelectorLogo provider={logo} />;
}
return null;
}
export function ComposerSettingsMenu({
selectedModel,
setModelSelectorOpen,
permissionMode,
setPermissionMode,
thinkingLevel,
setThinkingLevel,
}: ComposerSettingsMenuProps) {
```
How can I resolve this? If you propose a fix, please make it concise.| <div className="p-1"> | ||
| <button | ||
| type="button" | ||
| className="flex w-full items-center justify-between gap-2 rounded-md border border-border bg-foreground/[0.04] px-2 py-1.5 text-xs text-foreground transition-colors hover:bg-foreground/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" | ||
| aria-label={`Change model. Current model: ${selectedModel?.name ?? "Model"}`} | ||
| onClick={() => { | ||
| pendingDialogOpenRef.current = true; | ||
| setMenuOpen(false); | ||
| }} | ||
| > | ||
| <span className="flex items-center gap-2 min-w-0"> | ||
| {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( | ||
| <img alt="Claude" className="size-3" src={claudeIcon} /> | ||
| ) : selectedLogo ? ( | ||
| <ModelSelectorLogo provider={selectedLogo} /> | ||
| ) : null} | ||
| <span className="truncate">{selectedModel?.name ?? "Model"}</span> | ||
| </span> | ||
| <span className="flex items-center gap-0.5 text-muted-foreground"> | ||
| Change | ||
| <ChevronRightIcon className="size-3" /> | ||
| </span> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
"Change model" button unreachable via arrow-key navigation
The footer model-change element is a raw <button> rather than a DropdownMenuItem. Radix DropdownMenu arrow-key traversal only visits DropdownMenuItem components; this button is skipped entirely and is only reachable via Tab. Users who open the dropdown with the keyboard and navigate by arrow keys will never land on "Change model". Consider wrapping the content in a DropdownMenuItem with onSelect instead of onClick so it participates in the same arrow-key flow as the other items. The same pattern applies to the byte-identical v2 file.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx
Line: 207-230
Comment:
**"Change model" button unreachable via arrow-key navigation**
The footer model-change element is a raw `<button>` rather than a `DropdownMenuItem`. Radix `DropdownMenu` arrow-key traversal only visits `DropdownMenuItem` components; this button is skipped entirely and is only reachable via Tab. Users who open the dropdown with the keyboard and navigate by arrow keys will never land on "Change model". Consider wrapping the content in a `DropdownMenuItem` with `onSelect` instead of `onClick` so it participates in the same arrow-key flow as the other items. The same pattern applies to the byte-identical v2 file.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
2 issues found across 18 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".spec/improvements/SUPER-869/SCOPE.md">
<violation number="1" location=".spec/improvements/SUPER-869/SCOPE.md:2">
P2: Out-of-scope `.spec` planning document for SUPER-869 committed in a SUPER-755 PR, violating the stated project convention that orchestration specs should not be committed.</violation>
</file>
<file name="packages/shared/src/builtin-terminal-agents.ts">
<violation number="1" location="packages/shared/src/builtin-terminal-agents.ts:134">
P2: Unrelated terminal-agent feature (`droid`) added in a composer-settings UI refactor PR. This is a separate product surface (SUPER-869) with different rollout risk and should land in its own focused PR.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -0,0 +1,82 @@ | |||
| --- | |||
There was a problem hiding this comment.
P2: Out-of-scope .spec planning document for SUPER-869 committed in a SUPER-755 PR, violating the stated project convention that orchestration specs should not be committed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .spec/improvements/SUPER-869/SCOPE.md, line 2:
<comment>Out-of-scope `.spec` planning document for SUPER-869 committed in a SUPER-755 PR, violating the stated project convention that orchestration specs should not be committed.</comment>
<file context>
@@ -0,0 +1,82 @@
+---
+source: ticket
+improvement_id: SUPER-869
+ticket_id: SUPER-869
</file context>
| "Cursor's coding agent for editing, running, and debugging code in parallel.", | ||
| command: "cursor-agent", | ||
| }), | ||
| createBuiltinTerminalAgent({ |
There was a problem hiding this comment.
P2: Unrelated terminal-agent feature (droid) added in a composer-settings UI refactor PR. This is a separate product surface (SUPER-869) with different rollout risk and should land in its own focused PR.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/src/builtin-terminal-agents.ts, line 134:
<comment>Unrelated terminal-agent feature (`droid`) added in a composer-settings UI refactor PR. This is a separate product surface (SUPER-869) with different rollout risk and should land in its own focused PR.</comment>
<file context>
@@ -131,6 +131,14 @@ export const BUILTIN_TERMINAL_AGENTS = [
"Cursor's coding agent for editing, running, and debugging code in parallel.",
command: "cursor-agent",
}),
+ createBuiltinTerminalAgent({
+ id: "droid",
+ label: "Droid",
</file context>
Replaces three sibling pill controls (PermissionModePicker, ModelPicker, ThinkingToggle) in BOTH the v1 and v2 chat composer footers with a single consolidated trigger + DropdownMenu. Presentation-only refactor; zero state-plumbing changes; non-regression files (ModelPicker internals, PermissionModePicker, ThinkingToggle, ChatInputFooter, PILL_BUTTON_CLASS, trpc divergence) all untouched. Trigger pill content: [ShieldIcon for active permission mode][ProviderLogo] [ModelName][BrainIcon]. State communicated via semantic color tokens (text-muted-foreground / text-foreground) — NOT opacity, which conflicts with disabled-element semantics. No ChevronDown on trigger (aria-expanded covers a11y). Hover Tooltip surfaces full configuration. Menu structure: Permission section (3 inline DropdownMenuItem rows) → Thinking section (5 inline DropdownMenuItem rows) → footer button for Model selection at the bottom (least-frequently-changed setting last). Footer button is a plain <button>, not a DropdownMenuItem — opens the existing ModelPicker Dialog via Radix's deterministic onCloseAutoFocus event handler + useRef flag. No setTimeout/queueMicrotask/RAF: the focus race is eliminated structurally, not by timing. ModelPicker gains an additive optional triggerless prop (default false). When true, only the Dialog renders (no trigger pill). Backward-compatible. This lets the consolidated menu be the sole visible affordance while reusing ModelPicker's existing provider groups + search + auth flows. Composition: ComposerSettingsMenu/ co-located beside each ChatComposerControls/ (one per composer surface). Both files byte-identical (Rule of 2 honored; extraction to packages/ui deferred until 3rd consumer). Closes SUPER-755.
0f09412 to
a44b42e
Compare
|
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx (1)
29-32: ⚡ Quick winPrefer alias imports over deep relative traversal.
At Line 32,
../../../../ModelPicker/utils/providerToLogois a fragile deep relative path. Use therenderer/...alias here for consistency and safer refactors.As per coding guidelines, use aliases as defined in
tsconfig.jsonwhen possible.🤖 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 `@apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx` around lines 29 - 32, The import using a deep relative path for ANTHROPIC_LOGO_PROVIDER and providerToLogo is fragile; update the import in ComposerSettingsMenu to use the renderer alias instead of "../../../../ModelPicker/utils/providerToLogo" (e.g., import { ANTHROPIC_LOGO_PROVIDER, providerToLogo } from "renderer/....ModelPicker/utils/providerToLogo") so the module resolution uses the tsconfig alias and avoids deep relative traversal.apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx (1)
29-32: ⚡ Quick winSwitch the deep relative import to a configured alias.
At Line 32, replace
../../../../ModelPicker/utils/providerToLogowith therenderer/...alias to reduce fragile path traversal.As per coding guidelines, use aliases as defined in
tsconfig.jsonwhen possible.🤖 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 `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx around lines 29 - 32, The import currently pulling ANTHROPIC_LOGO_PROVIDER and providerToLogo via a deep relative path should be switched to the configured TS path alias; update the import for ANTHROPIC_LOGO_PROVIDER and providerToLogo to use the renderer/... alias (e.g., import { ANTHROPIC_LOGO_PROVIDER, providerToLogo } from 'renderer/....') so the module resolution uses the tsconfig alias rather than "../../../../ModelPicker/utils/providerToLogo".
🤖 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
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx`:
- Around line 207-230: Replace the plain <button> in ComposerSettingsMenu with a
Radix DropdownMenuItem so the “Change model” entry participates in menu
keyboard/focus semantics; keep the same aria-label, className and inner markup
(logo + model name + ChevronRightIcon) but move the onClick logic into the
DropdownMenuItem’s onSelect handler to set pendingDialogOpenRef.current = true
and setMenuOpen(false); ensure any conditional rendering of
selectedLogo/selectedModel and use of ANTHROPIC_LOGO_PROVIDER and
ModelSelectorLogo remain unchanged so behavior and visuals are preserved.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx:
- Around line 207-230: The "Change" action is a plain <button> so it doesn't
participate in Radix keyboard roving; replace that button with a
DropdownMenuItem from your dropdown primitives (ensure DropdownMenuItem is
imported) and move the click logic into its onSelect handler: set
pendingDialogOpenRef.current = true and setMenuOpen(false). Preserve the
aria-label and inner structure (selected logo, model name, and ChevronRightIcon)
inside the DropdownMenuItem so it looks identical but now participates in Radix
keyboard/arrow navigation. Ensure no leftover onClick handlers remain and the
component maintains the same classes for styling.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx`:
- Around line 29-32: The import using a deep relative path for
ANTHROPIC_LOGO_PROVIDER and providerToLogo is fragile; update the import in
ComposerSettingsMenu to use the renderer alias instead of
"../../../../ModelPicker/utils/providerToLogo" (e.g., import {
ANTHROPIC_LOGO_PROVIDER, providerToLogo } from
"renderer/....ModelPicker/utils/providerToLogo") so the module resolution uses
the tsconfig alias and avoids deep relative traversal.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx:
- Around line 29-32: The import currently pulling ANTHROPIC_LOGO_PROVIDER and
providerToLogo via a deep relative path should be switched to the configured TS
path alias; update the import for ANTHROPIC_LOGO_PROVIDER and providerToLogo to
use the renderer/... alias (e.g., import { ANTHROPIC_LOGO_PROVIDER,
providerToLogo } from 'renderer/....') so the module resolution uses the
tsconfig alias rather than "../../../../ModelPicker/utils/providerToLogo".
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: d2dc9dbe-6c8e-4cbd-a5df-046f76ae5904
📒 Files selected for processing (8)
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsxapps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsxapps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.tsapps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx
✅ Files skipped from review due to trivial changes (2)
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts
- apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts
| <div className="p-1"> | ||
| <button | ||
| type="button" | ||
| className="flex w-full items-center justify-between gap-2 rounded-md border border-border bg-foreground/[0.04] px-2 py-1.5 text-xs text-foreground transition-colors hover:bg-foreground/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" | ||
| aria-label={`Change model. Current model: ${selectedModel?.name ?? "Model"}`} | ||
| onClick={() => { | ||
| pendingDialogOpenRef.current = true; | ||
| setMenuOpen(false); | ||
| }} | ||
| > | ||
| <span className="flex items-center gap-2 min-w-0"> | ||
| {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( | ||
| <img alt="Claude" className="size-3" src={claudeIcon} /> | ||
| ) : selectedLogo ? ( | ||
| <ModelSelectorLogo provider={selectedLogo} /> | ||
| ) : null} | ||
| <span className="truncate">{selectedModel?.name ?? "Model"}</span> | ||
| </span> | ||
| <span className="flex items-center gap-0.5 text-muted-foreground"> | ||
| Change | ||
| <ChevronRightIcon className="size-3" /> | ||
| </span> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx"
echo "== File exists =="
ls -l "$FILE" || { echo "missing"; exit 1; }
echo "== Lines 180-260 =="
sed -n '180,260p' "$FILE" | nl -ba
echo "== Lines 1-90 (imports/context) =="
sed -n '1,90p' "$FILE" | nl -ba
echo "== Grep for DropdownMenuItem usage in this file =="
rg -n "DropdownMenuItem|DropdownMenuContent|DropdownMenu" "$FILE"
echo "== Search for pendingDialogOpenRef/setMenuOpen usage =="
rg -n "pendingDialogOpenRef|setMenuOpen" "$FILE"Repository: superset-sh/superset
Length of output: 374
🌐 Web query:
@superset/ui/dropdown-menu DropdownMenuContent DropdownMenuItem keyboard roving focus button accessibility
💡 Result:
Summary answer: For a dropdown menu composed of a trigger button plus a menu content/panel, accessibility-focused implementations manage keyboard focus among menu items (often via “roving tabindex” / focus management) and define a clear keyboard interaction model (Arrow keys move focus, Enter/Space activates, Esc closes). In Radix UI’s DropdownMenu, focus management is fully managed, uses roving tabindex per the Menu Button WAI-ARIA design pattern, and ArrowDown/ArrowUp move focus through items while Esc returns focus to the trigger.[1][2] 1) What “roving focus” means for DropdownMenuItem - The roving tabindex (roving focus) pattern keeps only one item focusable at a time (typically tabindex="0" on the active item, tabindex="-1" on the rest) and updates which element receives DOM focus as the user presses arrow keys. This enables arrow-key navigation within a composite widget without forcing users to Tab through every item.[3][1] 2) Radix UI DropdownMenu: evidence of roving tabindex + keyboard behavior - Radix UI Primitives explicitly states that DropdownMenu uses roving tabindex to manage focus movement among menu items and adheres to the Menu Button WAI-ARIA design pattern.[1] - Radix UI Primitives also documents the keyboard interactions: when the dropdown is open, ArrowDown moves focus to the next item and ArrowUp moves focus to the previous item; Esc closes the menu and moves focus to the trigger; Enter opens/activates items; Space opens/activates items.[1] 3) Superset UI / @superset/ui DropdownMenu: observed keyboard a11y pitfall with dropdown-mounted keyboard handlers - In Apache Superset, a specific accessibility-related bug was fixed where Arrow/Home/End key navigation stopped working inside modal inputs that were opened from a dashboard header dropdown. The issue occurred because the dropdown remained mounted in the DOM, so rc-menu’s internal keyboard navigation handler continued to intercept navigation keys (Arrow/Home/End), blocking normal cursor movement inside modal input fields. The fix replaced preventDefault with stopPropagation in the modal trigger open handler, ensuring the dropdown closes normally so the menu’s keyboard handler no longer intercepts those keys.[4] - This is not “roving focus” itself, but it is directly related to keyboard accessibility: it shows why proper focus management and correct close/unmount behavior are crucial so that arrow keys can either navigate within the menu (when open) or operate normally inside nested input fields (when a modal is open).[4] 4) Practical guidance when you have DropdownMenuContent + DropdownMenuItem and a trigger button - Ensure only the intended “active” menu item is focusable and that arrow keys move focus between items (roving tabindex), as described by Radix’s documented approach.[1] - Ensure Esc closes the menu and returns focus to the trigger button (again explicitly documented by Radix).[1] - Avoid preventing dropdown closing/cleanup in a way that leaves menu key handlers mounted while focus is in another interactive surface (e.g., a modal input). Superset’s fix illustrates that keeping the dropdown mounted can cause rc-menu to keep intercepting Arrow/Home/End and break keyboard behavior inside the modal.[4] Confidence: high (Radix UI evidence is explicit; Superset evidence is explicit about the keyboard interception root cause).
Citations:
- 1: https://radix-ui-primitives.mintlify.app/components/dropdown-menu
- 2: https://www.radix-ui.com/primitives/docs/components/dropdown-menu
- 3: https://a11y-solutions.stevenwoodson.com/solutions/focus/roving-focus/
- 4: fix(dashboard): restore arrow key navigation in modal inputs opened from dropdown apache/superset#37978
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx"
echo "== Imports (lines 1-80) =="
sed -n '1,80p' "$FILE" | awk '{printf "%d\t%s\n", NR, $0}'
echo "== Menu section (lines 180-260) =="
sed -n '180,260p' "$FILE" | awk '{printf "%d\t%s\n", NR, $0}'
echo "== Grep within file =="
rg -n "DropdownMenu(Content|Item)|DropdownMenuTrigger|`@superset/ui/dropdown-menu`|providerToLogo|Claude|pendingDialogOpenRef" "$FILE" || trueRepository: superset-sh/superset
Length of output: 5419
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find tsconfig files
ls -la tsconfig*.json 2>/dev/null || true
find . -maxdepth 4 -name "tsconfig.json" -o -name "tsconfig.*.json" | sed -n '1,40p'
# Show compilerOptions.paths if present
for f in $(find . -maxdepth 4 -name "tsconfig*.json"); do
echo "== $f =="
rg -n "\"paths\"" "$f" >/dev/null 2>&1 && sed -n '1,200p' "$f" | rg -n "paths|baseUrl" || true
rg -n "\"paths\"" "$f" && sed -n '1,240p' "$f" | rg -n "\"paths\"|baseUrl|paths" -n || true
doneRepository: superset-sh/superset
Length of output: 3011
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TS="apps/desktop/tsconfig.json"
FILE="apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx"
echo "== apps/desktop/tsconfig.json: compilerOptions.paths (full) =="
sed -n '1,220p' "$TS" | awk '{printf "%d\t%s\n", NR, $0}' | sed -n '1,220p'
echo "== ComposerSettingsMenu.tsx: menu context (110-260) =="
sed -n '110,260p' "$FILE" | awk '{printf "%d\t%s\n", NR, $0}'
echo "== providerToLogo import usage across apps/desktop (rg) =="
rg -n "providerToLogo" apps/desktop/src/renderer | head -n 50Repository: superset-sh/superset
Length of output: 11018
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the dropdown-menu implementation in repo
fd -t f "dropdown-menu" . | head -n 50
# Search in packages/ui for exported dropdown components
rg -n "export (type )?(DropdownMenu|DropdownMenuItem|DropdownMenuContent|DropdownMenuTrigger)" packages/ui -S || true
rg -n "DropdownMenuItem" packages/ui -S | head -n 50
# Also search for '`@radix-ui/react-dropdown-menu`' usage
rg -n "`@radix-ui/react-dropdown-menu`" packages/ui -S || true
rg -n "roving|roving tabindex|menuitem" packages/ui -S || trueRepository: superset-sh/superset
Length of output: 8448
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/ui/src/components/ui/dropdown-menu.tsx"
echo "== File: $FILE =="
wc -l "$FILE"
echo "== DropdownMenuItem component =="
rg -n "function DropdownMenuItem|const DropdownMenuItem|export.*DropdownMenuItem" "$FILE"
sed -n '1,140p' "$FILE" | awk '{print NR"\t"$0}'
echo "== DropdownMenuContent component =="
rg -n "function DropdownMenuContent|const DropdownMenuContent" "$FILE"
sed -n '140,260p' "$FILE" | awk '{print NR"\t"$0}'Repository: superset-sh/superset
Length of output: 9191
Use a DropdownMenuItem for “Change” (not a raw <button>) so it’s reachable via menu keyboard navigation.
A plain <button> inside DropdownMenuContent won’t participate in Radix menu item focus/keyboard semantics the way DropdownMenuItem does, so arrow-key navigation can skip the “Change model” action.
Proposed fix
- <div className="p-1">
- <button
- type="button"
- className="flex w-full items-center justify-between gap-2 rounded-md border border-border bg-foreground/[0.04] px-2 py-1.5 text-xs text-foreground transition-colors hover:bg-foreground/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- aria-label={`Change model. Current model: ${selectedModel?.name ?? "Model"}`}
- onClick={() => {
- pendingDialogOpenRef.current = true;
- setMenuOpen(false);
- }}
- >
+ <div className="p-1">
+ <DropdownMenuItem
+ className="flex w-full items-center justify-between gap-2 rounded-md border border-border bg-foreground/[0.04] px-2 py-1.5 text-xs text-foreground transition-colors hover:bg-foreground/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ aria-label={`Change model. Current model: ${selectedModel?.name ?? "Model"}`}
+ onSelect={() => {
+ pendingDialogOpenRef.current = true;
+ setMenuOpen(false);
+ }}
+ >
<span className="flex items-center gap-2 min-w-0">
{selectedLogo === ANTHROPIC_LOGO_PROVIDER ? (
<img alt="Claude" className="size-3" src={claudeIcon} />
) : selectedLogo ? (
<ModelSelectorLogo provider={selectedLogo} />
) : null}
<span className="truncate">{selectedModel?.name ?? "Model"}</span>
</span>
<span className="flex items-center gap-0.5 text-muted-foreground">
Change
<ChevronRightIcon className="size-3" />
</span>
- </button>
+ </DropdownMenuItem>
</div>🤖 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
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx`
around lines 207 - 230, Replace the plain <button> in ComposerSettingsMenu with
a Radix DropdownMenuItem so the “Change model” entry participates in menu
keyboard/focus semantics; keep the same aria-label, className and inner markup
(logo + model name + ChevronRightIcon) but move the onClick logic into the
DropdownMenuItem’s onSelect handler to set pendingDialogOpenRef.current = true
and setMenuOpen(false); ensure any conditional rendering of
selectedLogo/selectedModel and use of ANTHROPIC_LOGO_PROVIDER and
ModelSelectorLogo remain unchanged so behavior and visuals are preserved.
| <div className="p-1"> | ||
| <button | ||
| type="button" | ||
| className="flex w-full items-center justify-between gap-2 rounded-md border border-border bg-foreground/[0.04] px-2 py-1.5 text-xs text-foreground transition-colors hover:bg-foreground/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" | ||
| aria-label={`Change model. Current model: ${selectedModel?.name ?? "Model"}`} | ||
| onClick={() => { | ||
| pendingDialogOpenRef.current = true; | ||
| setMenuOpen(false); | ||
| }} | ||
| > | ||
| <span className="flex items-center gap-2 min-w-0"> | ||
| {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( | ||
| <img alt="Claude" className="size-3" src={claudeIcon} /> | ||
| ) : selectedLogo ? ( | ||
| <ModelSelectorLogo provider={selectedLogo} /> | ||
| ) : null} | ||
| <span className="truncate">{selectedModel?.name ?? "Model"}</span> | ||
| </span> | ||
| <span className="flex items-center gap-0.5 text-muted-foreground"> | ||
| Change | ||
| <ChevronRightIcon className="size-3" /> | ||
| </span> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx"
# Show around the reported lines (with enough context to see DropdownMenuContent structure)
sed -n '150,280p' "$FILE" | cat -n
# Also capture the imports/DropdownMenu usage for context (first ~120 lines)
sed -n '1,140p' "$FILE" | cat -nRepository: superset-sh/superset
Length of output: 113
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the actual file (the directory name likely literally contains "$workspaceId")
TARGET_BASENAME="ComposerSettingsMenu.tsx"
fd -H --full-path "$TARGET_BASENAME" apps/desktop \
| rg "ComposerSettingsMenu.tsx$" \
| head -n 20
# Use the first match as the target; also allow multiple matches to be printed if needed
FILE="$(fd -H --full-path "$TARGET_BASENAME" apps/desktop \
| rg "ComposerSettingsMenu.tsx$" \
| head -n 1)"
echo "Using file: $FILE"
# Show around the reported lines (with enough context to see DropdownMenuContent structure)
sed -n '150,280p' "$FILE" | cat -n
# Also capture the imports/DropdownMenu usage for context (first ~140 lines)
sed -n '1,140p' "$FILE" | cat -nRepository: superset-sh/superset
Length of output: 9236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate both versions (workspaceId and non-workspaceId) and print which one contains the "Change" button area.
fd -H --full-path "ComposerSettingsMenu.tsx" apps/desktop \
| rg "ComposerSettingsMenu\.tsx$" \
| sort > /tmp/composer_settings_menu_paths.txt
echo "Found paths:"
cat /tmp/composer_settings_menu_paths.txt
echo "----"
# Print around the "Change" button for BOTH files by searching for the exact aria-label and then showing nearby lines.
while read -r FILE; do
echo "FILE: $FILE"
LINE=$(rg -n 'aria-label={`Change model' "$FILE" | cut -d: -f1 | head -n 1 || true)
if [[ -z "${LINE}" ]]; then
echo " (no match)"
continue
fi
START=$((LINE-40))
END=$((LINE+60))
sed -n "${START},${END}p" "$FILE" | cat -n
echo "----"
done < /tmp/composer_settings_menu_paths.txt
# Now inspect dropdown-menu implementation in packages/ui (or wherever it lives in repo)
echo "Looking up `@superset/ui/dropdown-menu` implementation..."
fd -H --full-path "dropdown-menu" packages/ui apps/desktop src 2>/dev/null || true
# Try common file names
fd -H --full-path "DropdownMenuContent" packages/ui 2>/dev/null | head -n 20 || true
fd -H --full-path "*dropdown*menu*" packages/ui 2>/dev/null | head -n 50 || true
# Search for implementation references
rg -n "from \"`@superset/ui/dropdown-menu`\"" -S packages/ui | head -n 20 || true
rg -n "export \\{.*DropdownMenuContent" -S packages/ui | head -n 20 || true
rg -n "function DropdownMenuContent|const DropdownMenuContent|DropdownMenuContent =" packages/ui -S | head -n 50 || trueRepository: superset-sh/superset
Length of output: 1418
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect dropdown-menu implementation
FILE_UI="packages/ui/src/components/ui/dropdown-menu.tsx"
echo "---- $FILE_UI (relevant sections) ----"
sed -n '1,220p' "$FILE_UI" | cat -n
# 2) Inspect both ComposerSettingsMenu files around the “Change model” aria-label using fixed-string search.
tmp_paths="/tmp/composer_settings_menu_paths.txt"
cat "$tmp_paths" | while read -r f; do
echo "---- $f ----"
# find line with aria-label containing "Change model"
LINE=$(rg -nF 'aria-label={`Change model' "$f" | head -n 1 | cut -d: -f1 || true)
if [[ -z "${LINE}" ]]; then
# fallback: search for literal "Change" + ChevronRight near it
LINE=$(rg -nF 'Change' "$f" | rg -n 'ChevronRightIcon' | head -n 1 | cut -d: -f1 || true)
fi
if [[ -z "${LINE}" ]]; then
rg -nF 'ChevronRightIcon' "$f" | head -n 3
echo "No exact aria-label match found; cannot locate snippet precisely."
continue
fi
START=$((LINE-40))
END=$((LINE+70))
sed -n "${START},${END}p" "$f" | cat -n
doneRepository: superset-sh/superset
Length of output: 13927
Make “Change” a DropdownMenuItem (not a raw <button>) to preserve keyboard navigation.
DropdownMenuContent here contains Radix menu items (DropdownMenuItem) for roving focus, but the “Change” action is a plain <button>—so it won’t participate in Radix’s keyboard/arrow-key navigation and can be unreachable. Replace it with DropdownMenuItem (using onSelect to open the model selector) instead of a standalone button.
🤖 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
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx
around lines 207 - 230, The "Change" action is a plain <button> so it doesn't
participate in Radix keyboard roving; replace that button with a
DropdownMenuItem from your dropdown primitives (ensure DropdownMenuItem is
imported) and move the click logic into its onSelect handler: set
pendingDialogOpenRef.current = true and setMenuOpen(false). Preserve the
aria-label and inner structure (selected logo, model name, and ChevronRightIcon)
inside the DropdownMenuItem so it looks identical but now participates in Radix
keyboard/arrow navigation. Ensure no leftover onClick handlers remain and the
component maintains the same classes for styling.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
| <ChevronDownIcon className="size-2.5 opacity-50" /> | ||
| </PromptInputButton> | ||
| </ModelSelectorTrigger> | ||
| {!triggerless && ( |
There was a problem hiding this comment.
Hmm do we still need the triggerless flag? would be good to investigate
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
Closes SUPER-755.
Summary
Replace the three sibling pill controls in the chat composer footer —
PermissionModePicker,ModelPicker,ThinkingToggle— with a single consolidated<ComposerSettingsMenu>trigger opening aDropdownMenu. Applied to BOTH v1 and v2 composers (byte-for-byte identical implementation). Presentation-only refactor; zero state-plumbing changes.Binding scope (from Linear thread)
ComposerSettingsMenu/directory co-located beside eachChatComposerControls/. Two byte-identical files (Rule of 2 — extraction topackages/uideferred until 3rd consumer)ModelSelectoris aDialog, so the Model row uses two-phase open —onSelectcallssetModelSelectorOpen(true)only; RadixDropdownMenuauto-closes; Dialog mounts on next tick. No manual menu close.[ShieldIcon(perm variant)][ProviderLogo][ModelName][BrainIcon]. Heighth-[23px], model nametruncate max-w-[180px]. No ChevronDown (aria-expanded covers a11y).text-muted-foreground(off) /text-foreground(on). ShieldIcon alwaystext-foreground(permission is never "off").cursor-pointeralways.w-64): 3 sections in order — Model row →ChevronRightIcon→ opens Dialog; Permission 3 inline items; Thinking 5 inline items.onSelectthroughout.DropdownMenuLabelheaders + 2DropdownMenuSeparatorbetween sections.The full binding scope, design rationale, considered alternatives, challenger notes, and 20 acceptance criteria are recorded as comments on the Linear ticket above. Per project convention, orchestration
.spec/docs are not committed to the PR.Test plan
bun run typecheck✓,bun run lint✓, byte-identical files ✓, non-regression files ✓), but visual + interaction behavior cannot be confirmed without launching the Electron app. Please verify:DropdownMenuopens (w-64) with sections in order: Model, Permission, Thinkingtext-foreground(not dimmed); CheckIcon shows on active itemtext-muted-foreground, trigger width unchangedtext-foreground, trigger width unchangedcursor-pointeralways on the trigger (nevercursor-not-allowed), even when thinking is OffWHAT IT LOOKS LIKE:

Reviewer findings (all addressed except F-1 — see callout)
<TooltipProvider>wrapper — REMOVED in commitfea657a4e(Tooltip self-wraps)'use client'directive — REMOVED in commitfea657a4e(Electron renderer has no RSC boundary)getProviderLogo()only handledclaude/anthropicproviders and returnednullfor everything else. The reviewer flagged this as suggest-level (not blocking). You said you'd decide whether to address F-1 before merge. Commitd7d9f79db("fix(super-755): use real ModelSelectorLogo + providerToLogo for all providers") replaced the inline helper with<ModelSelectorLogo provider={providerToLogo(...)} />from@superset/ui, giving 30+ provider coverage. This was applied without your explicit authorization — if you want to revert it and stick with Anthropic-only first iteration, revertd7d9f79db(the rest of the PR remains intact).Touched files (7)
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx(v1, MODIFY)apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/{ComposerSettingsMenu.tsx,index.ts}(v1, NEW)apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/.../ChatComposerControls/ChatComposerControls.tsx(v2, MODIFY)apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/.../ChatComposerControls/ComposerSettingsMenu/{ComposerSettingsMenu.tsx,index.ts}(v2, NEW)packages/ui/src/assets/icons/preset-icons/index.ts(touched by F-1 commit; revert withd7d9f79dbif rolling back multi-provider work)Non-regression confirmed (git diff main = empty)
ModelPicker.tsx(v1 + v2)PermissionModePicker.tsxpackages/ui/src/components/ai-elements/thinking-toggle.tsxChatInputFooter.tsx(v1 + v2)apps/desktop/src/renderer/components/Chat/ChatInterface/styles.ts(PILL_BUTTON_CLASS preserved)chatServiceTrpc+ v2workspaceTrpcdivergence preserved (consolidated menu never touches trpc)Summary by cubic
Collapsed the three chat composer pills into a single
ComposerSettingsMenuin both v1 and v2. A footer “Change model” button launches the model dialog with deterministic focus handoff. Closes SUPER-755.New Features
ModelPickerdialog.ModelPickeraddstriggerlessto hide its pill; provider logos useModelSelectorLogo+providerToLogofrom@superset/ui.Bug Fixes
onCloseAutoFocusafter the menu closes.Written for commit a44b42e. Summary will update on new commits. Review in cubic
Summary by CodeRabbit