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
@@ -145,6 +145,34 @@ describe("useNotifications", () => {
vi.restoreAllMocks();
});
it("links a notification to the pane that holds the session, in any slot", async () => {
fetchMock.mockImplementation(async (url: string) => ({
ok: true,
json: async () =>
url === "/api/push/send"
? { ok: true, native: true, pushed: 1 }
: url === "/api/run"
? {
items: [
{ id: "agenthub-lane-4-2", laneId: 4, status: "running", sessionId: "s-7" },
],
}
: { lanes: [], counts: {} },
}));
enable();
renderHook(() => useNotifications());
eventBus.publish({
type: "new_event",
timestamp: new Date().toISOString(),
data: { event_type: "Stop", summary: "Done", session_id: "s-7" } as DashboardEvent,
});
await vi.waitFor(() => expect(sentTitles()).toHaveLength(1));
const push = fetchMock.mock.calls.find((c) => c[0] === "/api/push/send")!;
expect(JSON.parse((push[1] as RequestInit).body as string).url).toBe(
"/workspace?lane=4&run=agenthub-lane-4-2"
);
});
it("stays silent while notifications are disabled", async () => {
renderHook(() => useNotifications());