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:
@@ -556,4 +556,50 @@ describe("LaneConsolePane", () => {
|
||||
expect(api.lanes.action).not.toHaveBeenCalled();
|
||||
expect(api.run.start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attaches once the lane arrives on a cold load, and ignores another lane's run", async () => {
|
||||
vi.mocked(api.run.get).mockResolvedValue({
|
||||
id: "agenthub-lane-1-1",
|
||||
laneId: 1,
|
||||
slot: 1,
|
||||
status: "running",
|
||||
cwd: "/workspace/a",
|
||||
model: null,
|
||||
permissionMode: null,
|
||||
effort: null,
|
||||
resumeSessionId: null,
|
||||
sessionId: "s-9",
|
||||
startedAt: null,
|
||||
promptPreview: null,
|
||||
tool: "claude",
|
||||
});
|
||||
const req = { runId: "agenthub-lane-1-1", nonce: "k2" };
|
||||
const { rerender } = render(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane {...baseProps()} laneId={null} attachRequest={req} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
// The lane list lands only after the pane mounted (and after any early
|
||||
// attach would have resolved) — the switch effect must not win.
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane {...baseProps()} attachRequest={req} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await screen.findByTestId("terminal-view");
|
||||
expect(api.run.get).toHaveBeenCalledWith("agenthub-lane-1-1");
|
||||
|
||||
vi.mocked(api.run.get).mockClear();
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane
|
||||
{...baseProps()}
|
||||
attachRequest={{ runId: "agenthub-lane-12", nonce: "k3" }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(api.run.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user