feat(web): establish redesign primitives and shell#28839

open30
main
maria/redesign/1-primitives-shell
opened4d agobyMmaria
View on GitHubTwo-way sync enabledPR last imported 38m ago

Part 1 of 6 of the T3 Code redesign. The web client's controls, overlays and shell use a mix of one-off sizes and styles that don't follow the new design canvas.

This layer adds the redesign's shared foundation, which the other five build on:

  • components/ui primitives (buttons, menus, pickers, fields, dialogs, popovers, tooltips) restyled to the canvas, with relaxed picker spacing, compact key hints and the inset field surface.
  • type roles and the /design-system catalog, kept in sync with the primitives.
  • the app shell: sidebar, titlebar, project search, browser address toolbar, floating browser preview frame.
  • reduced motion honored in overlay primitives.

Needs approval: Theo for vite.config.ts and components/ui/scroll-area.tsx; Theo or Julius for packages/contracts (ipc, server, settings).

Stack

Do not merge: this stack still needs review. Each PR targets the one below it.

  1. #28839 1-primitives-shell (this PR)
  2. #28840 2-composer-timeline
  3. #28841 3-panels-pages
  4. #28842 4-settings-setup
  5. #28844 5-voice
  6. #28845 6-workspace-panes

Evidence

compared upstream main 57d764ea1aaa0227ed851641c155765525df9273 with 9873e800af8e7205038528dbe52ec556f11c079b in isolated worktrees, using the same seed and todo-list replay. captures are 1280ร—720, except the add action dialog at 1280ร—900. replay timestamps and measured turn duration differ between runs.

the sidebar and titlebar use inset panes, revised type and compact controls in dark mode.

before: app shell with the todo-list replay in dark mode

after: app shell with the todo-list replay in dark mode

the same shell and replay in light mode.

before: app shell with the todo-list replay in light mode

after: app shell with the todo-list replay in light mode

project filtering keeps the same two results for "t3" with revised field and picker geometry.

before: project picker filtered to t3

after: project picker filtered to t3

the thread action menu uses wider rows, revised spacing, icons and surface styling.

before: thread action menu for the todo-list replay

after: thread action menu for the todo-list replay

the add action dialog uses the new type roles, inset fields, buttons and switches.

before: add action dialog with empty fields

after: add action dialog with empty fields

the sidebar toggle tooltip uses the revised surface and type styling.

before: sidebar toggle tooltip with its keyboard shortcut

after: sidebar toggle tooltip with its keyboard shortcut

the browser address toolbar uses an inset address field and revised spacing around navigation controls.

before: browser address toolbar with example.com loaded

after: browser address toolbar with example.com loaded

the new web titlebar back and forward controls navigate between the seeded thread and replay.

after: titlebar history navigation between two threads

the browser floats into the revised landscape frame while retaining its loaded page.

after: loaded browser moving into the floating landscape frame

the /design-system catalog is new in this layer, so there is no before equivalent. these captures show the live foundation tokens and button specimens.

after: new design-system catalog foundation tokens

after: new design-system catalog button variants and sizes

all images were inspected at full size. uploaded images were opened at their original dimensions. both trimmed recordings were inspected, played through, and seeked in the uploaded browser player. project filtering, thread menus, dialog open/cancel, tooltips, sidebar collapse/restore, titlebar history navigation, browser URL navigation and floating preview opening were exercised in the real web client with the dev replay provider.

unverified: returning the floating preview to the right panel, because the automation click timed out; native desktop chrome, mobile, reduced-motion behavior and narrow layouts were outside this capture pass. no code checks were run for this evidence-only task. no pr edits, comments, commits or pushes were made.

Verification

Every layer, rebased on main 94690956fd, passes typecheck, knip, lint and format on Blacksmith on its own. At layer 6, the web suite passes (unit and browser). The desktop suite fails one libsecret test, which fails the same way on main. GitHub CI runs the full suite on every layer.

Done with Claude Code (Opus 5.5) and Codex (gpt-6.1-sol high) children in T3 Code.

๐Ÿค– Generated with Claude Code

Checks

