Skip to content

feat(web): add workspace panes and transitions - #28845

Open
maria-rcks wants to merge 118 commits into
maria/redesign/5-voicefrom
maria/redesign/6-workspace-panes
Open

maria-rcks wants to merge 118 commits into
maria/redesign/5-voicefrom
maria/redesign/6-workspace-panes

Conversation

@maria-rcks

@maria-rcks maria-rcks commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Part 6 of 6 of the T3 Code redesign. Chats, files, terminals and the browser open as panes in a workspace instead of fixed side panels.

  • workspace panes with tab strips: reorder by drag, overflow scrolling that reveals the active tab, restoring closed tabs, and terminal panes in tab drop targets.
  • layout hints and a guided tour introduce the workspace and stay inside the viewport on mobile.
  • ⌥⌘ + scroll moves between columns. Pane backgrounds paint without a retained compositor layer, and hints follow compositor motion without a continuous repaint.
  • the usage page reuses the floating settings shell.

The niri parts are behind Settings > Labs > Scrolling workspaces, off by default: the sideways scrolling strip and swipes, extra workspaces and the overview, and the layout tour and hints. With it off, panes shrink to fit; splits, tabs, side panes and per-project layout templates stay on for everyone. cmd+N and cmd+shift+O open main's project picker when there are several projects, and new threads replace the focused tab instead of opening a new one.

Scrolling workspaces off: six panes shrink to fit

Settings > Labs with the toggle off

cmd+N opens main's project picker

Needs approval: Theo for vite.config.ts (lint ceilings only); Theo or Julius for packages/contracts (ipc, keybindings, server, settings, including the new scrollingWorkspacesEnabled client setting, which decodes to false).

Stack

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

  1. feat(web): establish redesign primitives and shell #28839 1-primitives-shell
  2. feat(web): redesign composer and conversation timeline #28840 2-composer-timeline
  3. feat(web): redesign review panels and supporting pages #28841 3-panels-pages
  4. feat(web): redesign settings and setup surfaces #28842 4-settings-setup
  5. feat(web): style dictation with the redesign primitives #28844 5-voice
  6. feat(web): add workspace panes and transitions #28845 6-workspace-panes (this PR)

Evidence

Captured against base 5ba6597504b681819ff657e5e0808c54b37edb42 and head f6f00d4f4fe175e66f5d7fc6018ace6a1c841ca3, using separate seeded worktrees and the same replay scenarios at 1280 × 720. Dark mode unless labeled light. No code, PR, or branch changes.

Chat, terminal, and the same server/package.json file: the fixed right panel becomes an independent workspace column.

before: chat and terminal beside package.json in the fixed right panel

after: chat and terminal beside package.json in a workspace column

Browser: the same example.com page remains usable beside the chat and terminal, with the file and browser represented as workspace tabs.

before: example.com in the fixed right panel beside chat and terminal

after: example.com in a workspace pane beside chat and terminal

Workspace tab strip: overflow scrolling and active filename reveal were exercised with package.json, README.md, tsconfig.json, vite.config.ts, and the browser. Measured 466 px of tab content in a 326 px viewport; selecting vite.config.ts revealed its filename. Native drag reordered vite.config.ts before README.md and before tsconfig.json in separate passes. The base has no workspace pane tab strip; its fixed panel baseline is shown above.

after: overflowing pane tab strip reveals the selected vite.config.ts filename

Restore a closed chat: Ctrl+Shift+T reopened Replay: todo-list with its existing conversation. Closing and restoring a browser also retained its URL and page. Chat-tab restore has no equivalent on the base.

after: restored Replay todo-list chat retains its conversation beside the browser

Layout introduction and guided tour are new on this layer. Opened Show me, advanced through all five steps, entered the overview lesson, and finished back in the workspace.

after: tabs side by side introduction with four layout diagrams

after: guided tour anchors its first step to the new tab control

Usage adopts the floating settings shell. Both refs show the same real provider error, with matching selections, in dark mode.

