feat(web): unify workspace navigation#7153

merged4
main
agent/ui-workspace-refresh
openedAug 16, 2026byMmaria
View on GitHubTwo-way sync enabledPR last imported 4d ago

What changed

  • Adds one shared workspace header for chat, empty states, Settings, project settings, and onboarding.
  • Moves Settings, Usage, and Pull Requests navigation into the sidebar footer, with one consistent Back action on those pages.
  • Centralizes Settings content widths and scroll padding.

Screenshots

Direct parent on the left; this PR on the right. Same viewport and copied application state.

Settings layout

BeforeAfter
Settings layout before Settings layout after

Why

Workspace navigation and page headers were duplicated across several routes. This PR keeps only that shared layout work. Usage, Pull Requests, terminal, composer, tool lifecycle, and tool activity stay in separate reviews.

The independent thread action menu redesign remains in #7476.

Validation

  • Focused workspace header and Settings layout tests
  • Web typecheck
  • Browser verification of the Settings header, wide content layout, and shared Back footer

Stack order

  1. #7153 workspace navigation, this PR
  2. #7147 Usage
  3. #7148 Pull Requests
  4. #7149 terminal
  5. #7150 composer
  6. #7151 tool lifecycle projection
  7. #7152 tool activity UI

Built with GPT-5.6-sol in the Codex harness.


Note

Low Risk Mostly presentational refactors and shared navigation wiring; no auth, data, or API behavior changes in the diff.

Overview Unifies workspace top-bar chrome by adding WorkspacePageHeader and swapping duplicated <header> / Electron title-bar markup in chat (ChatView), empty thread state, settings layout, project settings, and hosted onboarding for that shared wrapper (electron, reserveNativeControls, collapsed-sidebar inset, safe-area padding).

Consolidates sidebar footer navigation by extracting SidebarUtilityMenu from SidebarChromeFooter (Settings / Usage / Pull Requests icon shortcuts, Back when on settings, usage, or pull-requests routes, with history.back when possible). Settings sidebar footer now reuses the same menu instead of its own Back handler.

Tweaks settings content layout via SettingsPageContainer width presets (readable / wide / expanded) and updated scroll-area padding; Keybindings and Diagnostics opt into wider max widths. Titlebar geometry expectations move to WorkspacePageHeader in the chat index titlebar test.

Reviewed by Cursor Bugbot for commit f756913ba6dadcd79cca53c1e244787e985616ce. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Unify workspace navigation headers into a shared WorkspacePageHeader component

  • Introduces WorkspacePageHeader.tsx, a reusable header that centralizes top-bar geometry, drag-region behavior, and native-control inset padding for Electron.
  • Replaces duplicated inline <header> markup and COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS logic across chat, onboarding, settings, and project settings views.
  • Centralizes sidebar footer utilities into a new SidebarUtilityMenu component in SidebarChrome.tsx; the Back button now prefers history.back() over navigating to /.
  • Standardizes SettingsPageContainer with a width prop ('readable' | 'wide' | 'expanded') and consistent padding, replacing per-page max-w-* classNames.

Macroscope summarized f756913.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d6074b3-0eb4-4b5e-a908-0906b9374f98

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review

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.