5 successful ยท 5 skipped ยท 1 neutral
All checks passed
159766e
oxlint-plugin-t3code/rules/no-styled-raw-controls.tsline 154Outdated ยท 9873e80Open thread
โ€ฆโ€ฆ@@ -0,0 +1,195 @@
151+ if (node.kind !== "const") return;
152+ for (const declarator of node.declarations) {
153+ if (declarator.id.type === "Identifier" && declarator.init) {
154+ constants.set(declarator.id.name, declarator.init);
MmacroscopeappBoton line 1544d ago

๐ŸŸข Low rules/no-styled-raw-controls.ts:154

A raw control's className can be missed or falsely reported when another function declares a const with the same name: the file-wide map overwrites earlier initializers, so Program:exit resolves every identifier to the last visited declaration instead of its lexical binding. Track constants by lexical binding so each JSX expression resolves to its own initializer.

No longer relevant as of ab9ef1256d764a406fc6fef7d50f965ea5779d9c

Note

Written by claude-opus-5-5 on behalf of Maria

fixed in b58d7983c6: identifiers now resolve to their lexical const declaration instead of a file-wide name map. the existing rule tests cover sibling scopes, parameter and mutable-binding shadowing, aliases, forward declarations, and cycles.

oxlint-plugin-t3code/rules/no-styled-raw-controls.tslines 88โ€“90Outdated ยท 9873e80Open thread
โ€ฆโ€ฆ@@ -0,0 +1,195 @@
87+ break;
88+ case "LogicalExpression":
89+ recurse(expression.right);
90+ break;
MmacroscopeappBoton lines 88โ€“904d ago

๐ŸŸข Low rules/no-styled-raw-controls.ts:88

A || or ?? expression can return appearance classes from its left operand, but classStrings only inspects the right operand, so <button className={"bg-red-500" || "flex"} /> is silently accepted. Inspect the left operand for || and ?? as well.

No longer relevant as of ab9ef1256d764a406fc6fef7d50f965ea5779d9c

Note

Written by claude-opus-5-5 on behalf of Maria

fixed in b58d7983c6: the rule now inspects both operands of || and ??, while && still reads only its result operand. the existing rule tests cover both missed left operands, a fallback, and the && condition.

Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval โ€” The redesign introduces a gradient-rim default appearance and adds a Knip dependency-analysis suppression for the bundled font. Both changes require human review under the configured approval requirements.

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

Synced from GitHub

Thread transfer impact

โœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiBโœ…
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiBโœ…
CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiBโœ…
CodexLive turn WebSocket decoded21.5 KiB21.5 KiB0 B (0.0%)24.4 KiBโœ…
CodexLive turn messages110 (0.0%)8โœ…
ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiBโœ…
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiBโœ…
ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiBโœ…
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiBโœ…
ClaudeLive turn messages110 (0.0%)8โœ…

Baseline: c52df24 ยท PR result: a123d01 ยท Source CI: failure

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: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

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

apps/web/src/components/chat/DraftHeroHeadline.tsxline 335Outdated ยท 2a80af8Open thread
โ€ฆโ€ฆ@@ -413,27 +308,119 @@ export function DraftHeroHeadline({
332+ icon={<FoldersIcon className="size-4 shrink-0 text-muted-foreground" />}
333+ label="All projectsโ€ฆ"
334+ detail={String(menuEntries.length)}
335+ onSelect={openAllProjects}
MmacroscopeappBoton line 3353d ago

๐ŸŸก Medium chat/DraftHeroHeadline.tsx:335

When there are more than three unpinned projects, choosing a hidden project through All projectsโ€ฆ leaves the prompt and attachments in the current draft. This row opens the global new-thread-in action, which creates or navigates to another draft instead of retargeting the populated draft as visible project rows do; route full-picker selections through selectProject.

No longer relevant as of de9a00aecdd0c05fc1f9565eeea7ddd9c52af9be

Note

Written by claude-opus-5-5 on behalf of Maria

Confirmed and fixed. "All projectsโ€ฆ" in DraftHeroHeadline.tsx now passes an onSelectProject callback through openCommandPalette (commandPaletteBus.ts), reduceCommandPaletteUiState (CommandPalette.logic.ts) and OpenCommandPaletteDialog (CommandPalette.tsx). Picking a hidden project goes through the same selectProject path as the visible rows. Verified in the real app: hidden and visible picks both keep the same draft, prompt and attachment. Cancelling the palette keeps it too. The global new-thread action still opens a separate draft.

Synced from GitHub

Explain Change โ†’

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix ยท Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
oxlint-plugin-t3code/rules/no-dot-separated-text.tsline 56Outdated ยท 9802f93Open thread
โ€ฆโ€ฆ@@ -0,0 +1,95 @@
53+ const hasLeft = left || TEXT.test(text.slice(0, index));
54+ const hasRight = right || TEXT.test(text.slice(index + 1));
55+ // A leading bullet in document/list content is not a metadata separator.
56+ if (glyph === "โ€ข" && !hasLeft && hasRight) continue;
MmacroscopeappBoton line 563d ago

๐ŸŸข Low rules/no-dot-separated-text.ts:56

A โ€ข at the start of a JSXText node is exempted even when earlier JSX siblings already render content, so <span>{name} โ€ข today</span> is not reported as dot-separated metadata. The exemption checks only the current text node; account for preceding rendered siblings before treating the bullet as a leading list marker.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @oxlint-plugin-t3code/rules/no-dot-separated-text.ts around line 56:
2
3A `โ€ข` at the start of a `JSXText` node is exempted even when earlier JSX siblings already render content, so `<span>{name} โ€ข today</span>` is not reported as dot-separated metadata. The exemption checks only the current text node; account for preceding rendered siblings before treating the bullet as a leading list marker.

Note

Written by claude-opus-5-5 on behalf of Maria

Fixed. JSXText in no-dot-separated-text.ts now checks the node's earlier siblings, so a leading โ€ข after rendered content such as <span>{name} โ€ข today</span> is reported. A bullet that starts a list item with nothing before it, like <li>โ€ข item</li>, is still allowed. Verified with a throwaway fixture that caught the first case and allowed the second, and the full apps/web/src lint stays at zero hits.

oxlint-plugin-t3code/rules/no-dot-separated-text.tsline 87Outdated ยท 15320fdOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,110 @@
84+ },
85+ JSXText(node) {
86+ // Earlier siblings such as `{name}` render content before a leading glyph.
87+ const parent = node.parent;
MmacroscopeappBoton line 873d ago

๐ŸŸข Low rules/no-dot-separated-text.ts:87

A leading โ€ข followed by a JSX expression is reported as dot-separated metadata, so ordinary list items such as <li>โ€ข {label}</li> trigger this rule while the static-text equivalent is exempt. JSXText only checks whether earlier siblings render content, leaving hasRight false when the text after the bullet is in a later expression; include following siblings when computing the right-side content.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @oxlint-plugin-t3code/rules/no-dot-separated-text.ts around line 87:
2
3A leading `โ€ข` followed by a JSX expression is reported as dot-separated metadata, so ordinary list items such as `<li>โ€ข {label}</li>` trigger this rule while the static-text equivalent is exempt. `JSXText` only checks whether earlier siblings render content, leaving `hasRight` false when the text after the bullet is in a later expression; include following siblings when computing the right-side content.

Note

Written by claude-opus-5-5 on behalf of Maria

Fixed. JSXText now checks content-rendering siblings on both sides of the text node. A leading โ€ข followed by an expression, as in <li>โ€ข {label}</li>, is treated as a list bullet and allowed, the same as <li>โ€ข item</li>. {name} โ€ข today and {a} ยท {b} are still reported. Verified with a throwaway fixture covering all four cases, and apps/web/src stays at zero hits.

oxlint-plugin-t3code/rules/no-dot-separated-text.tslines 56โ€“56Outdated ยท 3845a0aOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,109 @@
53+ const hasLeft = left || TEXT.test(text.slice(0, index));
54+ const hasRight = right || TEXT.test(text.slice(index + 1));
55+ // A leading bullet in document/list content is not a metadata separator.
56+ if (glyph === "โ€ข" && !hasLeft && hasRight) continue;
MmacroscopeappBoton lines 56โ€“563d ago

๐ŸŸข Low rules/no-dot-separated-text.ts:56

A second โ€ข at the start of a line in a multiline literal is reported as a separator, rejecting valid list content. The exemption checks all text before the bullet, including earlier lines; determine whether the bullet is leading using only the text since the previous newline.

suggestion
1 if (
2 glyph === "โ€ข" &&
3 !left &&
4 !TEXT.test(text.slice(text.lastIndexOf("\n", index - 1) + 1, index)) &&
5 hasRight
6 ) continue;
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @oxlint-plugin-t3code/rules/no-dot-separated-text.ts around line 56:
2
3A second `โ€ข` at the start of a line in a multiline literal is reported as a separator, rejecting valid list content. The exemption checks all text before the bullet, including earlier lines; determine whether the bullet is leading using only the text since the previous newline.

Note

Written by claude-opus-5-5 on behalf of Maria

Fixed. The leading-bullet exemption in no-dot-separated-text.ts now looks only at text on the same line as the glyph. So a โ€ข that starts any line of a multiline literal, as in "โ€ข one\nโ€ข two", is allowed. "one โ€ข two", {name} โ€ข today and {a} ยท {b} are still reported. Verified with a throwaway fixture, and apps/web/src stays at zero hits.

apps/web/src/components/Sidebar.tsxline 3650Outdated ยท c2cf09fOpen thread
โ€ฆโ€ฆ@@ -3541,8 +3641,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
3647+ // into it.
35443648 className={cn(
35453649 workingThreads.length + snoozedThreads.length === 0 && "mt-auto",
3650+ settledCount === 0 && from === null && "invisible",
MmacroscopeappBoton line 36503d ago

๐ŸŸก Medium components/Sidebar.tsx:3650

When settledCount is 0 and no drag is active, the visually hidden Settled header remains in the sidebar tree, so pressing End or Down can select a row that cannot receive focus and leave keyboard navigation stranded. installSidebarTree excludes [hidden], [inert], and [aria-hidden="true"] rows, but not CSS-invisible rows; mark this header aria-hidden under the same condition while retaining invisible to preserve its layout space.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/Sidebar.tsx around line 3650:
2
3When `settledCount` is 0 and no drag is active, the visually hidden Settled header remains in the sidebar tree, so pressing End or Down can select a row that cannot receive focus and leave keyboard navigation stranded. `installSidebarTree` excludes `[hidden]`, `[inert]`, and `[aria-hidden="true"]` rows, but not CSS-invisible rows; mark this header `aria-hidden` under the same condition while retaining `invisible` to preserve its layout space.

Note

Written by claude-opus-5-5 on behalf of Maria

Fixed. SidebarSectionHeader in SidebarListMarkers.tsx takes a concealed flag that sets aria-hidden and tabIndex={-1} on the shelf toggle while keeping its layout space. Sidebar.tsx passes it for the Settled header when settledCount is 0 and no drag is active. installSidebarTree now skips that row, so End and Down never land on an invisible header.

Added 30 commits

  • feat(web): establish redesign primitives and shell9256da7
  • fix(web): match catalog button and type specimensaf66f95
  • refactor(web): remove unused toggle pressed tokensedaaada
  • fix(ui): use the inset field surface and format picker variants01c4514
  • fix(ui): keep palette actions above provider notices2c9019c
  • fix(web): give provider tiles room for icons and labels50a5be3
  • fix(web): align project search and sidebar badge sizes5663d97
  • fix(web): honor reduced motion in overlay primitives620d1a5
  • fix(ui): preserve web context menu widths7faecfb
  • fix(web): match shell and overlay design geometry83f126c
  • fix(web): preserve shell type role and titlebar spacingca6ec68
  • fix(web): match browser address toolbar spacing to designd178276
  • fix(chat): match inline request cards to the timeline designdbac3a6
  • feat(ui): add relaxed picker spacing and compact key hintseef5811
  • fix(ui): fit browser previews inside a landscape floating framef235cf5
  • fix(web): preserve rebased server text and shortcut expectations090e77a
  • fix(web): sidebar New thread skips the project dialog (#29973)29c3f4d
  • fix(web): Snoozed and Settled rows drop the PR badge, and snooze times drop "Until" (#29134)3a9be72
  • fix(web): palette project rows show the location under the name (#29980)47b8861
  • fix(web): open pull requests show the arrow icon again (#29213)500e3b4
  • fix(web): project groups show an icon if any checkout has one (#29199)78dae29
  • fix(web): palette result titles use the 14px label role7a31fcf
  • fix(web): sidebar rows no longer show a Merged status (#31105)93377de
  • fix(web): restore main sidebar thread rows964b015
  • fix(web): new-thread project rows inset their environment icon (#29985)ae7f8af
  • fix(web): snoozed rows show the snooze time like maindaef335
  • fix(web): home screen project rows show the project they open (#29085)dbc587d
  • fix(web): restore compact panel controls and touch targetse54b227
  • fix(web): adapt main's dictation UI to the redesign primitives159766e
  • fix(web): sidebar PR badges use the same green as the rest of the redesign (#30470)63076a7