Skip to content

refactor(staged): migrate UI to shadcn-svelte + Tailwind v4#765

Merged
matt2e merged 28 commits into
mainfrom
shadcn-boss-ui
Jun 5, 2026
Merged

refactor(staged): migrate UI to shadcn-svelte + Tailwind v4#765
matt2e merged 28 commits into
mainfrom
shadcn-boss-ui

Conversation

@matt2e

@matt2e matt2e commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the Staged app UI from its bespoke Svelte components to shadcn-svelte on Tailwind v4.

  • Adds the shadcn-svelte component library (src/lib/components/ui/*): alert, alert-dialog, badge, button, checkbox, context-menu, dialog, dropdown-menu, input, label, popover, select, separator, sonner, toggle, toggle-group, tooltip.
  • Rewrites feature components (branches, diff, projects, sessions, settings, timeline, notes, actions, layout) to consume the new primitives.
  • Removes the old hand-rolled shared components and menu system (AlertCard, ConfirmDialog, DropdownMenu, FormButton, FormInput, FormToggle, ToastHost, alerts, backdropDismiss, menu/*).
  • Introduces app.css with the design tokens and wires up components.json, Tailwind v4, and the updated Vite/TS config.

Follow-up fixes

A series of light/dark-mode polish commits on top of the migration:

  • Decouple app chrome mode from the diff theme and from the git-green accent; switch the primary highlight to blue.
  • Bridge the Tailwind text scale to Staged's 13px size tokens; restore button roundness/heights.
  • Light-mode surface and chrome tone fixes across dialogs, forms, the New Project flow, and floating surfaces.
  • Anchor the repo search dropdown to its input inside dialogs; stop dialog titles clipping descenders; stop the header tooltip auto-opening when dialogs open; reload the session when SessionModal is reopened.

Test plan

  • staged-ci, differ-ci, and the Rust checks pass on push.

matt2e and others added 25 commits June 2, 2026 16:26
Land the shadcn-svelte spike: adopt Tailwind v4, bridge design tokens to
the Staged theme, and migrate bespoke chrome to shadcn-svelte primitives.

- Add shadcn-svelte UI components (Button, Input, Select, Checkbox, Label,
  Badge, Dialog, AlertDialog, DropdownMenu, Popover, Alert, Tooltip,
  Context Menu, etc.)
- Replace native inputs, bespoke menus, modals, banners, and theme
  selector with shadcn equivalents; delete the bespoke chrome they replace
- Migrate toasts/alerts to svelte-sonner
- Sweep Button and Tooltip usage across settings, branches, projects,
  sidebar, timeline, sessions, diff, notes, and actions
- Consolidate icon imports onto @lucide/svelte and drop lucide-svelte

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The app chrome and diff viewer shared a single theme axis: whatever Shiki
syntax theme the user picked drove both the diff highlighting AND every
chrome CSS variable via createAdaptiveTheme(). Split this into two
independent axes.

App chrome — fixed light/dark mode:
- Add a persisted `mode: light | dark | system` preference (store key
  `app-mode`, default `system`). `system` resolves via
  `prefers-color-scheme` and re-applies on its `change` event.
- Add fixed DARK_BASE (laserwave-derived, matching the prior :root
  defaults) and LIGHT_BASE (github-light-derived) color sets.
- applyChromeTheme() now feeds those fixed bases into createAdaptiveTheme()
  instead of getTheme(), so chrome no longer derives from the diff theme.
  --theme-is-dark (read by the darkMode store) reflects the resolved mode.

Diff viewer — keeps its full set of selectable themes, scoped to the diff:
- Rename the `syntaxTheme` preference to `diffTheme` (store key
  `diff-theme`, migrating from the legacy `syntax-theme` key). The full
  SYNTAX_THEMES list and setSyntaxTheme() are retained.
- selectDiffTheme() no longer writes app chrome variables. It only
  re-highlights the diff (via a bumped version prop) and recomputes a
  diff-scoped var map from a second createAdaptiveTheme() run.
- DiffModal applies that var map onto the diff-viewer container element so
  the diff area fully re-themes (bg/text/tints) while surrounding chrome
  stays on the fixed mode, and passes the version as syntaxThemeVersion to
  trigger re-highlighting.

Settings: split the single "Theme" control into an "Appearance" toggle
group (Light / Dark / System) bound to `mode` and a "Diff theme" swatch
picker bound to `diffTheme`.

Deviation from the note's minimal sketch: because diff lines consume the
full chrome var set (--bg-primary, --text-primary, …), the diff container
receives the entire adaptive var map — not just the --diff-* subset — so a
dark diff theme renders correctly on light chrome and vice versa (option i,
true re-theming).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration translated the old hand-set footer button
radii (6px compact / 8px enlarged) into token-backed radius utilities
driven by a `--radius: 4px` base, silently roughly halving them (md=3.2px,
lg=4px). Since the shared Button derives its corner radius entirely from
the global `--radius` token, bump that base to 8px so the derived scale
(md≈6.4px, lg=8px) restores parity with the pre-migration values. This
uniformly softens all shadcn surfaces, which matches the intent.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn migration's LIGHT_BASE chrome documents itself as derived from
the github-light palette, and its bg/fg/comment values do match plain
`github-light` (#ffffff / #24292e). But its gitColors were sourced from a
different theme — `github-light-default` — whose accents are darker and
more muted, darkening the added green from #28a745 to #1a7f37 (and shifting
deleted/modified off the plain github-light values, including modified from
blue #2188ff to brown #9a6700).

Align gitColors with the github-light theme the base actually derives from
so the light-mode added/commit greens (and deleted/modified accents) match
the pre-migration appearance.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shared shadcn Button outline variant baked in shadow-xs, which
rendered an unwanted drop shadow on RHS footer buttons (e.g. branch
cards). Remove shadow-xs from the outline variant so these buttons sit
flat against their surfaces.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Light-mode dialogs, popovers, dropdown/context menus, select content, and
shadcn cards rendered on a dirty #ebebeb gray instead of a clean panel.

createAdaptiveTheme() in src/lib/theme.ts computes floating surfaces with
elevate(0.08), where elevate(amount) = adjust(primaryBg, dir * amount) and
dir = isDark ? 1 : -1. The algorithm is dark-oriented: it lightens surfaces
to make them read as lifted. In light mode dir = -1, so the full 0.08 step
darkened floating surfaces all the way to #ebebeb against the white page —
the shadcn primitives now read --bg-elevated for these surfaces, so the
darkening became visible as a regression.

Gate the light branch on isDark and halve the step: dark themes keep
elevate(0.08) (lighten), light themes use adjust(primaryBg, -0.04) for a
subtle ~#f5f5f5 off-white surface that reads as a distinct, lifted panel
without going gray. The ring-1 ring-foreground/10 + shadow-md the shadcn
primitives already apply carry the rest of the elevation. The hover line is
unchanged — a slight darken on hover is correct.

Verified with `pnpm check` (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration changed SessionModal from a conditionally
mounted component ({#if openSessionId}) into a persistently mounted one
controlled by an `open` prop (the shadcn Dialog pattern). Its initial
data load lived in onMount, which now fires exactly once when the parent
card mounts — at which point sessionId is still the empty-string
fallback (sessionId={openSessionId ?? ''}). getSession('') returns null,
so loadSession() sets error = 'Session not found', and because onMount
never runs again the modal is stuck on that error for every subsequent
open.

Move the session load out of onMount into a reactive $effect keyed on
`open` + `sessionId`, so the modal (re)loads whenever it is opened or the
target session changes, and stops polling when it closes. onMount now
only registers the search shortcut target. This restores correct
behaviour for the always-mounted callers (BranchCard, ProjectSection)
and is a no-op for the keyed {#each} caller (SessionLauncher), which
already remounts per session id.

Verified with `pnpm check` (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
…face

The chat messages area (.modal-content) in SessionModal inherited
--bg-primary while the composer/input area (.input-area) paints
--bg-chrome, leaving the two regions visually identical apart from the
input's top border.

Set the messages-area background to a 50/50 color-mix of the two
endpoints so it reads as a distinct surface that sits halfway toward the
composer:

  background: color-mix(in srgb, var(--bg-primary) 50%, var(--bg-chrome) 50%);

Using color-mix over the existing theme variables keeps the blend
adaptive across light/dark modes rather than hardcoding a hex. The
composer's --bg-chrome background is left untouched.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
… surface

The note content area (.modal-content in NoteModal.svelte) set no
background of its own, so it inherited the shadcn Dialog.Content surface
(bg-popover -> --bg-elevated). That left it visually identical to the
dialog's surrounding chrome.

Set the note content background to a 50/50 color-mix between
--bg-elevated (its current inherited surface) and --bg-chrome (the same
endpoint the chat messages area blends toward), so it reads as a distinct
surface that sits halfway toward the chrome:

  background: color-mix(in srgb, var(--bg-elevated) 50%, var(--bg-chrome) 50%);

Using color-mix over the theme variables keeps the blend adaptive across
light/dark modes, mirroring the chat dialog change (7b84b25). The dialog
header, footer/next-steps action bar, and input fields are left untouched.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn dialog and alert-dialog overlay primitives hardcoded a
bg-black/10 backdrop, ignoring the mode-aware --shadow-overlay token.
In light mode this rendered a near-white scrim (only 10% black over the
white page) instead of the pre-migration overlay.

Replace bg-black/10 with the token-backed bg-[var(--shadow-overlay)] in
both overlay primitives. --shadow-overlay already encodes the full rgba
with alpha (0.4 light / 0.6 dark), is live on the document root via
applyChromeTheme(), and has a static fallback in app.css, so the
arbitrary-value utility applies it directly with no opacity modifier.

This restores the pre-migration scrim in light mode while leaving dark
mode essentially unchanged. The supports-backdrop-filter:backdrop-blur-xs
class and the theme.ts token are untouched.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Opening the chat session (SessionModal) or note detail (NoteModal)
dialog auto-popped the tooltip on the first header button.

When a bits-ui Dialog opens it auto-focuses the first focusable
element inside the content — here that is the first header
Tooltip.Trigger button. bits-ui tooltips open on focus (not just
hover), so the focused trigger's tooltip appeared immediately and
unprompted every time the dialog opened.

Pass onOpenAutoFocus={(e) => e.preventDefault()} to both
Dialog.Content elements (the prop is spread through to
DialogPrimitive.Content) so the dialog no longer forces focus onto
the first header button on open, leaving its tooltip closed until
the user actually hovers or focuses it. Esc-to-close and focus
trapping (driven by bits-ui's document-level handlers) are
unaffected.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The prior color-mix (9ba88cb) blended --bg-elevated with --bg-chrome,
but those collapse to essentially the same color in light mode (~#f5f5f5),
so the 50/50 mix equaled the original surface and the tint was invisible;
the endpoints only diverged in dark mode.

Switch the second endpoint to --bg-primary, which differs from
--bg-elevated in both modes, so the content surface is now perceptibly
distinct from the dialog chrome in light (~#fafafa) and dark (~#2f2b36).

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The base dialog-title.svelte applied leading-none (line-height: 1), so
the title's line box equaled the font's em size exactly and descenders
(g, y, p, q, j) hung below it. On its own that's fine, but the
SessionModal, NoteModal, ActionOutputModal, and ImageViewerModal titles
add overflow-hidden for ellipsis truncation, which clipped everything
below the line box — including those descenders.

Change the base title's leading-none to leading-tight (line-height:
1.25) so descenders sit inside the line box and survive overflow-hidden.
This fixes all four truncating titles uniformly and is a negligible
layout shift for the non-truncating dialog titles (headers center their
content via flex).

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The main app background and project detail background both paint
--bg-chrome, which createAdaptiveTheme() derives by darkening the base
background via calculateChromeColors(). On a near-white light base
(luminance 1.0) the logFloor step darkened chrome to ~0.916 luminance,
a noticeably gray surface, because darkening away from white reads
heavier than the equivalent lightening does on a dark base.

Add a LIGHT_CHROME_CONTRAST_SCALE (0.6) applied to the chrome luminance
difference for light themes (bgLum >= 0.5), so the derived chrome sits
closer to the page background — a lighter gray. Deepest stays at 2x the
(now reduced) diff, so it lightens proportionally and the chrome/deepest
relationship is preserved. Dark themes are unaffected (scale = 1).

Because this changes the derivation in theme.ts rather than any single
component, every surface that reads --bg-chrome lightens uniformly.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The previous LIGHT_CHROME_CONTRAST_SCALE of 0.6 took chrome from
~0.916 luminance to ~0.949 — still a noticeable gray against the white
page. Halve the scale again to 0.3 (chrome luminance ~0.975) so the
main app and project chrome read as a much lighter, off-white surface
that sits clearly closer to the page background. Deepest still scales
proportionally (2x the reduced diff), and dark themes remain
unaffected.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The 0.3 scale overshot — chrome went too pale. Pull it most of the way
back to the original (scale = 1.0) while keeping a small lightening, by
setting LIGHT_CHROME_CONTRAST_SCALE = 0.85. The darker chrome / deepest
surfaces in the light theme are now nearly as dark as pre-change but
sit a touch lighter against the page.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The repo search dropdown (Add Repo / New Project) rendered far down and
to the right of the modal instead of attached to the repo field.

RepoSearchInput positioned the dropdown with position:fixed and viewport
coords from getBoundingClientRect(). The shadcn Dialog.Content applies
-translate-x-1/2 -translate-y-1/2, and a CSS transform on an ancestor
makes that ancestor the containing block for position:fixed
descendants. So inside the dialog the dropdown's viewport-space top/left
were re-interpreted relative to the centered, translated dialog box —
placing it far from the input.

Drop the JS-driven fixed positioning entirely. Make .repo-search-wrapper
position:relative and anchor .repo-dropdown with position:absolute;
top:calc(100% + 4px); left:0; right:0 so the dropdown naturally sits
flush under the search field and matches its width, independent of any
transformed ancestor. Removes updateDropdownPosition, dropdownStyle, and
the window resize handler; as a side benefit, the dropdown now scrolls
with the field instead of detaching.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration replaced the bespoke FormToggle (which
rendered full-width Local/Remote option cards) with ToggleGroup.Root +
ToggleGroup.Items. The form passed `class="flex flex-col gap-2"` on
the root, but the base ToggleGroup.Root class already bakes in
`w-fit flex-row items-center` — the explicit `flex flex-col` didn't
override the cross-axis `items-center` (so items no longer stretched),
and `w-fit` shrunk the container to its content's intrinsic width.
The two option cards collapsed to the icon+label width instead of
spanning the dialog.

Switch the root to the proper API: set `orientation="vertical"` to
activate the base's `data-vertical:flex-col data-vertical:items-stretch`,
and pass `w-full` to override the baked-in `w-fit`. The items already
have `w-full` and stretch naturally now.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Drop the 8px gap between the Local and Remote option cards in the
New Project form so they sit flush against each other.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Name and Repository fields (and the Subpath / PR-or-Branch fields
revealed after a repo is picked) rendered on the dialog's gray surface
instead of white in light mode.

The shadcn-svelte Input is bg-transparent (only dark:bg-input/30 paints a
background), and RepoSearchInput's .search-input wrapper set background:
transparent. So in the New Project dialog all these fields showed through
to Dialog.Content's surface — --popover → --bg-elevated (~#f5f5f5
off-white) — rather than reading as distinct white fields.

Paint the fields with the primary background:
- Add bg-background (--background → --bg-primary, white in light mode) to
  the shadcn Input class on the Name, PR-or-Branch placeholder, Subpath,
  and BranchPicker inputs. dark:bg-input/30 still wins under .dark, so dark
  mode is unchanged.
- Change RepoSearchInput's .search-input wrapper from transparent to
  var(--bg-primary) so the repo field matches.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Local/Remote location toggles in the New Project form set no
background for the unselected (data-[state=off]) state, so the option
cards showed through to the dialog's gray Dialog.Content surface
(--popover -> --bg-elevated) instead of the page surface. Only the
selected card painted a background (data-[state=on]:bg-foreground).

Add bg-background to the item class (and switch the hover from
bg-transparent to bg-background) so the unselected card reads as white
in light mode and the dark base surface in dark mode, matching the
sibling form fields. The selected state's data-[state=on]:bg-foreground
still overrides, so the active card stays black (light) / white (dark).

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
shadcn's Button (and other migrated primitives) size text with Tailwind's
rem-based text-* utilities, which the migration never re-anchored to the
app's 13px root. Since 1rem = 13px here, text-sm (0.875rem) rendered at
~11.4px and text-xs (0.75rem) at ~9.75px — smaller than the pre-shadcn UI,
which sized text off the Staged --size-* tokens (e.g. FormButton's
var(--size-sm) ≈ 12px). The hand-added text-xs overrides on the branch-card
Diff/PR buttons and the note "Start note"/"Start commit" buttons compounded
this to ~19% small.

Option B (systemic): map the Tailwind text-* namespace to the Staged
--size-* tokens in the @theme inline block — text-xs→--size-xs,
text-sm→--size-sm, text-base→--size-md, text-lg→--size-lg, text-xl→--size-xl.
This realigns every migrated text-* utility to the old px scale in one place,
fixing the root cause rather than patching individual buttons. Line-heights
keep Tailwind's paired defaults.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…light to blue

The generic app accent (--ui-accent -> shadcn --primary) was sourced from
the git "added" color (gitColors.added), so primary buttons, focus rings,
active filter chips, running/cloud indicators, and status pills all rendered
in the same green that marks added files and commit dots. The two roles —
git-added semantic vs. generic UI highlight — shared one color and couldn't
be recolored independently.

Introduce a dedicated accentPrimary in createAdaptiveTheme(), sourced from
fallbackBlue (#58a6ff dark / #0969da light) so it stays blue in both modes —
unlike accentBlue (gitColors.modified), which is gold/orange in dark mode and
thus unsuitable as the accent. Point ui.accent / ui.accentHover at
accentPrimary instead of accentGreen.

status.added, timeline.commitColor/commitBg, and diff.addedBg are left on
accentGreen, so git semantics stay correct (added = green, commits = green
dots) while every --ui-accent consumer flips to blue. --primary-foreground
(--bg-deepest) tracks the resolved mode and keeps adequate contrast on both
the light- and dark-mode blue.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The decoupled accent used fallbackBlue (#0969da in light mode), which read
noticeably darker and heavier than the #28a745 green accent it replaced.

Give accentPrimary its own blue pair instead of reusing fallbackBlue: light
mode is now the brighter #2188ff, sitting at roughly the same luminance/
energy as the old green, while dark mode keeps #58a6ff (already close to the
old #3fb950). fallbackBlue is left untouched so accentBlue's fallback path
and other consumers are unaffected.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The branch-card footer holds two button groups in one align-items:center
row: the left "New note / New commit / New code review" add buttons (in
BranchTimeline) and the right Diff / PR buttons (in BranchCard's
footerActions snippet + BranchCardPrButton).

Both groups already render at text-xs, so their text sizes matched. But
their heights did not: the RHS buttons use the shared Button's size="sm"
(a fixed h-8 = 32px), while the LHS add buttons set h-auto + py-1, making
their height content-driven (~26-27px from the text-xs line box plus 8px
vertical padding and the 1px dashed border). Side by side in the centered
footer row, the LHS dashed buttons rendered visibly shorter than the
Diff/PR buttons.

Replace the non-enlarged LHS class's `py-1 h-auto` with `h-8` on all three
add buttons so they pin to the same 32px height as the RHS size="sm"
buttons. px-2.5 and rounded-md already align with sm's padding/radius
(rounded-md => --radius-md = 6px = min(--radius-md, 10px)), and the
enlarged variant is untouched.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e matt2e requested review from baxen and wesbillman as code owners June 5, 2026 01:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62cf25ab3a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +970 to +972
<ActionOutputModal
open={actionOutputModal !== null}
executionId={actionOutputModal?.executionId ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mount the action output modal only when it has an execution

When no action output is open, this still mounts ActionOutputModal with executionId=''. That component's reactive effect does not check open, so every branch card immediately calls getActionOutputBuffer('') and installs output/status listeners for a closed modal; on pages with many branches this creates unnecessary backend calls/listeners and can surface spurious load errors. Please keep the previous conditional mount or make the modal's effect bail out unless open && executionId.

Useful? React with 👍 / 👎.

Comment on lines +1667 to +1669
<SessionModal
open={sessionMgr.openSessionId !== null}
sessionId={sessionMgr.openSessionId ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't let closed session modals capture search shortcuts

This unconditionally mounts a SessionModal for every branch even when openSessionId is null. SessionModal registers a global search shortcut target in onMount, and runSearchShortcut() always dispatches to the last registered target, so a hidden per-branch modal can consume Cmd/Ctrl+F and search-next/previous even when no session modal is open (or when a different modal is visible). Please either mount it only while open, as before, or register/unregister the search target based on the open prop.

Useful? React with 👍 / 👎.

matt2e and others added 2 commits June 5, 2026 11:32
…cons

The branch-card footer buttons rendered their resting label (and, for the
LHS add buttons, their icons) in de-emphasized colors, so they read dimmer
than the sibling Diff button — which uses the shared outline Button's
default full-contrast foreground.

- PR button (BranchCardPrButton): drop the baked-in text-muted-foreground
  so its label inherits text-foreground like the Diff button. The
  state-specific overrides (destructive on error, status-added icon on
  MERGED) are untouched.
- LHS add buttons (New note / New commit / New code review in
  BranchTimeline): drop the resting text-[var(--text-muted)] from both the
  enlarged and compact class branches, and drop the always-on
  [&_svg]:!text-[var(--note|commit|review-color)] icon tint so the icons
  follow the label's foreground color at rest instead of painting their
  semantic accent. The hover states (border/bg/text shifting to the
  note/commit/review accent) and the [&_svg] transitions are kept, so the
  buttons still light up to their accent on hover — they just sit at full
  contrast at rest.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration left ActionOutputModal mounted persistently —
BranchCardActionsBar renders it with `open={actionOutputModal !== null}`
and `executionId={actionOutputModal?.executionId ?? ''}` rather than
conditionally mounting it. But the modal's load/listen $effect keyed off
executionId alone, so every branch card's closed instance immediately
called getActionOutputBuffer('') and installed output/status listeners for
a closed modal. On pages with many branches that is a burst of needless
backend calls and listeners, and the empty-id fetch surfaces a spurious
"not found" load error.

Guard the effect: bail out (running cleanup) unless `open && executionId`,
so a closed or execution-less instance does no work. Reading `open` also
subscribes the effect to it, so the load/listen path runs exactly when the
modal opens onto a real execution and tears down when it closes.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… open

SessionModal is mounted persistently for every branch card (the `open`
prop toggles visibility, per the shadcn Dialog pattern), and it registered
its global search-shortcut target in onMount. Since runSearchShortcut()
always dispatches to the last-registered target, every closed, off-screen
per-branch modal also held a target — so a hidden modal could capture
Cmd/Ctrl+F and search-next/previous even when no session modal was open, or
when a different modal was visible.

Move the registration out of onMount into a $effect gated on `open`: the
target is registered when the modal opens and torn down (via the effect's
cleanup) when it closes, so only a visible modal can claim the search
shortcut. onMount is no longer used and is dropped from the import;
onDestroy still unregisters as a final safety.

Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e matt2e merged commit 6e9f6f8 into main Jun 5, 2026
7 checks passed
@matt2e matt2e deleted the shadcn-boss-ui branch June 5, 2026 03:12
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