apps/web/src/components/Sidebar.tsxline 3312Outdated Β· b53b569Open thread
……@@ -3301,17 +3294,25 @@ export default function Sidebar() {
33143309 if (isMobile) setOpenMobile(false);
3310+ if (scopedProjectGroup) {
3311+ void newThreadContext.handleNewThread(
3312+ scopeProjectRef(scopedProjectGroup.environmentId, scopedProjectGroup.id),
MmacroscopeappBoton line 3312Aug 16, 2026

🟑 Medium components/Sidebar.tsx:3312

When a scoped logical project has members in multiple environments, this branch always creates the thread with scopedProjectGroup.environmentId and scopedProjectGroup.id, so a thread or draft opened in another member environment is silently created in the representative environment instead of the current context. Resolve the contextual member project ref (as the command-palette path does) before falling back to the group's representative ref.

βœ… Resolved in 93043b7f1baa338de70d66ee6e053a9dc032c1c2

Synced from GitHub

Thread transfer impact

βœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire12.6 KiB12.6 KiB+11 B (+0.1%)15.1 KiBβœ…
CodexThread snapshot wire6.3 KiB6.3 KiB+4 B (+0.1%)7.3 KiBβœ…
CodexLive turn WebSocket wire6.3 KiB6.3 KiB+7 B (+0.1%)7.8 KiBβœ…
CodexLive turn WebSocket decoded51.8 KiB51.8 KiB0 B (0.0%)66.4 KiBβœ…
CodexLive turn messages16160 (0.0%)21βœ…
ClaudeTotal thread wire12.6 KiB12.6 KiBβˆ’15 B (βˆ’0.1%)15.1 KiBβœ…
ClaudeThread snapshot wire6.3 KiB6.3 KiBβˆ’4 B (βˆ’0.1%)7.3 KiBβœ…
ClaudeLive turn WebSocket wire6.3 KiB6.3 KiBβˆ’11 B (βˆ’0.2%)7.8 KiBβœ…
ClaudeLive turn WebSocket decoded52.7 KiB52.7 KiB0 B (0.0%)66.4 KiBβœ…
ClaudeLive turn messages16160 (0.0%)21βœ…

Baseline: 36f4314 Β· PR result: f756913 Β· Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 99.9 KiB
  • Claude decoded thread snapshot: 100.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

MmacroscopeappBotreviewed Β· 3 inline threadsAug 16, 2026
Synced from GitHub

UI Consistency

Reviewed the changed web UI surfaces (shared WorkspacePageHeader/WorkspacePageContainer extraction, sidebar row/footer rework, settings layout widths, toggle/segmented variants, context-menu fallback icons, composer chip metrics, themed toolbar variable swap).

Verified as safe:

  • WorkspacePageHeader reproduces the previous per-surface geometry: --workspace-topbar-height resolves to 52px and to env(titlebar-area-height) under .wco, drag-region is still gated on Electron, COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS is retained, data-chat-header still forwards (so the themed toolbar rules keep applying), and className is merged last so call sites keep ownership. Passing reserveNativeControls in the browser is inert because wco: only matches Electron's .wco root.
  • --toolbar-control-foreground is defined in both the default token block and the html[data-theme-id] bridge, so the index.css swap does not drop a variable.
  • SettingsPageContainer keeps the fade/padding pairing: [--topbar-scroll-fade-height:1.5rem] sm:[...] is required to neutralise the utility's @variant sm bump, and pt-6 matches the 1.5rem band, same as the pull-requests page.
  • separatorBefore is honoured by both renderers of ContextMenuItem (Electron template + showContextMenuFallback), and the new copy submenu parent id is never dispatched.

Three consistency findings below; no visual or behavioural regression was identified.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/ui/segmented-tabs.tsxlines 19–40Outdated Β· b53b569Open thread
……@@ -0,0 +1,40 @@
37+ );
38+}
39+
40+export { SegmentedTab, SegmentedTabList };
MmacroscopeappBoton lines 19–40Aug 16, 2026

SegmentedTab/SegmentedTabList have no call sites anywhere in the repo, and the segmented / segmented-compact toggle size+variant added in toggle.tsx are referenced only from this file. A shared primitive landed ahead of its first consumer can't have its contract validated against real usage (density naming, role="group" rather than tab semantics, pressed/hover tones), and it becomes another variant future call sites may or may not pick up.

Suggest landing this together with the screen that needs it, or dropping it from this PR.

Posted via Macroscope β€” UI Consistency

Removed from this PR. The segmented control now lives with its first consumer in #7147.

apps/web/src/components/WorkspacePageContainer.tsxlines 56–61Outdated Β· b53b569Open thread
……@@ -0,0 +1,62 @@
58+ className,
59+ ...props
60+}: ComponentPropsWithoutRef<"div">) {
61+ return <div className={cn("-me-[7px] flex shrink-0", className)} {...props} />;
MmacroscopeappBoton lines 56–61Aug 16, 2026

components/WorkspacePageContainer.tsx:61

WorkspacePageHeaderEdgeControl is exported but not used by any header in this PR. The -me-[7px] offset is only meaningful relative to a concrete control's hit-target padding, so without a consumer there is nothing pinning that value to a real edge alignment.

Suggest landing this helper with its first call site so the offset is verifiable.

No longer relevant as of f18175796d90e63405862a4c2e246ec6b9f343c1

apps/web/src/components/Sidebar.tsxlines 1477–1481Outdated Β· b53b569Open thread
……@@ -1370,130 +1365,128 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1478+ type="button"
1479+ aria-label="Settle thread"
1480+ onClick={handleSettleClick}
1481+ className="-mr-1 inline-flex h-full cursor-pointer items-center gap-1 rounded-md bg-transparent ps-1 pe-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
MmacroscopeappBoton lines 1477–1481Aug 16, 2026

components/Sidebar.tsx:1481

This hover action now hand-rolls exactly what Button variant="ghost-muted" already provides (text-muted-foreground with hover:bg-accent hover:text-foreground, rounded-md, cursor-pointer) β€” the hover:bg-accent added here is the primitive's own hover treatment β€” while the Snooze and Unpin controls in the same slot were moved onto that primitive in this PR. Leaving one of the three siblings hand-rolled means hover/focus treatment for the row's actions now lives in two places and can drift.

Consider routing Settle through the same primitive; if the 20px slot needs a shorter size than xs, prefer adding that size to Button over keeping a bespoke class string here.

No longer relevant as of f18175796d90e63405862a4c2e246ec6b9f343c1

Synced from GitHub

Approvability

Verdict: Approved at f756913

Macroscope's review found this PR approvable β€” Mechanical refactor extracting duplicated header and sidebar menu styling into shared components (WorkspacePageHeader, SidebarUtilityMenu). No behavioral changes - same CSS classes applied from a centralized location. Tests updated accordingly.

You can add or adjust custom eligibility rules. Learn more.

CcursorBotreviewed Β· 1 inline threadAug 16, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f18175796d90e63405862a4c2e246ec6b9f343c1. Configure here.

apps/desktop/src/electron/ElectronMenu.tsline 155Outdated Β· f181757Open thread
……@@ -141,10 +142,17 @@ export const make = Effect.gen(function* () {
153+ }
146154 if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
147- template.push({ type: "separator" });
155+ appendSeparator();
CcursorBoton line 155Aug 16, 2026

Archive isolated from Delete on Electron

Low Severity

separatorBefore on archive and the Electron builder’s automatic separator before the first destructive item both fire, so native menus render Archive between two dividers. The web fallback only honors separatorBefore, so Archive stays next to Delete there.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit f18175796d90e63405862a4c2e246ec6b9f343c1. Configure here.

MmacroscopeappBotreviewed Β· 2 inline threadsAug 16, 2026
Synced from GitHub

Reviewed the changed web UI surfaces for shared-primitive, Tailwind-ownership and layout-preservation consistency. The header/container consolidation (WorkspacePageHeader/WorkspacePageContainer) preserves topbar height, WCO inset, drag region, safe-area padding and the collapsed-sidebar transition, the new micro button size correctly extends the primitive contract instead of hand-rolling the sidebar row actions, and the themed toolbar-control rule change is consistent with the existing [data-chat-header] control rule. Two smaller items below.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/PanelLayoutControls.tsxlines 45–45Outdated Β· f181757Open thread
……@@ -40,7 +42,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
4143 render={
4244 <Toggle
43- className="shrink-0 [-webkit-app-region:no-drag]"
45+ className="shrink-0 text-foreground [-webkit-app-region:no-drag]"
MmacroscopeappBoton lines 45–45Aug 16, 2026

chat/PanelLayoutControls.tsx:45

Toggle's base class and its ghost variant already apply text-foreground, and in themed chrome the [data-panel-layout-controls] / [data-workspace-titlebar-controls] rule updated in this PR sets color from --toolbar-control-foreground at higher specificity β€” so this call-site text-foreground can never win in either mode. It just adds a second, silent owner for the control tone that will drift from the primitive/theme rule (same on lines 68 and 119).

suggestion
1 className="shrink-0 [-webkit-app-region:no-drag]"

No longer relevant as of 27bb7f481e723c2eef312ca4fa5ba8c0c1394aae

apps/web/src/components/settings/SettingsSidebarNav.tsxlines 287–287Outdated Β· f181757Open thread
……@@ -296,14 +284,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
304- </SidebarMenuButton>
305- </SidebarMenuItem>
306- </SidebarMenu>
287+ <SidebarUtilityMenu />
MmacroscopeappBoton lines 287–287Aug 16, 2026

settings/SettingsSidebarNav.tsx:287

The local menu this replaces carried min-w-0 flex-1, but SidebarUtilityMenu renders SidebarMenu with only w-full (flex-basis auto). In this footer row (flex items-center gap-1 next to T3ConnectSidebarAvatar) a 100%-wide item overflows the row, so the shrink is now shared with the avatar instead of the menu absorbing the free space.

Suggest restoring the previous distribution (or adding a className passthrough on SidebarUtilityMenu and passing min-w-0 flex-1):

suggestion
1 <div className="min-w-0 flex-1">
2 <SidebarUtilityMenu />
3 </div>

No longer relevant as of 27bb7f481e723c2eef312ca4fa5ba8c0c1394aae

MmacroscopeappBotreviewed Β· 3 inline threadsAug 16, 2026
Synced from GitHub

UI consistency review found three items in the changed web UI scope. Details inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/ui/button.tsxlines 32–33Outdated Β· 27bb7f4Open thread
……@@ -29,6 +29,8 @@ const buttonVariants = cva(
3030 "size-7 sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
3131 lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
32+ micro:
33+ "h-5 gap-1 rounded-md px-[calc(--spacing(1.5)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3",
MmacroscopeappBoton lines 32–33Aug 16, 2026

ui/button.tsx:33

The new micro size keeps rounded-md (--radius-md, 8px β€” same as the base --control-radius), while its intended icon sibling icon-micro (size-5) uses rounded-sm (6px). This PR renders the two adjacent in the sidebar row hover slot (Snooze icon-micro next to Settle micro), so two 20px-tall controls in the same group end up with different corner radii.

Suggest matching icon-micro so the micro pair shares one radius contract:

suggestion
1 micro:
2 "h-5 gap-1 rounded-sm px-[calc(--spacing(1.5)-1px)] text-xs before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3",

No longer relevant as of e20c6800cde8857d00eb070bdfb1bfd4ba2e12d8

apps/web/src/components/chat/PanelLayoutControls.tsxlines 15–15Outdated Β· 27bb7f4Open thread
……@@ -12,6 +12,7 @@ interface PanelLayoutControlsProps {
1212 rightPanelAvailable: boolean;
1313 rightPanelOpen: boolean;
1414 rightPanelShortcutLabel: string | null;
15+ rightPanelUnavailableLabel?: string;
MmacroscopeappBoton lines 15–15Aug 16, 2026

rightPanelUnavailableLabel has no call site β€” the only references in the repo are this declaration, the default on line 30, and the read on line 99. A prop added to a shared control ahead of its first consumer can't have its contract validated (who overrides it, and with what copy), and it widens the component API without changing any current behavior.

Suggest landing this together with the surface that needs the alternate label, or keeping the literal until then.

Posted via Macroscope β€” UI Consistency

Removed from the focused PR. This component no longer adds the unused prop.

apps/web/src/components/Sidebar.tsxlines 1300–1326Outdated Β· 27bb7f4Open thread
MmacroscopeappBoton lines 1300–1326Aug 16, 2026

components/Sidebar.tsx:1300

The full row's Snooze / Unpin / Settle controls were migrated onto Button size="icon-micro"|"micro" variant="ghost-muted" in this PR, but the slim row's Wake / Un-settle / Settle buttons (lines 1266, 1279, 1291) still hand-roll the same treatment: inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground.

That is now a behavioral divergence, not just duplication: ghost-muted also supplies hover:bg-accent, so the same Settle action gets a filled hover affordance in the full row and a text-color-only hover in the slim row, and the row-action treatment has two owners in one file.

Suggest moving these three onto the same primitive and leaving only the positioning/visibility classes (pointer-events-none absolute inset-y-0 right-0 …) at the call site.

No longer relevant as of e20c6800cde8857d00eb070bdfb1bfd4ba2e12d8

apps/web/src/components/Sidebar.tsxlines 1391–1394Outdated Β· bc08174Open thread
……@@ -1382,136 +1384,138 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1391+ size="icon-micro"
1392+ variant="ghost-muted"
1393+ aria-label="Unpin thread"
1394+ title="Unpin thread"
MmacroscopeappBoton lines 1391–1394Aug 16, 2026

components/Sidebar.tsx:1394

This migration swapped the shared Tooltip/TooltipPopup for a native title on the Unpin control (and on the Snooze trigger at line 400). Those are the only two title attributes on interactive controls in apps/web/src β€” every other icon-only control, including SidebarUtilityItem added in this PR and the Woke button in this same slot (line 1432), keeps the primitive.

Besides the styling/delay difference, the row itself is a TooltipTrigger with detailsTooltip, so hovering these buttons now raises the browser's native tip and the row's styled tooltip instead of the button's own tooltip replacing it. Suggest keeping the primitive over the Button:

diff
1- <Button
2- size="icon-micro"
3- variant="ghost-muted"
4- aria-label="Unpin thread"
5- title="Unpin thread"
6- onClick={handleUnpinClick}
7- >
8- <PinIcon aria-hidden className={SIDEBAR_LIFECYCLE_ICON_CLASS} />
9- </Button>
10+ <Tooltip>
11+ <TooltipTrigger
12+ render={
13+ <Button
14+ size="icon-micro"
15+ variant="ghost-muted"
16+ aria-label="Unpin thread"
17+ onClick={handleUnpinClick}
18+ />
19+ }
20+ >
21+ <PinIcon aria-hidden className={SIDEBAR_LIFECYCLE_ICON_CLASS} />
22+ </TooltipTrigger>
23+ <TooltipPopup>Unpin thread</TooltipPopup>
24+ </Tooltip>

No longer relevant as of d8a25c983cbbe4ee4764448ae1664fe6f0a69393

apps/web/src/components/composerInlineChip.tslines 25–25Outdated Β· bc08174Open thread
……@@ -11,18 +11,18 @@ export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[
2323
2424export const COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME =
25- "inline-flex max-w-full select-none items-center gap-[0.33em] rounded-[0.5em] border border-fuchsia-500/25 bg-fuchsia-500/12 px-[0.5em] py-[0.08em] font-medium text-[0.86em] leading-[1.1] text-fuchsia-700 align-middle dark:text-fuchsia-300";
25+ "inline-flex h-[1.41em] max-w-full select-none items-center gap-[0.33em] rounded-[0.5em] border border-fuchsia-500/25 bg-fuchsia-500/12 px-[0.5em] font-medium text-[0.86em] leading-none text-fuchsia-700 align-middle dark:text-fuchsia-300";
MmacroscopeappBoton lines 25–25Aug 16, 2026

The skill chip repeats the base chip's geometry verbatim β€” inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] px-[0.5em] font-medium leading-none align-middle β€” so the new fixed h-[1.41em]/leading-none contract that replaced the removed py+top-[0.15em] offset now has two owners. Tuning one (the height is what keeps glyphs centered at the composer's user-configurable prompt size) silently misaligns the other, and these strings are raw-concatenated rather than merged through cn, so a call site cannot correct it.

Suggest hoisting the shared geometry into one constant (e.g. INLINE_CHIP_GEOMETRY_CLASS_NAME) that both INLINE_CHIP_CLASS_NAME and the skill chip build on, leaving only the fuchsia border/background/text tone here. Related: COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME (line 22) is now an exact alias of COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME; pointing its call sites at the base constant would drop the dead indirection.

Posted via Macroscope β€” UI Consistency

Removed from this PR while narrowing it to workspace navigation and page layout.

MmacroscopeappBotreviewed Β· 2 inline threadsAug 17, 2026
Synced from GitHub

Reviewed the UI changes for shared-component and Tailwind-ownership consistency. Two findings, both about the new shared contracts introduced here; the WorkspacePageHeader extraction, the Button size="micro"/icon-micro migration of the sidebar row actions, and the themed --toolbar-control-foreground alignment otherwise look correct and preserve focus-visible, coarse hit targets, drag regions and titlebar insets.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/WorkspacePageContainer.tsxlines 32–33Outdated Β· 8f1ea22Open thread
……@@ -0,0 +1,54 @@
30+}
31+
32+/** Shared top-bar geometry for every full-width workspace surface. */
33+export function WorkspacePageHeader({
MmacroscopeappBoton lines 32–33Aug 17, 2026

The doc comment claims this owns the top bar for "every full-width workspace surface", but apps/web/src/components/usage/UsagePage.tsx:107-131 still hand-rolls the exact {!isElectron && <header>} / {isElectron && <div>} twin this PR deletes from settings.tsx and ProjectSettingsPanel.tsx, with the pre-refactor strings (px-3 sm:px-5, h-[52px], wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]).

After this change the migrated surfaces use safe-area padding, --workspace-topbar-height, --workspace-native-controls-inset and gap-3, while Usage keeps the literal values β€” so its top bar sits at a different inset/height from every sibling surface, which is the drift the shared header exists to prevent.

Suggest migrating that header here too (<WorkspacePageHeader electron={isElectron}> around the single breadcrumb), or narrowing the doc comment to the surfaces actually covered. apps/web/src/routes/_chat.pull-requests.tsx:1841-1850 repeats the same geometry and could take reserveNativeControls={!rightPanelOpen}.

Posted via Macroscope β€” UI Consistency

The shared header scope was narrowed here. Usage and Pull Requests own their page-specific migrations in #7147 and #7148.

apps/web/src/contextMenuFallback.tslines 279–281Outdated Β· 8f1ea22Open thread
……@@ -200,6 +274,16 @@ export function showContextMenuFallback<T extends string>(
278+ const separator = document.createElement("div");
279+ separator.className = "my-1 h-px bg-border/70";
280+ separator.style.cssText =
281+ "height:1px;margin:0.25rem 0;background:var(--border);opacity:0.7;";
MmacroscopeappBoton lines 279–281Aug 17, 2026

src/contextMenuFallback.ts:281

This fallback menu mirrors the shared menu primitive elsewhere (row base copies MenuItem's rounded-sm px-2 py-1 min-h-8 sm:min-h-7, the popup copies MenuPopup's dropdown-glass … min-w-32 rounded-lg), but the new divider diverges from MenuSeparator (mx-2 my-1 h-px bg-border): it is lighter (bg-border/70 / opacity:0.7) and has no horizontal inset, so it runs 0.5rem wider than the item labels it separates while native/Base UI menus keep the divider aligned to the label column.

Suggest matching the shared separator contract:

suggestion
1 separator.className = "mx-2 my-1 h-px bg-border";
2 separator.style.cssText = "height:1px;margin:0.25rem 0.5rem;background:var(--border);";

No longer relevant as of d8a25c983cbbe4ee4764448ae1664fe6f0a69393

MmacroscopeappBotreviewed Β· 2 inline threadsAug 17, 2026
Synced from GitHub

Two icon-consistency findings in the changed context-menu code. Everything else in this revision looks consistent: the new WorkspacePageHeader reproduces the Electron/WCO geometry it replaces (--workspace-topbar-height resolves to env(titlebar-area-height, 52px) under .wco, and wco: selectors outrank the new sm: padding on specificity), micro now shares icon-micro's radius/icon contract, the sidebar hover actions all run through Button variant="ghost-muted", the fallback divider matches MenuSeparator, and every SettingsPageContainer caller now uses the width prop instead of a call-site max-w-*.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/threadActionMenu.logic.tslines 130–130Outdated Β· d8a25c9Open thread
……@@ -91,26 +94,48 @@ export function buildThreadActionMenuItems(
127+ {
128+ id: "archive",
129+ label: "Archive thread",
130+ icon: "folder",
MmacroscopeappBoton lines 130–130Aug 17, 2026

components/threadActionMenu.logic.ts:130

Archive thread takes the same folder glyph as the new Copy > Path child on line 115, so one menu tree now uses one icon for two unrelated actions β€” and the app already represents archived threads with lucide's ArchiveIcon (Settings > Archived threads, SettingsPanels.tsx:1), so the folder glyph also reads as a different concept here.

Suggest an archive icon name instead, adding the matching entry to ICON_PATHS in apps/web/src/contextMenuFallback.ts (the only renderer that draws these names):

ts
1 archive: [
2 { tag: "rect", attrs: { width: "20", height: "5", x: "2", y: "3", rx: "1" } },
3 { tag: "path", attrs: { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" } },
4 { tag: "path", attrs: { d: "M10 12h4" } },
5 ],

No longer relevant as of 904f059489d273e529dfae71a8c3e0211506d6e5

apps/web/src/contextMenuFallback.tslines 134–134Outdated Β· d8a25c9Open thread
……@@ -57,7 +131,7 @@ function createIconElement(name: string, tone: "neutral" | "destructive"): SVGSV
58132 svg.setAttribute(
59133 "class",
60- tone === "destructive" ? "size-3.5 shrink-0" : "size-3.5 shrink-0 text-muted-foreground",
134+ tone === "destructive" ? "size-4 shrink-0" : "size-4 shrink-0 text-muted-foreground",
MmacroscopeappBoton lines 134–134Aug 17, 2026

src/contextMenuFallback.ts:134

This fallback deliberately mirrors MenuItem's row metrics (min-h-8 text-base sm:min-h-7 sm:text-sm), but the new icon size is fixed at 16px while the primitive's icon contract is responsive ([&>svg:not([class*='size-'])]:size-4.5 sm:...:size-4). Below sm β€” the touch/mobile-web case where this fallback is the menu β€” its rows render 16px glyphs where a Base UI menu renders 18px, so the two menus drift on exactly the viewport range the row base is matching.

Same applies to the new chevron on line 335 (size-4 + text-muted-foreground/80), where MenuSubmenuTrigger renders -me-0.5 ms-auto opacity-80 at the same responsive size.

suggestion
1 tone === "destructive"
2 ? "size-4.5 shrink-0 sm:size-4"
3 : "size-4.5 shrink-0 text-muted-foreground sm:size-4",

No longer relevant as of 904f059489d273e529dfae71a8c3e0211506d6e5

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

One finding on the new fallback context-menu divider: it is the only renderer of the separatorBefore sections on web, and it currently has no coverage β€” the module's own test double cannot execute the new branch. Everything else in this revision (micro/ghost-muted migrations in the sidebar row, the shared WorkspacePageHeader/WorkspacePageContainer geometry, the --toolbar-control-foreground switch, and the fallback icon/chevron sizing) matched the shared primitives and theme contracts on review.

Posted via Macroscope β€” UI Consistency

apps/web/src/contextMenuFallback.tslines 284–284Outdated Β· ba1df53Open thread
……@@ -200,6 +281,15 @@ export function showContextMenuFallback<T extends string>(
200281 "max-height:min(24rem,70vh);min-width:0;max-width:24rem;overflow-x:hidden;overflow-y:auto;padding:0.25rem;";
201282
202283 for (const item of entries) {
284+ if (item.separatorBefore === true && inner.childElementCount > 0) {
MmacroscopeappBoton lines 284–284Aug 17, 2026

src/contextMenuFallback.ts:284

This divider is invisible to this module's own harness: FakeElement in contextMenuFallback.test.ts implements neither childElementCount (so the guard evaluates undefined > 0 and no divider is ever appended) nor setAttribute (so line 289 would throw the moment a test passes separatorBefore) β€” the same test-double gap that forced the createElementNS guard added just above. Net effect: the web fallback's section grouping ships untested, while the Electron template got two dedicated cases for the same contract.

Suggest using the array-compatible children.length here, then adding setAttribute to FakeElement plus a case asserting a divider lands between sections (a separatorBefore assertion in threadActionMenu.logic.test.ts would cover the item side cheaply too).

suggestion
1 if (item.separatorBefore === true && inner.children.length > 0) {

No longer relevant as of aa7c78cbb699367e3075b91585ccfc0431c820da

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one finding β€” a shared component's API is widened with a prop that has no consumer in this PR. Everything else in the changed web UI looks consistent with the shared component system (the fallback menu divider now matches MenuSeparator, the fallback icon sizes match MenuItem's responsive contract, the sidebar lifecycle actions are on Button size="icon-micro"|"micro" variant="ghost-muted", micro shares icon-micro's radius, and WorkspacePageHeader/WorkspacePageContainer preserve the WCO inset, safe-area padding, drag region and collapsed-sidebar inset of the headers they replace).

Posted via Macroscope β€” UI Consistency

apps/web/src/components/WorkspaceBreadcrumb.tsxlines 48–50Outdated Β· 883bf98Open thread
……@@ -45,9 +45,9 @@ export function WorkspaceBreadcrumbItem({
48+export function WorkspaceBreadcrumbSeparator({ className }: { readonly className?: string }) {
4949 return (
50- <li aria-hidden="true" className="flex shrink-0 items-center text-icon-muted">
50+ <li aria-hidden="true" className={cn("flex shrink-0 items-center text-icon-muted", className)}>
MmacroscopeappBoton lines 48–50Aug 17, 2026

className has no call site: all five WorkspaceBreadcrumbSeparator usages (ChatHeader.tsx:261, SettingsBreadcrumb.tsx:26, ProjectSettingsPanel.tsx:215, _chat.pull-requests.tsx:1861) render it bare. A pass-through added to a shared breadcrumb part ahead of its first consumer can't have its contract validated β€” which class is expected to be layered on, and whether it should be allowed to override the separator's shrink-0/text-icon-muted tone β€” and it widens the API without changing behavior.

Suggest landing this with the surface that needs the override.

suggestion
1export function WorkspaceBreadcrumbSeparator() {
2 return (
3 <li aria-hidden="true" className="flex shrink-0 items-center text-icon-muted">

Posted via Macroscope β€” UI Consistency

Fixed by removing the unused override from the focused PR.

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the new shared page frame lands with a single consumer while a near-identical literal frame remains in the pull-requests route.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/WorkspacePageContainer.tsxlines 23–23Outdated Β· a323f96Open thread
……@@ -0,0 +1,54 @@
20+ return (
21+ <div
22+ className={cn(
23+ "mx-auto flex w-full flex-col gap-6 px-5 pt-6 pb-12 sm:px-6",
MmacroscopeappBoton lines 23–23Aug 17, 2026

components/WorkspacePageContainer.tsx:23

WorkspacePageContainer ships with one consumer (SettingsPageContainer), while apps/web/src/routes/_chat.pull-requests.tsx:1923 still hand-rolls the same frame verbatim β€” mx-auto flex w-full max-w-4xl flex-col gap-4 px-5 pt-6 pb-12, inside the identical topbar-scroll-fade … [--topbar-scroll-fade-height:1.5rem] sm:[--topbar-scroll-fade-height:1.5rem] scroller and carrying the same "top padding is the fade band's own height" comment this container's pt-6 now encodes.

Two owners for one geometry drift immediately: this container adds sm:px-6, so above sm the settings frame insets 1.5rem while the pull-requests frame stays at 1.25rem, and the next tweak to the shared frame reaches only one of the two pages.

Suggest migrating that call site in this PR so the frame has a single owner:

diff
1- <div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-5 pt-6 pb-12">
2+ <WorkspacePageContainer className="gap-4">

No longer relevant as of aa7c78cbb699367e3075b91585ccfc0431c820da

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One finding: the new Copy submenu makes three previously top-level actions unreachable without a pointer in the browser context-menu fallback. Details inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/threadActionMenu.logic.tslines 109–121Outdated Β· aa7c78cOpen thread
……@@ -91,26 +94,48 @@ export function buildThreadActionMenuItems(
118+ : []),
119+ { id: "copy-thread-id", label: "Thread ID", icon: "hash" },
120+ ],
121+ },
MmacroscopeappBoton lines 109–121Aug 19, 2026

Grouping these three under a submenu drops them out of reach in the web build. showContextMenuFallback (the renderer used whenever window.desktopBridge is absent) only opens a submenu from the parent row's mouseenter handler and explicitly swallows the parent's activation (button.addEventListener("click", (event) => event.preventDefault()), contextMenuFallback.ts:383), and the menu handles no keys other than Escape. So Copy path / Copy branch / Copy thread ID, which were flat, focusable, Enter-activatable rows before this change, now have no keyboard path at all on web β€” only the native Electron menu handles submenu traversal. Snooze had the same limitation, but it is duplicated by the row's snooze control; these copy actions are not.

Smallest fix: keep the grouping and let the fallback's submenu parent open on activation too β€” in the hasChildren branch, open the child menu from the click handler (and focus its first row) instead of only preventing default. Alternatively leave the copy items flat until the fallback supports keyboard submenu navigation.

Posted via Macroscope β€” UI Consistency

The thread action menu redesign was removed from this stack PR and moved to #7476, where it can be reviewed independently.

MmacroscopeappBotreviewed Β· 2 inline threadsAug 19, 2026
Synced from GitHub

Reviewed the shared-header extraction, the settings container/width contract, the sidebar row control migration, and the toolbar theming change. The WorkspacePageHeader migration preserves the previous geometry (--workspace-topbar-height already resolves env(titlebar-area-height) under .wco, and wco: outranks sm: on specificity, so the native-controls inset still wins), and the new micro size now matches icon-micro's radius. Two findings below.

Posted via Macroscope β€” UI Consistency

apps/web/src/routes/-chatIndexTitlebar.test.tslines 18–18Outdated Β· 528d2c9Open thread
……@@ -15,9 +15,7 @@ describe("hosted static onboarding header", () => {
18- expect(onboardingHeader).toContain("h-[var(--workspace-topbar-height)]");
19- expect(onboardingHeader).toContain("min-h-[var(--workspace-topbar-height)]");
20- expect(onboardingHeader).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS");
18+ expect(onboardingHeader).toContain('<WorkspacePageHeader className="border-b border-border">');
MmacroscopeappBoton lines 18–18Aug 19, 2026

routes/-chatIndexTitlebar.test.ts:18

This file exists as "regression coverage [that] compares the onboarding header with the shared titlebar contract" (line 2), but the rewritten assertion no longer checks the contract β€” it matches an exact JSX substring in _chat.index.tsx. It now passes even if WorkspacePageHeader loses h-[var(--workspace-topbar-height)] or COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, and it fails on a pure formatting change (Prettier wrapping the props onto separate lines).

Since the geometry β€” and a new default (reserveNativeControls = electron) β€” now live in one owner, suggest keeping a loose usage check here and asserting the class contract against ../components/WorkspacePageHeader.tsx:

suggestion
1 expect(onboardingHeader).toContain("<WorkspacePageHeader");
2 const headerSource = NodeFS.readFileSync(
3 new URL("../components/WorkspacePageHeader.tsx", import.meta.url),
4 "utf8",
5 );
6 expect(headerSource).toContain("h-[var(--workspace-topbar-height)]");
7 expect(headerSource).toContain("min-h-[var(--workspace-topbar-height)]");
8 expect(headerSource).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS");

No longer relevant as of 3f11bc2f64d3c4e5d0cd175b386a2a10065b0141

apps/web/src/components/Sidebar.tsxlines 1518–1529Outdated Β· 528d2c9Open thread
……@@ -1384,136 +1392,144 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1515- </span>
1516- ) : null}
1528+ </Button>
1529+ ) : null}
MmacroscopeappBoton lines 1518–1529Aug 19, 2026

components/Sidebar.tsx:1529

The Button migration dropped this control's Tooltip/TooltipPopup (Settle thread), which the other actions in this slot kept (SnoozePopoverButton line 412, Unpin line 411, Woke line 1466).

That is a behavior change, not just missing chrome: the whole row is a TooltipTrigger for detailsTooltip (lines 1359-1374, 1594), and the nested tooltips are what suppress it while the pointer is over an action. With this one removed, hovering Settle now pops the row's full details tooltip instead of the action's own label β€” so the last control in the hover strip behaves differently from its two neighbours.

suggestion
1 {props.settlementSupported ? (
2 <Tooltip>
3 <TooltipTrigger
4 render={
5 <Button
6 size="micro"
7 variant="ghost-muted"
8 aria-label="Settle thread"
9 onClick={handleSettleClick}
10 className="-mr-1 ps-1 pe-1.5"
11 />
12 }
13 >
14 <CheckIcon aria-hidden className={SIDEBAR_LIFECYCLE_ICON_CLASS} />
15 Settle
16 </TooltipTrigger>
17 <TooltipPopup side="top">Settle thread</TooltipPopup>
18 </Tooltip>
19 ) : null}

No longer relevant as of 3f11bc2f64d3c4e5d0cd175b386a2a10065b0141

MmacroscopeappBotreviewed Β· 2 inline threadsAug 19, 2026
Synced from GitHub

UI consistency review: two findings in the sidebar row control migration and the scoped new-thread button. Everything else in the header/settings-layout consolidation looks consistent with the shared primitives.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/Sidebar.tsxlines 3496–3496Outdated Β· 3f11bc2Open thread
……@@ -3435,7 +3492,9 @@ export default function Sidebar() {
34373494 <TooltipPopup side="right">
3438- {projectGroups.length > 1 ? (
3495+ {scopedProjectGroup ? (
3496+ `New thread in ${scopedProjectGroup.displayName}`
MmacroscopeappBoton lines 3496–3496Aug 19, 2026

components/Sidebar.tsx:3496

The tooltip now names the scoped target, but the trigger still sets aria-label="New thread" (line 3484), so the accessible name no longer matches the visible label or what the button does β€” assistive-tech users get no signal that creation is scoped to a project. Suggest deriving both from the same string.

diff
1 onClick={handleNewThreadClick}
2- aria-label="New thread"
3+ aria-label={
4+ scopedProjectGroup
5+ ? `New thread in ${scopedProjectGroup.displayName}`
6+ : "New thread"
7+ }

No longer relevant as of a22960d18260f21b711753e6298fb454ad94ae85

apps/web/src/components/Sidebar.tsxlines 197–197Outdated Β· 3f11bc2Open thread
……@@ -193,6 +194,7 @@ const SETTLED_TAIL_PAGE_COUNT = 25;
193194// Keep the v2 key so existing preferences survive the v2-to-default rename.
194195const SETTLED_SHELF_EXPANDED_KEY = "t3code:sidebar-v2:settled-expanded";
195196const SNOOZED_SHELF_EXPANDED_KEY = "t3code:sidebar-v2:snoozed-expanded";
197+const SIDEBAR_LIFECYCLE_ICON_CLASS = "size-3 shrink-0";
MmacroscopeappBoton lines 197–197Aug 19, 2026

components/Sidebar.tsx:197

Moving these controls onto Button variant="ghost-muted" pins their glyph tone. The variant sets [--control-icon-color:var(--muted-foreground)], and the base rule [&_svg:not([class*='text-'])]:text-[var(--control-icon-color)] matches because this class carries no text-* β€” so the variant's [:hover]:text-foreground brightens only the element's own text, not the icon.

The hand-rolled controls this replaces were text-muted-foreground hover:text-foreground with the glyph inheriting currentColor, so Snooze (400), Wake (1286), Un-settle (1301), Settle (1315), Unpin (1400) all lost their hover brightening. It is most visible on the micro Settle button (1522): the "Settle" label brightens on hover while its check icon stays muted, a two-tone state the control never had.

Suggest opting the glyph back into currentColor here β€” the pinned-icon call site (cn(SIDEBAR_LIFECYCLE_ICON_CLASS, "text-muted-foreground")) still wins through cn, and per-button [--control-icon-color:currentColor] (the pattern used in ui/toast.tsx, ComposerPreviewAnnotationCards.tsx) also works if you prefer keeping the override on the button.

suggestion
1const SIDEBAR_LIFECYCLE_ICON_CLASS = "size-3 shrink-0 text-current";

No longer relevant as of a22960d18260f21b711753e6298fb454ad94ae85

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One finding on the new micro button size: it sets text-xs (and a size-3 icon rule) without overriding the base's sm: step, so the contract flips at β‰₯640px. Details inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/ui/button.tsxlines 32–33Outdated Β· a22960dOpen thread
……@@ -29,6 +29,8 @@ const buttonVariants = cva(
3030 "size-7 sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
3131 lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
32+ micro:
33+ "h-5 gap-1 rounded-sm px-[calc(--spacing(1.5)-1px)] text-xs before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3",
MmacroscopeappBoton lines 32–33Aug 19, 2026

The new micro size sets text-xs / [&_svg…]:size-3 but doesn't override the base's sm: step, so both are dead above 640px. The base string carries text-base … sm:text-sm and [&_svg:not([class*='size-'])]:size-4.5 sm:…:size-4; twMerge only resolves conflicts between classes with identical modifiers, so sm:text-sm survives and wins inside its media query. Net effect: a micro button renders 12px text on mobile and 14px on desktop β€” larger than xs (text-sm sm:text-xs β†’ 12px) β€” inside a 20px-tall control, and its unsized icons snap back to size-4.

That is a regression for this PR's consumer: the Settle control (Sidebar.tsx:1522) was text-xs before the migration and sits beside 12px status text in the same slot. Every other size that shrinks the type pins the sm: step too (xs, xl, and the select/badge equivalents).

suggestion
1 micro:
2 "h-5 gap-1 rounded-sm px-[calc(--spacing(1.5)-1px)] text-xs sm:text-xs before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3 sm:[&_svg:not([class*='size-'])]:size-3",

Posted via Macroscope β€” UI Consistency

The micro size moved to its first consumer in #7150 and now pins both desktop text and icon sizes.

Added 4 commits

  • feat(web): unify workspace navigation0505995
  • fix(web): preserve workspace action contractsa9ba974
  • refactor(web): focus workspace navigationf756913
  • fix(web): preserve sidebar action feedbackffa4428