Skip to content

feature (desktop): presets bar + group strip UX polish#1727

Merged
AviPeltz merged 6 commits into
mainfrom
default-open-presets
Feb 24, 2026
Merged

feature (desktop): presets bar + group strip UX polish#1727
AviPeltz merged 6 commits into
mainfrom
default-open-presets

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Feb 23, 2026

Summary

Overhauls the desktop presets bar and group strip UX. The presets bar now defaults to visible and introduces pin/unpin semantics so users control which presets appear. The group strip's "+" button is simplified to core tab types and stays accessible even when tabs overflow.

What changed

Presets bar (PresetsBar.tsx)

  • Manage dropdown — new gear-icon menu lists all presets (existing + quick-add templates from AGENT_TYPES). Each row shows a pin/unpin toggle or a "+" icon for templates not yet created. Keyboard shortcuts are displayed inline via DropdownMenuShortcut.
  • Pin semantics — only presets with pinnedToBar !== false render as quick-launch buttons. Clicking a preset in the manage dropdown toggles its pin state; clicking a template that doesn't exist yet creates it pinned.
  • Show/hide toggle — the "Show Preset Bar" checkbox and a "Manage Presets" link (→ settings page) live inside the manage dropdown.

Group strip (GroupStrip.tsx)

  • Simplified "+" dropdown — removed preset list, "Configure Presets" link, and star icons. Now only offers Terminal / Chat / Browser with their hotkeys, plus the "Show Preset Bar" toggle.
  • Overflow-aware layout — uses ResizeObserver + useLayoutEffect to detect when tabs exceed the container width. When they do, the "+" button pins to the right edge outside the scroll area so it's always reachable.
  • Cleanup — removed unused imports (useNavigate, usePresets, HiStar, HiMiniChevronDown, PresetMenuItemShortcut, tooltip wrappers). Replaced inline button group with a single compact icon button.

Data model

  • Added optional pinnedToBar: boolean to the terminal preset schema (packages/local-db/src/schema/zod.ts) and plumbed it through the settings tRPC create and update mutations.

Defaults & styling

  • DEFAULT_SHOW_PRESETS_BAR flipped from falsetrue so the bar is visible out of the box.
  • TopBar and WorkspaceSidebar backgrounds changed from bg-background to bg-muted/45 dark:bg-muted/35 for a subtle tinted look.

Test plan

  • Open desktop app — presets bar should be visible by default
  • Click gear icon in presets bar → manage dropdown opens with all presets + agent templates
  • Pin/unpin presets from the dropdown and verify the bar updates
  • Quick-add a template preset (e.g. an agent type not yet created) — it should appear pinned
  • Verify keyboard shortcuts display correctly in both the manage dropdown and the "+" menu
  • Open enough tabs to cause horizontal overflow — "+" button should remain visible at the right edge
  • Toggle "Show Preset Bar" from both the presets bar dropdown and the "+" dropdown
  • Confirm TopBar and sidebar have the updated muted tint in both light and dark themes

Summary by CodeRabbit

  • New Features

    • Pin terminal presets to the preset bar; preset bar now shown by default.
    • Preset management moved to an interactive dropdown with quick-add templates, pin toggles and visible hotkey labels.
    • New plus/dropdown control for adding Terminal, Chat, and Browser tabs with overflow-aware tab strip behavior.
  • Hotkeys

    • Added "New Chat" hotkey and updated the "Reopen Tab" keybinding.
  • Style

    • Updated top-bar and sidebar backgrounds for improved light/dark appearance.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Adds optional pinnedToBar to TerminalPreset schema and TRPC inputs; converts presets UI to a dropdown-driven manager with pin toggles and optimistic TRPC updates; refactors tab strip for horizontal overflow and plus-control; adds HotkeyMenuShortcut and NEW_CHAT hotkey; tweaks top/side bar translucency and enables the presets bar by default.

Changes