before: dark usage limits page reports the Codex JSON-RPC error

after: dark usage limits page in the floating settings shell reports the same error

Usage, matching light-mode pair.

before: light usage limits page with the Codex JSON-RPC error

after: light usage limits page in the floating settings shell with the same error

Nine-second recording: native tab drag, horizontal column scrolling, closing the chat, and restoring it. Reviewed the exported sequence, then played the uploaded video to completion and checked seeking.

after: native pane tab reorder, column scrolling, and closing and restoring a chat

Findings and limits:

  • The recording shows a brief empty pane during chat restoration around 5–6 seconds, before its conversation returns. This is observed app behavior, not a successful no-blank-frame result.
  • Usage retrieval is unverified: both refs report Codex could not read usage (JSON-RPC -32600). No usage data was fabricated.
  • This is a Linux web browser. Ctrl+Alt plus horizontal scroll moved the workspace strip from 223 px to 0 px. The macOS ⌥⌘ path, Electron desktop, native mobile, and three-finger gestures remain unverified.
  • Browser automation intermittently disconnected. Native drag succeeded after targeting the visible pane specifically; exploratory pointer-event checks were not used in the final recording.
  • No automated test suite or production-build checks were run for this evidence-only task.

Verification

Every layer, rebased on main 9469095, 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

@maria-rcks maria-rcks added the DO NOT MERGE Experimental pull request. Do not merge. label Nov 1, 2026
@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Nov 1, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting).

This review would cost an estimated $33.71, which exceeds your per-review limit of $15.00.

The top 3 files driving up this estimate:

File Diff Size Estimate
apps/web/src/layout/SurfaceHost.tsx 81.72KB $2.91
apps/web/src/layout/layoutModel.ts 74.85KB $2.67
apps/web/src/layout/Overview.tsx 54.69KB $1.95

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeapp

macroscopeapp Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — The workspace redesign changes panel animations from instant to 150 ms by default and adds lint-suppression directives. Both changes require human review under the repository’s explicit requirements.

Needs human approval because:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 5.0 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 21.5 KiB — 24.4 KiB ✅
Codex Live turn messages — 1 — 8 ✅
Claude Total thread wire — 5.0 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 21.9 KiB — 24.4 KiB ✅
Claude Live turn messages — 1 — 8 ✅

Baseline: unavailable · PR result: e976636 · 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: 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.

@maria-rcks
maria-rcks added this pull request to stack #28864 November 1, 2026 20:00
@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch from f6f00d4 to b653b87 Compare November 1, 2026 20:11
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

SunkenInTime and others added 30 commits November 9, 2026 09:25
The Context card listed cached input, new input and output for every
provider. For Claude it now lists the categories Claude's /context
reports: system prompt, system tools, MCP tools, T3 Code tools, memory
files, skills, custom agents and messages. The Claude adapter reads them
once per turn end with getContextUsage({ detail: "summary" }) and puts
them on the turn's token usage as an optional breakdown. Other providers,
and Claude CLIs that cannot answer, keep today's rows.
The current thread no longer gets its own row; a thread with no relatives
hides the card unless its menu still offers disconnecting the agent session.
A child opened from the Lineage card or a timeline link opens as an italic
ghost tab after its parent's tab instead of replacing it. The next child
replaces the ghost, closing it returns to the parent, and double-clicking,
Keep open, moving it or sending a message makes it an ordinary tab.
A delegated task on a linked environment kept its own Done row after it
finished. It now joins the previous agents menu and its count, failures
included in the failed count; running and waiting tasks keep their rows.
The parent link in a subagent's header was a grey pill with no icon. It is
now an inactive tab with the parent's project icon, and the timeline drops
the Subagent of divider the header already covers.
…arts under the tabs

