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:
@@ -221,9 +221,13 @@ export function LaneConsolePane({
|
||||
}, [laneId, slot, refreshList]);
|
||||
|
||||
// Runs after the lane-switch effect above, so a sidebar pick that changes
|
||||
// lane AND run at once lands on the picked run, not the lane's own.
|
||||
// lane AND run at once lands on the picked run, not the lane's own. Keyed on
|
||||
// `laneId` too: on a cold load the pane mounts before the lane list does,
|
||||
// and the switch effect then resets the pane to its own slot once `laneId`
|
||||
// arrives — the request must land after that. Only this lane's own panes.
|
||||
useEffect(() => {
|
||||
if (!attachRequest) return;
|
||||
if (!attachRequest || laneId === null) return;
|
||||
if (!new RegExp(`^agenthub-lane-${laneId}(-[1-3])?$`).test(attachRequest.runId)) return;
|
||||
let live = true;
|
||||
autoAttachedForLane.current = laneId;
|
||||
api.run
|
||||
@@ -239,9 +243,10 @@ export function LaneConsolePane({
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
// Only a new request (nonce) attaches; a lane switch never replays one.
|
||||
// A new request (nonce), or the lane it names arriving, attaches; a switch
|
||||
// to another lane drops `?run=` (Workspace), so it never replays one.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [attachRequest?.nonce]);
|
||||
}, [attachRequest?.nonce, laneId]);
|
||||
|
||||
// Picking a pipeline template writes NOTHING into the prompt box: a prefilled
|
||||
// slash command (or the codex stage-checklist prose) is sent as soon as the
|
||||
|
||||
@@ -142,7 +142,7 @@ interface PromptEditorProps {
|
||||
fileCwd: string;
|
||||
}
|
||||
|
||||
function PromptEditor({
|
||||
export function PromptEditor({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
|
||||
@@ -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