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:
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user