The terminal pane's split and close actions move to a mirrored right notch, as a panel pane's do, and its "+" follows the last tab like the chat tab strip's. The shell drops the band and side gutter around it, so it starts right under the notch with its text in line with the tab icons.
- ThreadServersPanel, ThreadAutomationsPanel: drop the empty-card visibility cases, a visual fix proven by captures
- ThreadAutomationsPanel: drop toBeDefined asserts on elements the next line already uses
- ThreadTerminalDrawer: drop the add-to-chat menu item case, it mirrors an optional prop
- KeybindingsSettings.logic: drop the default label copy check
- settingsSearch: fold seven identical layout rows into one case
- contracts settings: drop the onboardingCompletedAt decode/encode round trip, a schema mirror
runInNewLayoutTab cleared whatever request the store held once its own
action settled, so a second cmd+N started before the first settled lost
its request and replaced the first new tab instead of adding one. Each
request now carries its own ticket and an action clears only its own.
The chip and divider drags kept their gesture in a press ref plus a drag
state, and the release read the drag from the last render. A flick whose
move had not rendered yet lost its drop: the release click appended the
pane on the right and the ghost and drop zone stayed on screen. Each
gesture is now one explicit phase the handlers read directly; the state
only draws the canvas.
A plain open (sidebar click, link, palette) lands in the focused pane's
ghost tab. A ghost or launcher is replaced in place, a kept tab gets the
ghost right after it, and a thread already open is focused where it is.
A focused surface targets its thread's chat, or opens the ghost beside
itself when that thread has no chat. A window's lone ghost looks like any
other tab, so one pane with no arranging behaves as before.

Middle-click opens a kept tab, alt-click opens to the side, and new
threads (cmd+N) still replace the focused tab. Sending a message no longer
keeps a ghost, for sidebar and lineage ghosts alike. A thread's terminal
and side panes hide when an open swaps its chat off a pane and come back
when an open brings it back. The sidebar's current row follows a focused
surface's thread. Layouts saved before make their lone chat the ghost.
Panes other than the focused one draw their active tab a tier down, in the
muted foreground, and their other tabs in the dim foreground, so the window
has one full-strength tab label. A single pane is unchanged.
Thread details reads the context breakdown from compaction.logic, where main moved the meter's helpers, and the context file keeps only the segment bar it still renders. cmd+N no longer reads the removed legacy sidebar setting, ChatView drops the compact reason nothing shows anymore, and the settings search filter wraps as the formatter expects.
…debar

Pressing Ctrl or Cmd re-rendered the whole sidebar and the composer's
send controls on keydown and again on keyup, so every chord like Mod+K
paid two full sidebar renders before its own work. Held modifiers now
live in one shared store; the sidebar and composer subscribe to a
derived boolean (hints shown after the hold delay, queue mode) and
re-render only when it flips. A quick chord never flips it.

Shortcut labels walked every keybinding per lookup (96 rules, several
lookups per render). Effective shortcuts are now computed once per
keybindings array and context. Keydown resolution skips rules for other
keys before evaluating when-clauses.

cmd+K open, keydown to list painted, median/p90 ms (10-12 runs,
interleaved with main and nightly, production builds, shared load):
  before: this 374/724, main 530/793, nightly 703/979 (load ~22)
  after:  this 262/349, main 473/569, nightly 433/527 (load ~15)
Ctrl keydown task during cmd+K: 19-42 ms -> 5.5 ms (trace).

Layer 1 (establish redesign primitives and shell); all touched lines
are main's code.
…very row

The virtualized scroll fade sets --fade-size and the mask in custom
properties, which inherit by default. Each time a fade turned on or off
(opening cmd+K, the first scroll in any faded list) Chrome restyled every
row below the scroller. Both properties are only read on the element
that sets them, so they are now registered as non-inherited, like the
surface and overview properties. Pixels are unchanged (palette open,
scrolled, typed and model picker screenshots are byte-identical).

cmd+K open, frame after the list renders (trace, same machine):
  ResizeObserver pass 38-42 ms -> 18.5 ms
  restyled elements 835 -> 319

Layer 1 (establish redesign primitives and shell); the fade utility is
main's code.
…eline

