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 -7
View File
@@ -84,8 +84,19 @@ export function useNotifications() {
.then((r) => (lanesRef.current = r.lanes))
.catch(() => {});
function laneUrlForSession(sessionId: string): string | undefined {
const lane = lanesRef.current.find((l) => l.session_id === sessionId);
// Straight into the pane that holds the session (any slot, from the
// hooks' `run_id` binding) — the agent waiting on the user is there, not
// on the workspace's home; the home when no pane holds it.
async function laneUrlForSession(sessionId: string): Promise<string | undefined> {
const runs = await api.run
.list()
.then((r) => r.items ?? [])
.catch(() => []);
const run = runs.find((r) => r.sessionId === sessionId && r.status === "running");
if (run && run.laneId !== null) {
return `/workspace?lane=${run.laneId}&run=${encodeURIComponent(run.id)}`;
}
const lane = (lanesRef.current ?? []).find((l) => l.session_id === sessionId);
return lane ? `/workspace?lane=${lane.id}` : undefined;
}
@@ -106,11 +117,18 @@ export function useNotifications() {
if (!prefs.enabled) return;
// A notification that was shown (pushed, native or local) plays its
// type's sound; one that reached nobody plays nothing.
const show = (type: NotifType, title: string, body: string, url?: string) =>
void notify(title, body, url).then((r) => {
if (r.via === "native" || r.via === "push" || r.via === "local")
playSound(soundFor(prefs, type));
});
const show = (
type: NotifType,
title: string,
body: string,
url?: Promise<string | undefined>
) =>
void Promise.resolve(url)
.then((u) => notify(title, body, u))
.then((r) => {
if (r.via === "native" || r.via === "push" || r.via === "local")
playSound(soundFor(prefs, type));
});
switch (msg.type) {
case "session_created": {