feat(ui): a Claude-Desktop-style home for every workspace

/workspace?lane=<id> opens on the workspace's home (WorkspaceHome): a prompt
box whose Start opens a new session in the first free pane slot, Recents (a
live session attaches its pane, any other opens its transcript, nothing
resumes), and Instructions / Memory / Context / Folder. Instructions live in
lanes.instructions, extra folders and uploaded files in the new lane_sources
table (copies under <data dir>/workspace-context/<lane id>/); spawnRun reads
them from the lane's rows and hands Claude --add-dir + an appended system
prompt file, Codex --add-dir + developer_instructions. Memory is read-only.

Every dashboard pane carries AGENTHUB_RUN_ID; hooks forward it and the run
records which session the pane holds (a Claude pane follows /clear and
/resume but not a nested claude -p), so the sidebar and notifications open
the right pane (&run=). Settings loses its Projects group; Worktrees now
decides which worktrees Add folder suggests.
This commit is contained in:
2026-10-02 01:10:58 +07:00
parent 2e051e23b6
commit d0535af626
44 changed files with 2515 additions and 466 deletions
+25
View File
@@ -16,6 +16,7 @@ const { ingestWorkflowsForSession } = require("../lib/workflow-ingest");
// `liveness.probeLiveCwds` and the watchdog picks the stub up at call time.
const liveness = require("../lib/session-liveness");
const lanesLib = require("../lib/lanes");
const dashboardRuns = require("../lib/dashboard-runs");
const { broadcastLane } = require("./lanes");
const { detect } = require("../lib/stage-detect");
const { getPipeline } = require("../lib/pipelines");
@@ -1293,6 +1294,29 @@ function antigravityHookInput(hookType, data) {
const HOOK_TOOLS = { codex: codexHookInput, antigravity: antigravityHookInput };
/**
* Record which session a dashboard pane holds. The pane's CLI inherits
* `AGENTHUB_RUN_ID` (pty-run.js) and the hook handler forwards it as `run_id`;
* without this a run's `session_id` stays whatever it was started with (null
* for a new session), and the sidebar could not tell that a session is live in
* a pane. Follows `/clear` too, which starts a new session in the same pane.
* Only a dashboard pane name is accepted, and only a run that exists. Once
* bound, only a SessionStart that is not a fresh `startup` (a `/clear`, a
* `/resume`) moves it: a `claude -p` the agent runs through Bash inherits
* AGENTHUB_RUN_ID too, and its hooks must not take the pane over. That rule
* is Claude's only: for Codex and agy it is not verified that a new session in
* the pane and a nested one can be told apart, so their panes follow every one.
*/
function bindRunSession(runId, sessionId, hookType, source, tool) {
if (typeof runId !== "string" || !/^agenthub-lane-\d+(-[1-3])?$/.test(runId)) return;
if (typeof sessionId !== "string" || !sessionId) return;
const row = dashboardRuns.getRun(runId);
if (!row || row.session_id === sessionId) return;
const reopened = hookType === "SessionStart" && source !== "startup";
if (tool === "claude" && row.session_id && !reopened) return;
dashboardRuns.patchRun({ id: runId, sessionId });
}
router.post("/event", (req, res) => {
let { hook_type, data } = req.body;
if (!hook_type || !data) {
@@ -1334,6 +1358,7 @@ router.post("/event", (req, res) => {
// ponytail: full lane scan per hook — fine at dozens of lanes; index by cwd prefix if lane counts grow.
touchLaneFromHook(hook_type, data, tool);
bindRunSession(req.body.run_id, data.session_id, hook_type, data.source, tool);
res.json({ ok: true, event: result });