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
+48 -3
View File
@@ -22,9 +22,10 @@
* Spawns `claude`, `codex` or `agy` (Antigravity) in the tmux pane based on
* the tool parameter.
*
* A lane can hold up to four sessions, one per Workspace split pane: pane 0 is
* `agenthub-lane-<laneId>` (the lane's own `run_id`), panes 1–3 are
* `agenthub-lane-<laneId>-<slot>`.
* A lane can hold up to four sessions, one per Workspace tab: slot 0 is
* `agenthub-lane-<laneId>` (the lane's own `run_id`), slots 1–3 are
* `agenthub-lane-<laneId>-<slot>`. A slot holds an AI tool (`spawnRun`) or a
* plain shell (`spawnShell`, a Terminal tab).
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -498,6 +499,49 @@ function spawnRun(args) {
return getRun(id);
}
/**
* A Workspace "Terminal" tab: the user's own default shell (tmux's, run with
* nothing after `--`) in a lane's pane slot. No AI tool, no prompt, nothing
* from the request but the slot — the cwd is the lane's. Recorded as tool
* `shell` so the tab can say what it is. The caller has already made sure the
* slot holds no live session.
*/
function spawnShell({ laneId, slot = 0, cwd }) {
if (!Number.isInteger(laneId)) throw makeErr("EBADLANE", "laneId must be an integer");
if (!Number.isInteger(slot) || slot < 0 || slot > MAX_PANE_SLOT) {
throw makeErr("EBADSLOT", `slot must be an integer 0–${MAX_PANE_SLOT}`);
}
if (!tmux.isTmuxAvailable()) throw makeErr("ETMUXMISSING", "tmux is not installed");
const id = runIdForLane(laneId, slot);
const env = {
AGENTHUB_LANE_ID: String(laneId),
AGENTHUB_RUN_ID: id,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
...desktopEnv(),
};
tmux.newSession({ name: id, cwd, argv: [], env });
dashboardRuns?.recordRun({
id,
sessionId: null,
mode: "interactive",
cwd,
tool: "shell",
model: null,
permissionMode: null,
effort: null,
resumeSessionId: null,
worktree: false,
router: null,
prompt: "",
status: "running",
startedAt: Date.now(),
endedAt: null,
exitCode: null,
laneId,
});
return getRun(id);
}
// Only a dashboard lane pane is a run. Without this, `DELETE /api/run/main`
// would kill the user's own tmux session named `main`.
function killRun(id) {
@@ -544,6 +588,7 @@ function listRuns() {
module.exports = {
spawnRun,
spawnShell,
killRun,
getRun,
listRuns,