feat(workspace): session view console as tabs, with New terminal

The 1/2/4 split view becomes a tab bar (ConsoleTabs.tsx): one tab per live
tmux slot (0-3) of the workspace, the active one being ?run=, every live tab's
pane kept mounted and hidden with CSS. The + menu opens a new tab in the first
free slot: New terminal (a plain shell - shell: true on POST
/api/lanes/:id/start, spawnShell, tool 'shell', no run_id/status change), any
installed enabled AI tool started bare, or Settings -> Agents. A tab's x stops
its session. splitViewStorage, &pane= and the sidebar's Ctrl/Cmd+click are gone.
This commit is contained in:
2026-10-02 18:09:31 +07:00
parent fadecaddd0
commit d9511ea446
21 changed files with 611 additions and 617 deletions
+25
View File
@@ -1785,6 +1785,31 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
error: { code: "EBADSLOT", message: `slot must be an integer 0–${runs.MAX_PANE_SLOT}` },
});
}
// A Terminal tab: a plain shell in the slot. No agent is at work in
// it, so the lane's run_id/status stay as they are.
if (body.shell === true) {
const shellOutcome = await withLaneLock(lane.id, async () => {
const current = lanesLib.getLane(lane.id);
if (!current) return { missing: true };
const id = runs.runIdForLane(current.id, slot);
if (runs.getRun(id)?.status === "running") return { conflict: true };
startedRunId = runs.spawnShell({ laneId: current.id, slot, cwd: current.cwd }).id;
return {};
});
if (shellOutcome.missing) {
return res
.status(404)
.json({ error: { code: "ENOLANE", message: "workspace not found" } });
}
if (shellOutcome.conflict) {
return res
.status(409)
.json({
error: { code: "ERUNLIVE", message: "that tab already has a live session" },
});
}
break;
}
// A session that ran in a worktree of its own resumes (or forks) THERE:
// Claude finds a session by the folder it ran in. Resolved from the
// session row, before the lock; anything else starts as it always did.