Cohort / File(s) Summary
Schema & Local DB
packages/local-db/src/schema/zod.ts
Added optional pinnedToBar?: boolean to terminalPresetSchema and inferred TerminalPreset type.
TRPC Settings Router
apps/desktop/src/lib/trpc/routers/settings/index.ts
createTerminalPreset and updateTerminalPreset inputs now accept optional pinnedToBar; updateTerminalPreset applies input.patch.pinnedToBar when defined.
Presets UI
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx
Replaced in-place preset controls with a dropdown manager: template creation, pin toggle (mutations with optimistic updates), show-presets toggle, managed/pinned preset lists.
Tab Strip / GroupStrip
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
Refactored tab rendering into a horizontal scroll track with overflow detection, added dropdown plus-control, per-tab status mapping, removed legacy preset navigation/actions.
Hotkey UI Component
apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx, .../index.ts
Added HotkeyMenuShortcut component and re-export; renders DropdownMenuShortcut for assigned hotkeys.
Hotkeys & Page Wiring
apps/desktop/src/shared/hotkeys.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
Added NEW_CHAT hotkey (meta+shift+t), reassigned REOPEN_TAB to meta+shift+r, wired NEW_CHAT to addChatTab(workspaceId) and simplified REOPEN_TAB handler.
Removed Shortcut Helper
apps/desktop/src/renderer/screens/.../PresetMenuItemShortcut/*
Removed PresetMenuItemShortcut component and its re-export; related shortcut rendering replaced or removed.
UI Styling
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
Changed background classes from bg-background to translucent variants (bg-muted/45 with dark:bg-muted/35).
Defaults & Constants
apps/desktop/src/shared/constants.ts
DEFAULT_SHOW_PRESETS_BAR changed from false to true.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant UI as Renderer UI
  participant TRPC as Electron TRPC
  participant DB as LocalDB

  User->>UI: Open Presets dropdown & toggle pin
  UI->>TRPC: mutate updateTerminalPreset(presetId, { pinnedToBar: true/false })
  TRPC->>DB: write terminal_preset { id, pinnedToBar }
  DB-->>TRPC: success
  TRPC-->>UI: mutation result
  UI->>UI: optimistic update reconciled (pinnedPresets recalculated)
  UI-->>User: updated pinned state shown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hop on keys and pin a shining bar,
Dropdowns bloom like clover near and far,
Hotkeys tap a rhythm, tabs slide in a row,
Presets scoped and pinned — a tidy burrow,
A rabbit grins at UI's new star!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: presets bar and group strip UX improvements with polish.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, detailed changes, data model updates, defaults/styling, and test plan with checked-off tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch default-open-presets

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AviPeltz AviPeltz changed the title Desktop presets bar + group strip UX polish feature (desktop): presets bar + group strip UX polish Feb 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx (1)

128-257: ⚠️ Potential issue | 🟠 Major

Confirm parent component conditionally mounts entire PresetsBar based on showPresetsBar.

In ContentView/index.tsx line 21, the entire PresetsBar component is conditionally mounted ({showPresetsBar && <PresetsBar />}). When showPresetsBar is false, the cog button used to re-enable it becomes inaccessible. While users can toggle this setting from GroupStrip, the pinned presets section should either:

  1. Always render the cog button (via the parent, before conditional mount), or
  2. Never conditionally unmount the component and instead hide only the pinned presets area.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 128 - 257, The PresetsBar cog button becomes inaccessible when
ContentView conditionally unmounts the entire <PresetsBar /> based on
showPresetsBar; either move the cog trigger out of that conditional into the
parent so it always renders, or change ContentView to always mount PresetsBar
and let PresetsBar itself hide only the pinnedPresets area when showPresetsBar
is false. Concretely: in ContentView/index.tsx stop using "{showPresetsBar &&
<PresetsBar />}" and instead always render "<PresetsBar
showPresetsBar={showPresetsBar} />" (or render the DropdownMenu/cog button JSX
before the conditional), then inside PresetsBar use the showPresetsBar prop to
conditionally render the pinnedPresets mapping while keeping the cog
DropdownMenu (DropdownMenuTrigger/DropdownMenuContent) always mounted so users
can re-enable the bar.
🧹 Nitpick comments (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx (3)

94-100: Implicit pin-by-default for legacy presets is a deliberate choice — worth a brief comment.

pinnedToBar === false is the only "unpinned" state; undefined (all legacy/existing presets) is treated as pinned. This ensures backward compatibility but may surprise future readers. A one-line comment here would help.

📝 Suggested clarification comment
 	const pinnedPresets = useMemo(
 		() =>
-			presets.flatMap((preset, index) =>
-				preset.pinnedToBar === false ? [] : [{ preset, index }],
+			// Legacy presets without pinnedToBar are treated as pinned (backward compat)
+			presets.flatMap((preset, index) =>
+				preset.pinnedToBar === false ? [] : [{ preset, index }],
 			),
 		[presets],
 	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 94 - 100, The current useMemo that builds pinnedPresets treats any
preset where pinnedToBar is undefined as pinned (only pinnedToBar === false is
unpinned), which is intentional for backward compatibility but not documented;
add a one-line clarifying comment above the useMemo (referencing pinnedPresets,
presets, useMemo and the pinnedToBar property) stating that legacy presets
default to pinned unless explicitly set to false to avoid surprising future
readers.

55-61: PresetShortcut is a separate component in the same file.

Per coding guidelines, each component should live in its own file. Since this is a tiny, tightly-coupled helper, extracting it is optional — but worth noting for guideline compliance.

As per coding guidelines, "No multi-component files - use one component per file."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 55 - 61, The file contains a secondary component PresetShortcut
(using useHotkeyText and rendering DropdownMenuShortcut) inside PresetsBar;
extract PresetShortcut into its own file (e.g., PresetShortcut.tsx) as a
named/default exported React component, move its dependencies (useHotkeyText
import and any related types like HotkeyId), update the original PresetsBar to
import PresetShortcut where it was used, and run typechecks to ensure HotkeyId
and DropdownMenuShortcut imports remain correct; keep the component behavior
identical (return null when useHotkeyText === "Unassigned").

33-53: Consider co-locating PresetTemplate interface and QUICK_ADD_PRESET_TEMPLATES constant.

These are only consumed by PresetsBar. If they grow or get reused, extracting them to a sibling file (e.g., PresetsBar/preset-templates.ts) would keep the component file focused. Fine for now. As per coding guidelines, "Co-locate hooks, utils, constants, config, stories, and dependencies next to the files using them."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 33 - 53, The PresetTemplate interface and
QUICK_ADD_PRESET_TEMPLATES constant are declared inside PresetsBar but are only
consumed by that component; move them to a new sibling module (e.g.,
PresetTemplates or preset-templates) to co-locate constants and keep the
component focused, export both PresetTemplate and QUICK_ADD_PRESET_TEMPLATES
from the new file, then replace the inline definitions in PresetsBar with an
import of those symbols and update any references to PresetTemplate and
QUICK_ADD_PRESET_TEMPLATES accordingly.
apps/desktop/src/lib/trpc/routers/settings/index.ts (1)

56-88: Consider setting pinnedToBar: true explicitly on default presets.

Default presets omit pinnedToBar, relying on the UI treating undefined as pinned. While this works, explicitly marking them as pinned would make the intent clear and prevent surprises if the default-pin logic ever changes.

📝 Example for one entry
 const DEFAULT_PRESETS: Omit<TerminalPreset, "id">[] = [
 	{
 		name: "claude",
 		description: "Danger mode: All permissions auto-approved",
 		cwd: "",
 		commands: ["claude --dangerously-skip-permissions"],
+		pinnedToBar: true,
 	},
 	// ... same for others
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/settings/index.ts` around lines 56 - 88,
DEFAULT_PRESETS relies on implicit UI behavior for pinning; explicitly add
pinnedToBar: true to each preset object in the DEFAULT_PRESETS array (the
constant named DEFAULT_PRESETS of type Omit<TerminalPreset, "id">) so the intent
is clear and resistant to future UI changes—update every entry (e.g., "claude",
"codex", "copilot", "opencode", "gemini") to include pinnedToBar: true and
ensure the TerminalPreset shape accepts this field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 90-93: presetByName currently creates a Map keyed by preset.name
which lets later presets with duplicate names silently shadow earlier ones;
update the memo to key by a unique identifier (e.g., preset.id) or build a
multi-value map (Map<string, Preset[]>), and then update any consumers such as
managedPresets to look up by id or handle multiple matches accordingly (refer to
useMemo => presetByName, the presets array, and the managedPresets logic) so
duplicate names no longer hide earlier presets.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`:
- Around line 65-68: The overflow flag (hasHorizontalOverflow) is oscillating
because track.scrollWidth includes the plusControl when it conditionally moves
in/out; fix by measuring tabs-only width and subtracting a fixed reserved width
for the plus-control before comparing to container width. Update the logic that
reads scrollContainerRef and tabsTrackRef (where you currently compute
scrollWidth/containerWidth and call setHasHorizontalOverflow) to instead compute
tabsOnlyWidth = tabsTrackRef.querySelector(...) or measure children excluding
the plusControl, then compare tabsOnlyWidth + RESERVED_PLUS_WIDTH to
scrollContainerRef.clientWidth; keep RESERVED_PLUS_WIDTH consistent (e.g., match
the plusControl CSS width) and use that in the hasHorizontalOverflow calculation
so setHasHorizontalOverflow stops flipping when the plusControl moves.
- Around line 38-46: The MenuItemShortcut component should be moved into its own
file: create a new folder MenuItemShortcut with MenuItemShortcut.tsx exporting
the MenuItemShortcut component (using the existing implementation that
references HotkeyId, useHotkeyText and DropdownMenuShortcut) and add a barrel
index.ts that re-exports it; then replace the inline definition in
GroupStrip.tsx with an import of MenuItemShortcut from that barrel. Ensure prop
typing (HotkeyId) and the conditional return (null when hotkeyText ===
"Unassigned") are preserved exactly.
- Around line 203-209: The icon-only Button in GroupStrip.tsx (the <Button>
wrapping the <LuPlus /> icon) lacks an accessible label; update the Button
component to provide an explicit accessible name (for example via an aria-label
or title prop such as aria-label="Add group" or similar) or include visually
hidden text alongside the LuPlus icon so screen readers get context; locate the
Button that contains LuPlus and add the label to that element.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 128-257: The PresetsBar cog button becomes inaccessible when
ContentView conditionally unmounts the entire <PresetsBar /> based on
showPresetsBar; either move the cog trigger out of that conditional into the
parent so it always renders, or change ContentView to always mount PresetsBar
and let PresetsBar itself hide only the pinnedPresets area when showPresetsBar
is false. Concretely: in ContentView/index.tsx stop using "{showPresetsBar &&
<PresetsBar />}" and instead always render "<PresetsBar
showPresetsBar={showPresetsBar} />" (or render the DropdownMenu/cog button JSX
before the conditional), then inside PresetsBar use the showPresetsBar prop to
conditionally render the pinnedPresets mapping while keeping the cog
DropdownMenu (DropdownMenuTrigger/DropdownMenuContent) always mounted so users
can re-enable the bar.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/settings/index.ts`:
- Around line 56-88: DEFAULT_PRESETS relies on implicit UI behavior for pinning;
explicitly add pinnedToBar: true to each preset object in the DEFAULT_PRESETS
array (the constant named DEFAULT_PRESETS of type Omit<TerminalPreset, "id">) so
the intent is clear and resistant to future UI changes—update every entry (e.g.,
"claude", "codex", "copilot", "opencode", "gemini") to include pinnedToBar: true
and ensure the TerminalPreset shape accepts this field.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 94-100: The current useMemo that builds pinnedPresets treats any
preset where pinnedToBar is undefined as pinned (only pinnedToBar === false is
unpinned), which is intentional for backward compatibility but not documented;
add a one-line clarifying comment above the useMemo (referencing pinnedPresets,
presets, useMemo and the pinnedToBar property) stating that legacy presets
default to pinned unless explicitly set to false to avoid surprising future
readers.
- Around line 55-61: The file contains a secondary component PresetShortcut
(using useHotkeyText and rendering DropdownMenuShortcut) inside PresetsBar;
extract PresetShortcut into its own file (e.g., PresetShortcut.tsx) as a
named/default exported React component, move its dependencies (useHotkeyText
import and any related types like HotkeyId), update the original PresetsBar to
import PresetShortcut where it was used, and run typechecks to ensure HotkeyId
and DropdownMenuShortcut imports remain correct; keep the component behavior
identical (return null when useHotkeyText === "Unassigned").
- Around line 33-53: The PresetTemplate interface and QUICK_ADD_PRESET_TEMPLATES
constant are declared inside PresetsBar but are only consumed by that component;
move them to a new sibling module (e.g., PresetTemplates or preset-templates) to
co-locate constants and keep the component focused, export both PresetTemplate
and QUICK_ADD_PRESET_TEMPLATES from the new file, then replace the inline
definitions in PresetsBar with an import of those symbols and update any
references to PresetTemplate and QUICK_ADD_PRESET_TEMPLATES accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b7f881 and be34c47.

📒 Files selected for processing (7)
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx
  • apps/desktop/src/shared/constants.ts
  • packages/local-db/src/schema/zod.ts

Comment on lines 65 to +68
const hasAiChat = useFeatureFlagEnabled(FEATURE_FLAGS.AI_CHAT);
const { presets } = usePresets();
const isDark = useIsDarkTheme();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const tabsTrackRef = useRef<HTMLDivElement>(null);
const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent overflow detection from oscillating near the threshold.

Because plusControl moves in/out of the scroll track (and the spacer width differs), track.scrollWidth can change across renders, making hasHorizontalOverflow flip repeatedly when tabs are near the boundary. That can cause flicker and extra layout work. Stabilize overflow detection by measuring tabs-only width and reserving a consistent plus-control width.

🔧 Suggested fix (stable overflow calculation)
 const scrollContainerRef = useRef<HTMLDivElement>(null);
 const tabsTrackRef = useRef<HTMLDivElement>(null);
+const tabsListRef = useRef<HTMLDivElement>(null);
+const plusControlRef = useRef<HTMLDivElement>(null);
 const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false);

 const updateOverflow = useCallback(() => {
   const container = scrollContainerRef.current;
-  const track = tabsTrackRef.current;
-  if (!container || !track) return;
-  setHasHorizontalOverflow(track.scrollWidth > container.clientWidth + 1);
+  const tabsList = tabsListRef.current;
+  const plusControlEl = plusControlRef.current;
+  if (!container) return;
+  const tabsWidth = tabsList?.scrollWidth ?? 0;
+  const reserved = plusControlEl?.offsetWidth ?? 0;
+  setHasHorizontalOverflow(tabsWidth + reserved > container.clientWidth + 1);
 }, []);

 ...
-        <div ref={tabsTrackRef} className="flex items-stretch">
+        <div ref={tabsTrackRef} className="flex items-stretch">
           {tabs.length > 0 && (
-            <div className="flex items-stretch h-full shrink-0">
+            <div ref={tabsListRef} className="flex items-stretch h-full shrink-0">
               {tabs.map(...)}
             </div>
           )}
           {hasHorizontalOverflow ? (
             <div className="h-full w-10 shrink-0" />
           ) : (
-            <div className="shrink-0">{plusControl}</div>
+            <div ref={plusControlRef} className="shrink-0">{plusControl}</div>
           )}
         </div>
 ...
-      {hasHorizontalOverflow && (
-        <div className="shrink-0 bg-background/95 pr-1">{plusControl}</div>
-      )}
+      {hasHorizontalOverflow && (
+        <div ref={plusControlRef} className="shrink-0 bg-background/95 pr-1">
+          {plusControl}
+        </div>
+      )}

Also applies to: 168-173, 252-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 65 - 68, The overflow flag (hasHorizontalOverflow) is oscillating
because track.scrollWidth includes the plusControl when it conditionally moves
in/out; fix by measuring tabs-only width and subtracting a fixed reserved width
for the plus-control before comparing to container width. Update the logic that
reads scrollContainerRef and tabsTrackRef (where you currently compute
scrollWidth/containerWidth and call setHasHorizontalOverflow) to instead compute
tabsOnlyWidth = tabsTrackRef.querySelector(...) or measure children excluding
the plusControl, then compare tabsOnlyWidth + RESERVED_PLUS_WIDTH to
scrollContainerRef.clientWidth; keep RESERVED_PLUS_WIDTH consistent (e.g., match
the plusControl CSS width) and use that in the hasHorizontalOverflow calculation
so setHasHorizontalOverflow stops flipping when the plusControl moves.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 24, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (2)

191-197: ⚠️ Potential issue | 🟡 Minor

Add an accessible label to the icon-only button.

Screen readers need a name for the “+” control.

🛠️ Suggested fix
 <Button
   variant="ghost"
   size="icon"
+  aria-label="Add tab"
   className="size-7 px-1 shrink-0 rounded-md border border-border/60 bg-muted/30 text-muted-foreground hover:bg-accent/60 hover:text-foreground"
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 191 - 197, The icon-only Button in GroupStrip.tsx (the Button
containing <LuPlus />) lacks an accessible name; update that Button (the one
with variant="ghost" and size="icon") to provide a clear accessible label by
adding an aria-label (e.g., aria-label="Add group") or by including visually
hidden text associated with the button so screen readers can announce its
purpose; ensure the label is concise and descriptive and avoid changing the
visual appearance.

156-173: ⚠️ Potential issue | 🟠 Major

Stabilize overflow detection to avoid flip-flopping.

track.scrollWidth changes when the plus control moves in/out of the track, which can cause hasHorizontalOverflow to oscillate near the threshold. Consider measuring tabs-only width and reserving a fixed plus-control width so the calculation is stable.

Also applies to: 265-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 156 - 173, The overflow toggle is unstable because
track.scrollWidth includes the plus-control when it moves; update the
updateOverflow logic to measure only the tabs area and subtract/ reserve a fixed
width for the plus-control instead of using raw track.scrollWidth: use
scrollContainerRef and tabsTrackRef to compute tabsOnlyWidth (e.g., sum child
widths or query a tabs-only element) and compare tabsOnlyWidth against
container.clientWidth minus a reservedPlusControlWidth constant, then
setHasHorizontalOverflow based on that stable threshold; apply the same change
to the other overflow detection instance that uses updateOverflow-like logic so
both places use the reserved plus-control width calculation.
🧹 Nitpick comments (1)
packages/local-db/src/schema/zod.ts (1)

59-70: Confirm the implicit "pinned by default" semantic for existing presets.

z.boolean().optional() makes pinnedToBar absent for all existing stored presets. Per the PR description the UI renders a preset whenever pinnedToBar !== false, so every legacy preset will appear in the bar immediately upon upgrade — no migration needed, but this is a behavioral side-effect worth an explicit acknowledgment in the schema comment (or a Zod .default(true) to make the intent self-documenting).

💡 Optional: make the default explicit
-	pinnedToBar: z.boolean().optional(),
+	/** When absent or true the preset is shown in the quick-launch bar; set to false to hide it. */
+	pinnedToBar: z.boolean().optional(),

