feat: split-screen multi-task view (finished, reconciled with the #263 viewer model) - #276
Open
kovtcharov wants to merge 3 commits into
Open
kovtcharov wants to merge 3 commits into
kovtcharov wants to merge 3 commits into
Conversation
…ask set) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odel Completes the inherited split-screen snapshot and reconciles it with the multi-client viewer model (#263), which assumed one terminal per client. Viewer model (#263) x split screen - ViewerRegistry: a client that declares its panes with task:setVisible keeps ownership of every pane and is counted as a viewer of each. Previously mounting pane B released pane A, and a second client's bare resize on A claimed it, shrinking a terminal the desktop was actively showing. Clients that never declare a set keep the single-focus behaviour. - server: task:setVisible feeds the registry and re-broadcasts task:viewers. - TerminalView: regaining ownership now re-sends the terminal size explicitly; the small-change filter compared against the pre-follow size and swallowed it, leaving the PTY at the other client's dimensions. Split-screen fixes found in browser verification - Reload wiped every pane: the prune-missing-tasks effect ran before init delivered the task list. Gated on a new taskStore.hasTaskList flag. - A pane grown by a divider drag stopped following output; the refit now keeps a tailing terminal pinned (DOM + buffer bottom check, short pin window). - Narrow panes clipped the split/close controls and collapsed the title; the pane is now a size container and the header compacts on the pane's width. - OpenCode adapters no longer make setTaskActive exclusive, which silenced all but one OpenCode pane. Tests: ViewerRegistry split cases, WS split-ownership case, TerminalView regain/refit/pin cases, App reload case; SplitContainer mock typing fix.
…t test After #261 every WebSocket upgrade needs the API token. - test-cli bootstrapped its token from CLAUDIA_BACKEND_URL or :4001 before argv was parsed, ignoring --url. `--url http://127.0.0.1:4811` therefore fetched a DIFFERENT instance's token and presented it to :4811, which refused the upgrade (401). The bootstrap now asks the backend named by --url, falling back to CLAUDIA_BACKEND_URL, then the default. - ws-visible-tasks.test.ts presents getAuthToken(base), and gains a case proving an untokened socket cannot reach task:setVisible at all.
kovtcharov
force-pushed
the
feat/split-screen-multi-task-view
branch
from
September 11, 2026 02:22
8986e62 to
d733507
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VSCode-style split screen for task terminals: split a pane right or down, open a different task in each pane, drag the dividers, and every pane streams its own live terminal at once. The default layout is still a single pane, and it looks exactly as it did before. The layout persists across reloads.
This PR lands the split-screen work built in August (tasks #67–#72), which was never PR'd. The first commit is that snapshot, rebased onto current
main. The second commit finishes it: it reconciles the feature with the multi-client viewer model from #263 and fixes the bugs that only showed up in a real browser.What it does
Ctrl/Cmd+\) or down (Ctrl/Cmd+Shift+\) from each pane's header. Close a pane withCtrl/Cmd+Alt+W; the task keeps running.Ctrl/Cmd+Alt+←/→cycles focus. The limit is 6 panes.task:setVisible, and the server streams the union across clients. Each client is capped at 8 tasks, and a client's set is released when it disconnects. A phone's one-task declaration can't blank a desktop's panes.--test-split <ids>asserts that N visible tasks all stream concurrently.Inherited vs. finished
State I found. Task #67's last session reported the work complete on unit tests: 24/24 backend and 126/126 frontend split tests, with #68 (backend set) and #72 (remount fix) integrated. It had never been shown in a browser (the session said so explicitly), never been rebased or PR'd, and it predates #263. I diffed #70 (
claudia/task-587e65ae) and #71 (claudia/task-c943b7da) against the integration branch. Both are superseded: #71's layout tests exist in expanded form, and #70's single-set backend was replaced by the per-client union. Nothing unique was lost.What I took "complete" to mean:
mainand coexists correctly with feat(viewers): per-task terminal ownership so multiple clients stop fighting over PTY size #263's per-task terminal ownership.Finished in this PR (commit 2):
Viewer model (#263) vs. split screen. #263 assumed one terminal per client:
ViewerRegistry.focus()released the task a client had been looking at. With panes, mounting pane B released pane A. A second client's plain resize could then claim A and shrink the PTY under a desktop that was actively showing it. Pane A also reported 0 viewers.task:setVisiblenow keeps ownership of every pane and counts as a viewer of each.setVisibleomits it (the pane closed or switched task), or when the client disconnects.setVisibleinto the registry and re-broadcaststask:viewers.Bugs found by browser verification:
inithad delivered the task list. It's now gated on a newtaskStore.hasTaskListflag. Deleting the last task still clears its pane.setTaskActivewas still exclusive, which would silence all but one OpenCode pane. It's now additive, and TaskSpawner tells the adapter when a task leaves the visible set.Tests added:
Mocktyping fix in the SplitContainer test.How it interacts with the #263 viewer model
Ownership is still per task and exclusive, and only the owner's
task:resizereaches the PTY. What changed is the unit of "what a client is showing": it's now the set a split-aware client declares, not only its last focus.In practice:
Both of these were verified in the browser (screenshots 06 and 07 below).
Rebase conflicts and resolutions
The snapshot's own work was replayed with
git rebase --onto origin/main d37a6c1.Conflicts:
server.ts, WebSocket close handler: kept bothviewers.dropClient()(feat(viewers): per-task terminal ownership so multiple clients stop fighting over PTY size #263) andtaskSpawner.releaseClient().task-spawner.ts:chooseParentLinknext toMAX_VISIBLE_TASKSsetTaskActivederivesisActivefrom the visible set, using main'slogger.debugremoveVisibleTask()and then main'skillTaskTree()test-cli.tshelp text: kept both option groups.App.tsx,TerminalView.tsx(paneControlsalongside #263's ownership code),useWebSocket.ts,WorkspacePanel.tsxandshared/src/index.tsauto-merged. I then reviewed each by hand. A second rebase onto the latestmain(#256, #259, #272, #275) was clean.Verification
Unit tests (local, Windows, on the final rebase onto
mainat #249):tsc --noEmitis clean for backend, frontend and electron.Coverage:
Browser verification. I ran a fully sandboxed stack on Windows. The #254 Playwright harness isn't on
main: it relies on a bash fake CLI and onVITE_CLAUDIA_BACKEND_PORT, whichmaindoesn't have. So I used the same approach, adapted:node backend/dist/index.js) on :4811.CLAUDIA_DATA_DIR,HOME,USERPROFILEandAPPDATAall pointed under~/.claudia-split-e2e, andCLAUDIA_SHARED_MCP=0.%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\cli.js). It prints a ticker with its real PTY size and aSIGWINCH -> CxRline on every resize. That way each pane's rendered text proves which task it shows and what size that task's PTY actually is.4001rewritten to4811and then asserted absent.--requireguard in the sandbox's Node processes refused any outbound socket to 4001/5173. It logged zero attempts. This matters because several backend paths hardcodelocalhost:4001.53/53 checks passed on the final rebased build:
task:setVisibledeclares bothScreenshots (fake tasks in the sandbox):
Three panes: ALPHA | BRAVO over CHARLIE. Each pane streams its own task, and each task's reported PTY size matches its pane.

After both divider drags: each PTY was resized independently. The narrow ALPHA pane keeps its title and controls.

A second client took ALPHA. The desktop's ALPHA pane shows the #263 "viewing at 38×27" follower badge, while BRAVO and CHARLIE stay owned.

After a reload, the same layout comes back and all panes are live.

More: two panes · input routing · second client's view · after closing a pane · AuthTokenGate (loopback grant refused). All images were re-taken on the auth-on run. The images live on the
pr-assets/split-screen-multi-task-viewbranch, which contains screenshots only and can be deleted after merge.Known limitations / follow-ups
MAX_PANES), with a backend cap of 8 per clienttask:selectrequests. Verified with 3 panes of light output; CPU with 6 busy panes hasn't been measured.e2e/testsonce test: Playwright browser E2E suite on a fully sandboxed stack #254 lands.llm-service, the learnings embeddings and the tunnel manager hardcodelocalhost:4001regardless ofCLAUDIA_BACKEND_PORT. A sandboxed or second instance can reach the live one through them. I found this while sandboxing.Test plan
npm test -w backend/npm test -w frontend, plustsc --noEmitfor bothnpm run coverage: new-file floor ✓ (the ratchet is evaluated on Linux CI)