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
+19 -2
View File
@@ -357,7 +357,20 @@ function setProvisioningFacts(id, facts = {}) {
}
function deleteLane(id) {
return db.prepare("DELETE FROM lanes WHERE id = ?").run(id).changes > 0;
const gone = db.prepare("DELETE FROM lanes WHERE id = ?").run(id).changes > 0;
// Its `lane_sources` rows cascade; AgentHub's own copies of its uploaded
// context files go with them. A folder source is the user's and stays.
// Lazy: lane-sources.js reads the db directly and must not load at import.
// The row is already gone: a copy that will not delete is logged, not a
// failed remove.
if (gone) {
try {
require("./lane-sources").removeLaneContext(id);
} catch (err) {
console.warn(`[lanes] workspace ${id}: context files not removed:`, err.message);
}
}
return gone;
}
/** Slots currently held by a lane, ascending. Input to the slot allocator. */
@@ -755,8 +768,12 @@ function lanePayload(lane, ageSec = null) {
const inferredIdx = inferred ? phaseIdx(pipeline, inferred) : -1;
const view = inferredIdx === -1 ? lane : { ...lane, stage: inferred };
const nodes = nodeStates(pipeline, view);
// Up to 16 KB per lane, on every list and broadcast; the home reads it from
// `GET /api/lanes/:id/sources` instead.
const row = { ...lane };
delete row.instructions;
return {
...lane,
...row,
pipeline_name: pipeline.name,
pipeline_skill: pipeline.skill || "",
pipeline_nodes: