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