Every chat view render re-rendered ChatComposer because `setThreadError` was a
new function each render, and every canvas measurement re-rendered the whole
MessagesTimeline because it read `registerTimeline` from the canvas layout
context. `setThreadError` is now a stable callback, and the timeline reads its
register function from its own context, so canvas resizes reach only the
details card and the preview mini player.

Stack layer 1 (feat(web): establish redesign primitives and shell); the code is
from main.

Measured with the layer 2 footer and banner commits that follow (prod build,
trusted input, 12 samples each, interleaved; median / p90 ms):
- details panel open: 132.5 / 191.9 -> 99.5 / 130.2 (main 95.9 / 142.8, nightly 94.2 / 134.2)
- thread switch, short: 380.2 / 428.6 -> 308.8 / 445.1 (main 298.4 / 356.9, nightly 303.5 / 357.9)
- thread switch, long: 449.9 / 608.1 -> 408.8 / 502.3 (main 449.8 / 529.5, nightly 488.0 / 741.4)
Details toggle commits render 138 + 33 components instead of 182 + 106 for open
and 32 instead of 76 + 75 for close.
…oss chat view renders

The timeline footer was a new element on every chat view render, because the
compiler leaves an element unmemoized when it depends on the provider list or
the resolved handoff, so every ChatView render re-rendered the whole
MessagesTimeline. The footer is now a TimelineFooter component that reads the
providers and resolves the handoff itself, built outside the condition where
the compiler memoizes it.

An empty composer banner list was a new array each render, which re-created the
ChatComposer element. The empty case now uses the shared constant before the
element is built.

Stack layer 2 (feat(web): redesign composer and conversation timeline); the
footer shape and the tray banner list come from that layer.

Measured together with the layer 1 commit before it (prod build, trusted input,
12 samples each, interleaved; median / p90 ms):
- details panel open: 132.5 / 191.9 -> 99.5 / 130.2 (main 95.9 / 142.8, nightly 94.2 / 134.2)
- thread switch, short: 380.2 / 428.6 -> 308.8 / 445.1 (main 298.4 / 356.9, nightly 303.5 / 357.9)
- thread switch, long: 449.9 / 608.1 -> 408.8 / 502.3 (main 449.8 / 529.5, nightly 488.0 / 741.4)
…red every frame

The terminal hint was positioned from the host by measuring the focused pane,
host and composer on every animation frame through the browser surface
tracker while the terminal pane slid in, and it still trailed the pane by a
frame.

The terminal hint now renders inside the focused pane, which slides in on
top, where its resting classes already center it, so the pane's own
transform carries it. Only a lift above a taller composer is measured, once
on mount and on resize. Column and workspace hints keep the host placement:
their pane can pass behind the panes it crosses, and the hint stays above.

Stack layer 6 (feat(web): add workspace panes and transitions).

Main-thread CPU in the 700 ms after each open (trace, 2x6 opens each):
  before 400-443 ms, 56-57 forced layouts
  after  255-260 ms, 21-22 forced layouts
  main   180 ms, 12 forced layouts

Terminal open wall-clock (first frame with the terminal on screen) is
unchanged within noise; the saving is in the frames of the slide after it.
Interleaved, n=30 each, median / p90 ms, three runs:
  before 209.6 / 242.3, after 203.3 / 239.3 (main 185.6, nightly 183.0)
  before 245.1 / 268.4, after 228.9 / 260.6 (main 193.9, nightly 194.4)
  before 209.1 / 242.4, after 211.3 / 248.4 (main 176.2, nightly 187.4)
Thread switch (short), n=30: 350.1 -> 340.3 ms, unchanged within noise.
cmd+D toggles the diff where Settings > Layout puts the side pane, as a normal
tab, instead of opening an empty launcher pane. A diff tab hidden behind
another tab comes forward. A new pane to the right moves to cmd+\, and a saved
copy of the old cmd+D binding is dropped on startup.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE Experimental pull request. Do not merge. size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants