Workspaces

A workspace is a folder and the layout you open it in. It holds its own tabs, its own panes and its own title, and a window holds several at once. The sidebar down the left lists the ones you have open, and S docks it or collapses it again.

Workspaces
SSH
Agents
1 import AppKit
2
3 final class SidebarView: NSView {
4 private let workspaces: WorkspaceList
5 private let agents: AgentRoster
6 private var isCollapsed = false
7
8 static let expandedWidth: CGFloat = 240
9
10 func render(_ state: SidebarState) {
11 workspaceSection.render(state.workspaces)
12 agentSection.render(state.agents)
13 }
14
15 /// Jump: workspace, tab and pane in one move.
16 func jump(to agent: AgentID) {
17 guard let target = agents.locate(agent) else { return }
18 onSelectWorkspace(target.workspace)
19 }
20 }
21
22 extension SidebarView {
23 /// Rows the collapsed rail still answers for.
24 var waitingCount: Int {
25 agents.filter { $0.isWaiting }.count
26 }
27 }
NORMALSidebarView.swiftswift19:9
~/Dev/zen-512-sidebar zen-512-sidebar ❯
swift build
Building for debugging...
[412/418] Compiling ZenTerm SidebarView.swift
[417/418] Linking ZenTerm
Build complete! (8.42s)
 
~/Dev/zen-512-sidebar zen-512-sidebar ❯
swift test
Test Suite 'All tests' started
Executed 214 tests with 0 failures (1.86s)
~/Dev/zen-512-sidebar zen-512-sidebar ❯
git status -s
M Sources/ZenTerm/WindowController.swift
M Sources/ZenTerm/TabBarView.swift
?? Sources/ZenTerm/SidebarView.swift
?? Sources/ZenTerm/AgentRoster.swift
 
~/Dev/zen-512-sidebar zen-512-sidebar ❯
git log --oneline -3
c6e1e36 Nest worktrees under their workspace
e9ea16d List the window's agents in the sidebar
a8b1197 Reveal the sidebar from the window edge
1 editor2 tests3 logs
The sidebar, docked

What the sidebar lists

Your open workspaces come first, each with its branch, and a workspace's Git worktrees sit nested under it. Click a row to switch to it. The workspaces you leave keep running, the way a tab you are not looking at does.

A worktree deleted from a shell or another tool stays open until you close it, since an agent in it may still be running. Its row reads removed here and in the picker, and a notice offers Close Workspace.

Under them, Agents lists what is running in the window: what each agent is doing, then the workspace it is in. Waiting comes first, then working, then idle. Click one and ZenTerm switches workspace, selects the tab and focuses the pane, opening a collapsed drawer to get there. Hover one to read its last message in full. A workspace row carries a dot while an agent inside it waits on you.

When agents wait in your other windows, a row in Agents counts them. Click it or press Return, and ZenTerm raises that window and lands on the agent there that has waited longest. A does the same for this window.

claude and codex count as agents out of the box. Name the other agent programs you run in Settings → General, under Agents, or in the agents key of your config. The ai key names one more, and lives in the config file only. A notification from any other program posts a notice that it finished, with no macOS banner, and its pane does not wait on you.

The footer carries the command palette and Settings. A new window opens the way you last left the sidebar.

Reach it from the keyboard

Arrows move focus between rows, Return activates the focused row, and Esc hands focus back to the pane. ← past the leftmost pane lands in the docked sidebar, and → comes back out.

Collapsed

Point at the window's left edge and the sidebar floats in over the panes, close enough to click a row without docking anything. Move away and it goes. A window too narrow to dock into floats it rather than taking the space. While it is collapsed, the workspace or worktree name leads the tab bar.

Open one

Press P for the workspace picker. It lists Open in this window, Open Elsewhere in another, and Configured for the ones in your workspaces file. Typing ranks the rows inside each section, and a workspace appears once, in the first section that claims it.

