---
title: "Projects on the left, sessions on top"
slug: projects-on-the-left-sessions-on-top
summary: "Running several coding agents across several repos, I ended up with one layout: projects as a vertical list on the left, and the sessions of the selected project as tabs on top. I built it in Teerminal, then ported the same idea to iTerm2 as a draft pull request. Here is why I think it's a good way to organize harness UX, and what the port got wrong first."
tags: ai, agents, ux, terminal, teerminal, iterm2, coding-agents
ai_model: claude-sonnet-5-5
published_at: 2026-10-03T04:16:58.01+00:00
canonical_url: https://jarkkolietolahti.com/articles/projects-on-the-left-sessions-on-top
markdown_url: https://cemecdwfdvdbvpvowuln.supabase.co/functions/v1/content-md?slug=projects-on-the-left-sessions-on-top
---

Most terminals organize work as a flat row of tabs. That worked when a tab was a shell. It works worse when a tab is a Claude Code conversation, another is Codex, a third is a plain shell in the same repo, and two more are agents in a different repo that I started yesterday and haven't looked at since.

The layout I've ended up with is simple. **Projects go in a vertical list on the left. The sessions of the selected project go on top as tabs.** Pick a project on the left and the tab bar shows only that project's shells, chats and harnesses.

I built this in my own terminal, Teerminal. Then I ported the same idea to iTerm2 and opened it upstream as a draft pull request: [gnachman/iTerm2#762](https://github.com/gnachman/iTerm2/pull/762), "Add project sidebar and tabs for machine-wide coding harnesses". This post is about why I think the layout is right, and about the parts of the port that did not go smoothly.

## A project is the stable thing, a session is not

A coding-agent session is short-lived and replaceable. I start a new conversation, switch from one agent to another, or open a shell to check something. The project directory is what stays put.

A flat tab row mixes those two levels. The thing I want to remember (where does the work on this repo live?) is placed in the same row as the things that change constantly (which conversation is this?). Every new session reshuffles the row.

Splitting them gives each level its own direction and its own job. When I change project, I look left. When I change conversation inside the same project, I look at the top. Opening a new shell or switching agents doesn't move anything on the left.

## The rule that decided the details: don't move things I just learned

Teerminal's guidance file for contributors says the core UX goal is helping users build spatial memory. That sounds abstract until a specific bug makes it concrete.

During the earlier experiments the left panel had six modes: Working Set, All Projects, All Sessions, Work Map, Navigator and Active. I wrote about that in an earlier post. In one of them, clicking a directory moved it up a couple of rows in the list. It annoyed me immediately. I had already learned where the directory was, and selecting it changed the order. Next time I had to look for it again.

The rule I took from that is boring and strict: focus, activity, titles and status must not reshuffle rows. Project order is manual and predictable. Colour and labels help me recognize a row, but they don't decide where it sits.

A tab bar that lists every session on the machine can't make that promise. The tab you want is somewhere in a row that changes every time anything starts or stops.

## Closing a view is not stopping a process

Teerminal runs direct sessions inside a private tmux server, so the process outlives its view. Closing a tab closes the view and the process keeps running; the session moves to a detached list in the sidebar. Stopping the process is a separate, confirmed action.

That matters for this layout. If a project's tabs are only a view onto work, then switching projects can't be a destructive act. Everything keeps running and I can come back to it.

## Porting it to iTerm2

The iTerm2 version is opt-in. You enable it in Advanced Settings by searching for "harnesses". It groups coding harnesses on the machine by directory in a left sidebar. Selecting a directory filters the existing tab bar to that project's tabs; **All projects** brings the full bar back.

Some of the details of the port, taken from the pull request:

- **Discovery is machine-wide.** It finds this instance's sessions, native Codex, Claude, Antigravity and similar processes owned by the current user, and tmux-backed harnesses, including ones that are detached or attached in another terminal. When a launcher clears `TMUX` and `TMUX_PANE`, it asks the ancestor tmux server for its pane inventory over the UNIX socket and matches pane PIDs.
- **Filtering hides, it doesn't remove.** The project filter hides tab cells and group chips without closing tabs or changing saved tab-group collapse state. A tab with mixed-directory split panes appears under each project it touches.
- **Shortcuts follow the two levels.** Command-Option-1 through 9 selects a project. Command-1 through 9 and tab cycling stay inside the selected project, and the numbers restart at 1 per project.
- **Transfer is deliberately cautious.** Clicking an external Codex, Claude or Antigravity run offers to move it into a tmux tab or a native tab. The identity of the conversation comes from process-linked transcripts or Claude's PID records, never from guessing the latest history entry. If the identity is ambiguous, the transfer is refused. After saving a recovery record it sends SIGTERM only to the original harness and waits up to 15 seconds for it to exit. It never force-kills.

That last item is the opposite of the "close a view" model above. Moving a running agent between terminals really does end the original process, so the dialog says so and asks you to do it only when the agent is idle.

## What went wrong along the way

The pull request's own commit history shows where the layout fought the host application. Two examples:

- On Tahoe, hidden tabs were still being drawn by the tab bar. The fix was a commit named "Exclude filtered tabs from Tahoe tab bar drawing".
- In the first combined test run, two tab-group drag-and-drop tests failed. Both passed when run alone, and the complete rerun of 102 tests passed. I don't know whether that was a real ordering problem or flakiness in those tests. I'm reporting it as it happened.

I can also list what is not done. The PR text says interactive visual verification and real harness-resume verification were still pending, and project selection is transient: it is rebuilt by discovery when the sidebar is recreated. As I write this the pull request is a draft and GitHub reports it as conflicting with master. It is a proposal, and I don't know whether it will be merged.

## What I'd keep from this

I don't think the specific controls matter much. What I'd keep is the separation: a stable list of places on one axis, a changing list of conversations on the other, and a promise that selecting something doesn't rearrange what I just learned.

I use this layout for somewhere between two and nine hours a day, and I don't yet know how it holds up with many more projects than I have now. The shape of the problem, though, doesn't depend on which terminal hosts it. Any harness UX that treats every session as a peer will eventually have this tab-row problem.

If you want to read the iTerm2 side, the diff is 36 files and about 3,300 added lines: [gnachman/iTerm2#762](https://github.com/gnachman/iTerm2/pull/762). The earlier Finnish post on how the Teerminal version came about is [Teerminal: vaikein osa oli vasen paneeli](https://jarkkolietolahti.com/articles/teerminal-vaikein-osa-oli-vasen-paneeli).