Or, if a runtime default is desirable:

-	pinnedToBar: z.boolean().optional(),
+	pinnedToBar: z.boolean().default(true),

Note: switching to .default(true) changes the inferred type from boolean | undefined to boolean and means parse({…}) will always materialise the field — verify that the tRPC update mutation's pinnedToBar !== undefined guard still behaves correctly before applying.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/local-db/src/schema/zod.ts` around lines 59 - 70, The schema
terminalPresetSchema currently declares pinnedToBar as z.boolean().optional(),
which leaves legacy presets without the key and thus treated as "pinned by
default" by the UI; either add an explicit comment above terminalPresetSchema
noting this implicit semantic for existing records, or change pinnedToBar to
z.boolean().default(true) to make the default explicit at runtime—if you choose
.default(true) verify consumers (notably any tRPC update mutation that checks
pinnedToBar !== undefined) still behave correctly since the inferred type will
become boolean rather than boolean | undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`:
- Around line 191-197: The icon-only Button in GroupStrip.tsx (the Button
containing <LuPlus />) lacks an accessible name; update that Button (the one
with variant="ghost" and size="icon") to provide a clear accessible label by
adding an aria-label (e.g., aria-label="Add group") or by including visually
hidden text associated with the button so screen readers can announce its
purpose; ensure the label is concise and descriptive and avoid changing the
visual appearance.
- Around line 156-173: The overflow toggle is unstable because track.scrollWidth
includes the plus-control when it moves; update the updateOverflow logic to
measure only the tabs area and subtract/ reserve a fixed width for the
plus-control instead of using raw track.scrollWidth: use scrollContainerRef and
tabsTrackRef to compute tabsOnlyWidth (e.g., sum child widths or query a
tabs-only element) and compare tabsOnlyWidth against container.clientWidth minus
a reservedPlusControlWidth constant, then setHasHorizontalOverflow based on that
stable threshold; apply the same change to the other overflow detection instance
that uses updateOverflow-like logic so both places use the reserved plus-control
width calculation.

---

Nitpick comments:
In `@packages/local-db/src/schema/zod.ts`:
- Around line 59-70: The schema terminalPresetSchema currently declares
pinnedToBar as z.boolean().optional(), which leaves legacy presets without the
key and thus treated as "pinned by default" by the UI; either add an explicit
comment above terminalPresetSchema noting this implicit semantic for existing
records, or change pinnedToBar to z.boolean().default(true) to make the default
explicit at runtime—if you choose .default(true) verify consumers (notably any
tRPC update mutation that checks pinnedToBar !== undefined) still behave
correctly since the inferred type will become boolean rather than boolean |
undefined.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be34c47 and c8c94f6.

📒 Files selected for processing (10)
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx
  • apps/desktop/src/renderer/components/HotkeyMenuShortcut/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/PresetMenuItemShortcut.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx
  • apps/desktop/src/shared/hotkeys.ts
  • packages/local-db/src/schema/zod.ts
💤 Files with no reviewable changes (2)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/PresetMenuItemShortcut.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx
  • apps/desktop/src/lib/trpc/routers/settings/index.ts

# Conflicts:
#	apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (2)

229-235: [Unresolved from previous review] Icon-only button still missing an accessible label.

The <Button> wrapping <LuPlus /> has no aria-label, so assistive technology announces it as a generic "button" with no context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 229 - 235, The icon-only Button in GroupStrip (the <Button>
wrapping <LuPlus /> inside GroupStrip.tsx) lacks an accessible label; update
that Button to include an explicit aria-label (or aria-labelledby) with a clear,
concise description such as "Add group" (or similar context-appropriate text) so
assistive tech can announce its purpose; ensure you add the prop on the same
Button element that renders <LuPlus /> (or add visually hidden text associated
via aria-labelledby) rather than relying on title-only tooltips.

194-199: [Unresolved from previous review] Overflow detection oscillates when plusControl straddles the boundary.

track.scrollWidth at line 198 includes the plusControl when hasHorizontalOverflow is false. Once overflow is detected and plusControl is evicted from the track, scrollWidth decreases, potentially making the condition false again — causing a flip-flop on every measurement cycle.

The previous review already proposed a stable fix (separate tabsListRef / plusControlRef and measuring tabs-only width). That fix hasn't been applied yet.

Also applies to: 303-311

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 194 - 199, The overflow detection in updateOverflow currently uses
tabsTrackRef.scrollWidth which can include the plusControl and cause oscillation
when the plusControl straddles the boundary; update updateOverflow to measure
only the tabs area (not the plusControl) by adding/using a tabsListRef (or a
plusControlRef) and compute tabsWidth = tabsListRef.current.scrollWidth (or
track.scrollWidth - plusControlRef.current.offsetWidth if you prefer) then
setHasHorizontalOverflow(tabsWidth > scrollContainerRef.current.clientWidth +
1); update any other mirror checks (lines ~303-311) to the same tabs-only
measurement to stabilize behavior.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (1)

210-220: Minor redundancy: window resize listener and deferred requestAnimationFrame call can be removed.

  • Line 210: window.addEventListener("resize", updateOverflow) is redundant — ResizeObserver already observes container, which resizes with the window, so the window event never fires first.
  • Lines 218-220: updateOverflow is stable (empty useCallback deps), so this useEffect runs once on mount, scheduling a deferred duplicate of the synchronous updateOverflow() call already made inside useLayoutEffect on line 206. It's only useful to catch post-paint layout changes (e.g. font loading), which aren't a concern in Electron.
♻️ Suggested cleanup
 	useLayoutEffect(() => {
 		const container = scrollContainerRef.current;
 		const track = tabsTrackRef.current;
 		if (!container || !track) return;

 		updateOverflow();
 		const resizeObserver = new ResizeObserver(updateOverflow);
 		resizeObserver.observe(container);
 		resizeObserver.observe(track);
-		window.addEventListener("resize", updateOverflow);

 		return () => {
 			resizeObserver.disconnect();
-			window.removeEventListener("resize", updateOverflow);
 		};
 	}, [updateOverflow]);

-	useEffect(() => {
-		requestAnimationFrame(updateOverflow);
-	}, [updateOverflow]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`
around lines 210 - 220, Remove the redundant window resize listener and the
extra useEffect that calls requestAnimationFrame(updateOverflow): rely solely on
the existing ResizeObserver attached in the useLayoutEffect and call
updateOverflow there; delete the window.addEventListener("resize",
updateOverflow) registration and its matching removeEventListener in the
cleanup, and remove the separate useEffect that schedules
requestAnimationFrame(updateOverflow). Ensure the cleanup in the component still
disconnects the resizeObserver (resizeObserver.disconnect()) so only the
observer is cleaned up.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`:
- Around line 229-235: The icon-only Button in GroupStrip (the <Button> wrapping
<LuPlus /> inside GroupStrip.tsx) lacks an accessible label; update that Button
to include an explicit aria-label (or aria-labelledby) with a clear, concise
description such as "Add group" (or similar context-appropriate text) so
assistive tech can announce its purpose; ensure you add the prop on the same
Button element that renders <LuPlus /> (or add visually hidden text associated
via aria-labelledby) rather than relying on title-only tooltips.
- Around line 194-199: The overflow detection in updateOverflow currently uses
tabsTrackRef.scrollWidth which can include the plusControl and cause oscillation
when the plusControl straddles the boundary; update updateOverflow to measure
only the tabs area (not the plusControl) by adding/using a tabsListRef (or a
plusControlRef) and compute tabsWidth = tabsListRef.current.scrollWidth (or
track.scrollWidth - plusControlRef.current.offsetWidth if you prefer) then
setHasHorizontalOverflow(tabsWidth > scrollContainerRef.current.clientWidth +
1); update any other mirror checks (lines ~303-311) to the same tabs-only
measurement to stabilize behavior.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx`:
- Around line 210-220: Remove the redundant window resize listener and the extra
useEffect that calls requestAnimationFrame(updateOverflow): rely solely on the
existing ResizeObserver attached in the useLayoutEffect and call updateOverflow
there; delete the window.addEventListener("resize", updateOverflow) registration
and its matching removeEventListener in the cleanup, and remove the separate
useEffect that schedules requestAnimationFrame(updateOverflow). Ensure the
cleanup in the component still disconnects the resizeObserver
(resizeObserver.disconnect()) so only the observer is cleaned up.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8c94f6 and dd9585d.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx (1)

