# Workbench (/cloud/surfaces/workbench)



<HostMatrix
  primitive="{
  label: &#x22;@nyte-ai/ui/tabs, /toolbar, /context-menu, /button&#x22;,
  href: &#x22;/cloud/headless/tabs&#x22;,
}"
  desktop="{
  label: &#x22;workbench/workbench.tsx&#x22;,
  note: &#x22;Workbench, WorkbenchTabStrip, FilesPanel, ChangesPanel, TerminalPanel, BrowserPanel, WorkspaceFileEditor, WorkspaceSearch&#x22;,
}"
  terminal="none"
/>

## `Workbench` [#workbench]

"Eager workbench controller and rail. Open panels stay mounted while hidden; closing a tab
releases its panel. The window titlebar owns the tab strip."

| Prop      | Type              | Description                                                                     |
| --------- | ----------------- | ------------------------------------------------------------------------------- |
| `target`  | `WorkbenchTarget` | `session` / `workspace` / `home`.                                               |
| `paneKey` | `string`          | "Stable stage identity; views are keyed on it together with their data target." |

Hidden views are `hidden` + `aria-hidden` + `inert`. Tab panels are `role="tabpanel"` with
`id="{viewKey}-panel-{tab}"` and `aria-labelledby="{viewKey}-tab-{value}"`. The rail is
`<nav aria-label="Workbench navigation">`.

Resize sash: `role="separator"`, `aria-label="Resize workbench"`, `aria-valuemin/max/now`,
`title="Drag to resize. Double-click to reset."` Writes `--nyte-active-workbench-width` live.

| Key          | Effect    |
| ------------ | --------- |
| ArrowLeft    | +20px     |
| ArrowRight   | −20px     |
| Home / End   | min / max |
| Double-click | reset     |

## Tab strip [#tab-strip]

`WorkbenchTabStrip({ viewKey, view, scope, workspacePath })`, "the only workbench tab strip,
rendered in the window titlebar," on [`@nyte-ai/ui/tabs`](/cloud/headless/tabs) with
`Tabs.List aria-label="Workbench tabs"`.

| Input                    | Effect                                                               |
| ------------------------ | -------------------------------------------------------------------- |
| Delete on a focused tab  | Close                                                                |
| Middle-click             | Close                                                                |
| Double-click a file tab  | Pin (preview → permanent)                                            |
| Right-click terminal tab | ContextMenu: New Terminal, Copy Selection, Clear Terminal, Close Tab |
| `+` button               | Menu "New workbench tab"                                             |

Closing a dirty file or a running shell opens a [ConfirmDialog](/cloud/desktop/confirm-dialog).
After close, focus returns to the selected tab or to `#workbench-toggle`. The close button is
tabbable only on the active tab and revealed on hover via `--_tab-close-opacity`.

## Files [#files]

`FilesPanel({ viewKey, workspaceActive })`: Pierre `FileTree` (compact, flattened empty dirs,
git status), one `WorkspaceFileEditor` per open tab, `WorkspaceSearch`, and a File options `Menu`
with `MenuSwitchItem`s (Line Numbers, Word Wrap, Git Blame, Auto Save, Format on Save; `tone="green"`,
`size="compact"`, `layout="plain"`).

| Key                                                                     | Effect                                    |
| ----------------------------------------------------------------------- | ----------------------------------------- |
| ⌘S / Ctrl+S                                                             | Save the active file                      |
| ⌘F / Ctrl+F                                                             | Open workspace search and focus its input |
| In the tree's context menu: Escape closes; ArrowDown/Up cycle; Home/End |                                           |

`WorkspaceFileEditor` wraps Pierre's `EditProvider` + `CodeView` (history 200 entries). Inactive
editors stay `visibility: hidden` so "virtualized editors retain their scroll position." Autosave
after 1s idle. Status line is `role="status"` / `role="alert"` (Saving…, Saved, Unsaved changes,
conflict, error with Retry). "Format/reload are ordinary undoable edits; they do not recreate the
editor or its history."

`WorkspaceSearch({ onOpen, drafts, active })`: 250ms debounce; `aria-pressed` toggles for match
case, whole word, regex; include/exclude globs; results as `<section aria-label>` per file with
`<mark>` highlights and fully-labelled match buttons.

## Changes [#changes]

See [Diff view](/cloud/surfaces/diff-view#changes-panel).

## Terminal [#terminal]

`TerminalPanel({ owner, sessionId, workspacePath, visible })`. Keyboard inside the terminal
belongs to the runtime (Ghostty). `TerminalStatus` renders `role="status"` / `role="alert"`
lines: "Agent command · read-only", completed / failed / cancelled / interrupted, "Starting
terminal…", "Shell exited \[with code N]". Buttons: New Terminal, Try Again, Restart.

## Browser [#browser]

`BrowserPanel({ surface, visible, historyVisible, url, onUrlChange, toolbarActions })`. The address
field (`inputMode="url"`, `aria-label="Address"`) selects all on focus and swaps to the raw URL;
Enter navigates (or reloads if unchanged); Escape cancels the draft and blurs. The focus ring is
drawn on the rounded wrapper via `:focus-within`: "The ring belongs on the rounded field, not on
the square input nested inside it." Bounds stream to the host through a `ResizeObserver` on every
ancestor; surface close is deferred 50ms to survive remounts.

## Panes and the stage [#panes-and-the-stage]

`ThreadScreen` (`screens/thread.tsx`): "The persistent desktop stage. Pane hosts are keyed only by
PaneId; selecting another session changes a host's data binding without replacing its DOM or its
view-state owner." Each `PaneHost` is `<section aria-label="[Active ]chat pane">`; pointer down
or focus makes it active. `SplitSash`: `role="separator"`, `aria-label="Resize chat panes"`,
`aria-valuemin=20`, `aria-valuemax=80`.

| Key                         | Effect      |
| --------------------------- | ----------- |
| Arrows along the split axis | ±0.02 ratio |
| Shift + arrow               | ±0.1        |

"Only a side-by-side split can starve a pane of width, so the pixel floor applies on that axis
alone." Sessions are dragged into panes from the sidebar; see
[Sidebar › Drag to split](/cloud/surfaces/sidebar#drag-to-split).

## Titlebar [#titlebar]

`Titlebar` (`chrome/titlebar.tsx`): "Permanent window chrome. The sidebar toggle stays on the rail
side; chat actions and the stage-level workbench entry stay at the trailing edge." macOS reserves
a 72px traffic-light lane. Handles `terminal`, `new-terminal`, and `workbench` client actions;
advertises sidebar, back, and forward via `aria-keyshortcuts`. See the
[client action table](/cloud/surfaces/command-palette#client-actions).