Workspaces
Agents
Wants to run swift testzen-498-toast-queue · claude
Rewriting the pane testszen-term-website · claude
Editing QueueView.swiftzen-review · codex
Docs build, 3 of 9 pageszen-term-website · claude
Release notes page buildszen-term-website · claude
1 import AppKit​ 2 ​ 3 final class SidebarView: NSView {​ 4 private let workspaces: WorkspaceList​ 5 private let agents: AgentRoster​ 6 private let worktrees: WorktreeList​ 7 private var isCollapsed = false​ 8 ​ 9 static let expandedWidth: CGFloat = 240​ 10 static let railWidth: CGFloat = 72​ 11 ​ 12 func render(_ state: SidebarState) {​ 13 workspaceSection.render(state.workspaces)​ 14 agentSection.render(state.agents)​ 15 worktreeSection.render(state.hosts)​ 16 }​ 17 ​ 18 func jump(to agent: AgentID) {​ 19 guard let target = agents.locate(agent) else { return }​ 20 onSelectWorkspace(target.workspace)​ 21 onSelectTab(target.tab)​ 22 onFocusPane(target.pane)​ 23 }​ 24 }​ 25 ​ 26 ​ 27 ​ 28 ​ 29 ​ 30 ​ 31 ​ 32 ​ 33 ​ 34 ​ 35 ​ 36 ​ 37 ​
NORMALSidebarView.swiftswift19:9
~/Dev/zen-512-sidebar zen-512-sidebar​ ❯ swift build​ Building for debugging...​ [412/418] Compiling ZenTerm SidebarView.swift​ [417/418] Linking ZenTerm​ Build complete! (8.42s)​ ​ ~/Dev/zen-512-sidebar zen-512-sidebar​ ❯
~/Dev/zen-512-sidebar zen-512-sidebar​ ❯ git status -s​ M Sources/ZenTerm/WindowController.swift​ M Sources/ZenTerm/TabBarView.swift​ ?? Sources/ZenTerm/SidebarView.swift​ ?? Sources/ZenTerm/AgentRoster.swift​ ​ ~/Dev/zen-512-sidebar zen-512-sidebar​ ❯
1 editor
2 tests
3 logs
The ⌘P workspace picker

Return opens a configured workspace and switches to it, or switches to one that is already open. Choosing a workspace running in another window brings that window forward instead of starting a second copy of it, so its dev server keeps the port it has.

New Workspace leads the picker, on T. It opens a workspace with no entry in your workspaces file, starting in your home folder. Add Workspace… at the foot of the list writes an entry and opens it.

Each row in a Git repo shows its branch and what has changed: ⇡ and ⇣ count commits ahead of and behind the upstream, and + ~ » - ? ≠ count staged, modified, renamed, deleted, untracked and conflicted files.

A workspace does not have to be the root of the repo. Point one at a package inside a monorepo and it belongs to the repo around it, with that repo's branch, its counts and its worktrees. Your home folder is the one exception: it is never treated as a repo that encloses something, or dotfiles tracked at ~ would claim every folder under it.

The picker shows the workspaces you add and their Git worktrees. ZenTerm does not scan any directory for more.

Switch and close

1 through 9 select a workspace in sidebar order, and [ and ] step through them, wrapping at both ends. W closes the one you are in, and asks first when something is still running there.

Right-click a row for its menu, which holds New Worktree and Close Workspace.

Add a workspace

Open Settings with , and choose Workspaces, then Add workspace. Add Workspace… at the foot of the P picker opens the same form. Name the workspace and point it at a folder. Until you type a name, the form fills it in from the folder.

Last login: Fri Sep 19 09:24:07 on ttys006
~❯09:24
zen-term1editor2tests
Add Workspace
Workspace name *
web
Folder *
~/Dev/web
Tabs
Main pane
nvim
Right drawerOpens focused
claude
codeserverlazygit
Environment
Add variable
Copy into new worktrees
2 chosen
CancelAdd Workspace
The workspace form

Tabs draws the selected tab as its main pane, with the right drawer beside it and the bottom drawer under it. Type the command each one runs where it sits. A main pane with no command opens a shell. A closed drawer is a thin rail: click it to open the drawer and type its command, and leave the command empty to close it again.

The chips under the drawing are the workspace's tabs. + adds one. Double-click a chip, or press Return on it, to rename it, drag it to reorder, and its × removes it, with Undo in the footer to bring it back. A chip with no name shows its main command, and the tab it opens shows its live title. While the form is open, the tab shortcuts act on its tabs: T adds a tab, W removes one, and the move and select shortcuts work on the chips.

With the cursor in a pane or drawer, L makes it the place the cursor lands when the workspace opens. That region reads Opens focused, and a dot marks its tab's chip. A workspace has one.

⏎ adds the workspace. To change one later, choose its row in Settings → Workspaces. The same form opens with the workspace filled in, Save in place of Add Workspace, and Delete at the left of the footer.

The name is independent of the folder. A workspace called ZenTerm can point at ~/Dev/zen-term, and that name leads its sidebar row, its picker row and the window title while you are in it.

Put them in the order you want

The picker's Configured section lists your workspaces in the order their sections appear in the workspaces file. To change it, focus a row in Settings → Workspaces and press ↑ or ↓. Settings moves that whole section in the file for you, so moving it by hand gets you the same result.

One keystroke, the whole layout

Opening a workspace starts its tabs in order. In each tab the main pane starts its program and each drawer opens running its command. The cursor lands where you set it, and any environment variables you added reach every pane and drawer. An editor in the main pane and an agent in a drawer beside it arrive together, in one keystroke.

Work on a branch in a worktree

A Git worktree is a second checkout of the same Git repo in its own folder, on its own branch. ZenTerm lists the worktrees under their workspace, in the sidebar and the picker both, each with its folder and branch, including worktrees you made with git outside ZenTerm. Open one and you get the workspace's layout in that folder.

Create one

Select a workspace in the picker and press ⏎, or use the same chord on a focused sidebar row. Hovering a workspace row in the sidebar turns its branch into a + that does it too. Name a new branch, then choose where it starts: Default branch, or This checkout for the commit you are on. Type the name of a branch that already exists and ZenTerm checks that branch out instead. If your main checkout is on that branch, ZenTerm asks before it moves the main checkout to the default branch.

The worktree opens nested under the workspace it came from. It lands in ~/.zenterm/worktrees/, in a folder for the Git repo and one for the branch. A worktree is cut at the repo root and holds the whole tree, so a workspace pointing inside the repo opens its own folder in the new checkout, not the root. The chord works over a worktree row too, where This checkout means the commit that worktree is on.

Copy what it needs to run

A new worktree gets the files Git tracks and none of the files it ignores, so it has no .env and no node_modules until you install again. The workspace form has a Copy into new worktrees list of what Git ignores in the folder. Tick what a worktree needs, and ZenTerm copies it into each worktree you create. On APFS the copy shares disk space with the original, so a copied node_modules costs close to nothing.

Files land in the same folder the tab opens in, so a workspace inside a monorepo gets them in its own package rather than at the repo root. If the branch you started from has no such folder, the tab opens at the worktree root and nothing is copied; the notice says the entry needs a folder this worktree does not have.

The copied install comes from your main checkout. A branch that changed a lockfile still needs its package manager run in the worktree.

Remove one

Select a worktree row in the picker and press ⌫. Before anything is deleted, ZenTerm lists what goes with the folder: uncommitted files, commits no branch holds, the files it copied in, and the tabs open there, which close. The branch and its commits stay.

⏎ acts over the picker and a focused sidebar row, and ⌫ over the picker only. Anywhere else the keys reach the program in the pane.

Edit the file directly

Settings writes each workspace to ~/.config/zen-term/workspaces, one section per workspace. You can edit that file by hand, and the picker reads your changes the next time you open it. A tab line starts a tab, and the main, right, bottom and focus lines under it belong to that tab. tab = name pins the tab's title, and a bare tab shows its live title. The workspaces reference covers every field a section takes, including the drawer commands, the environment it injects, and the carry lines behind the copy list.