88-99: presetsByName duplicate-name fix confirmed.

The map now correctly accumulates all presets per name into Preset[] arrays, and managedPresets consumes only [0] for the template primary match. Duplicate-named presets surface as separate customExisting rows as intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 88 - 99, The Map value type is incorrect: change the declaration in
PresetsBar for presetsByName from Map<string, typeof presets> to Map<string,
Preset[]> (or Array<Preset>) and ensure the loop builds arrays of Preset objects
from the presets iterable; update any related type imports/aliases so presets is
typed as Preset[] and usages like managedPresets that access
presetsByName.get(name)?.[0] remain valid.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx (1)

177-218: Two optional UX improvements for managed preset rows.

  1. Dropdown stays open after checkbox toggle but closes after pin/unpin. DropdownMenuCheckboxItem (line 225) correctly uses onSelect={(e) => e.preventDefault()} so the menu stays open for successive checkbox changes. The same pattern would benefit preset rows — users pinning/unpinning multiple presets currently have to reopen the dropdown each time.

  2. disabled is broader than necessary. createPreset.isPending disables every row, including rows for existing presets that only call updatePreset.mutate. Consider tightening this per row type.

♻️ Proposed refactor
 <DropdownMenuItem
   key={item.key}
   className="gap-2"
-  disabled={createPreset.isPending}
+  disabled={hasPreset ? updatePreset.isPending : createPreset.isPending}
+  onSelect={(e) => {
+    if (hasPreset) e.preventDefault(); // keep open for multi-pin
+  }}
   onClick={() => {

Note: hasPreset must be defined before the JSX. Move the const hasPreset = !!item.preset declaration above the return.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 177 - 218, The preset menu rows close when clicked and are overly
disabled; before returning JSX move const hasPreset = !!item.preset above the
return, then change the DropdownMenuItem onClick to call e.preventDefault()
(like DropdownMenuCheckboxItem) so the menu stays open when pinning/unpinning
multiple items, and tighten the disabled logic so rows are disabled per-action
(e.g., disable creation rows when createPreset.isPending and disable
existing-preset rows when updatePreset.isPending) while keeping the existing
updatePreset.mutate and createPreset.mutate behavior that toggles pinnedToBar
for item.preset / item.template.preset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 153-155: The icon-only Button in PresetsBar (the Button containing
HiMiniCog6Tooth) is missing an accessible name; update the Button component in
PresetsBar (where HiMiniCog6Tooth is rendered) to include an accessible label
(e.g., aria-label="Settings" or aria-labelledby referencing a visible label) so
screen readers announce its purpose; ensure the label matches the Tooltip text
and use aria-hidden on the icon if your Button implementation needs it.

---

Duplicate comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 88-99: The Map value type is incorrect: change the declaration in
PresetsBar for presetsByName from Map<string, typeof presets> to Map<string,
Preset[]> (or Array<Preset>) and ensure the loop builds arrays of Preset objects
from the presets iterable; update any related type imports/aliases so presets is
typed as Preset[] and usages like managedPresets that access
presetsByName.get(name)?.[0] remain valid.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`:
- Around line 177-218: The preset menu rows close when clicked and are overly
disabled; before returning JSX move const hasPreset = !!item.preset above the
return, then change the DropdownMenuItem onClick to call e.preventDefault()
(like DropdownMenuCheckboxItem) so the menu stays open when pinning/unpinning
multiple items, and tighten the disabled logic so rows are disabled per-action
(e.g., disable creation rows when createPreset.isPending and disable
existing-preset rows when updatePreset.isPending) while keeping the existing
updatePreset.mutate and createPreset.mutate behavior that toggles pinnedToBar
for item.preset / item.template.preset.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd9585d and 6b93611.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx

Comment on lines +153 to +155
<Button variant="ghost" size="icon" className="size-6 shrink-0">
<HiMiniCog6Tooth className="size-3.5" />
</Button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Icon-only button is missing an accessible name.

The Tooltip provides a visual label but not an aria-label for screen readers. Without it, assistive technologies will announce this as an unlabeled button.

♿ Proposed fix
-<Button variant="ghost" size="icon" className="size-6 shrink-0">
+<Button variant="ghost" size="icon" className="size-6 shrink-0" aria-label="Manage Presets">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx`
around lines 153 - 155, The icon-only Button in PresetsBar (the Button
containing HiMiniCog6Tooth) is missing an accessible name; update the Button
component in PresetsBar (where HiMiniCog6Tooth is rendered) to include an
accessible label (e.g., aria-label="Settings" or aria-labelledby referencing a
visible label) so screen readers announce its purpose; ensure the label matches
the Tooltip text and use aria-hidden on the icon if your Button implementation
needs it.

@AviPeltz AviPeltz merged commit fc6540f into main Feb 24, 2026
14 checks passed
@Kitenite Kitenite deleted the default-open-presets branch March 15, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant