feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces
The Workspace page's lane strip moves into the sidebar as a project tree: workspaces grouped by git repository (new GET /api/projects, cached in memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline rename, drag/Alt+arrow ordering and a right-click row menu. The selected workspace is the URL's ?lane=, one shared lane list feeds the tree, the new bottom status bar and the page. The Changes panel gains an activity bar (Files / Changes / Sessions, with resume), and split panes get a header.
This commit is contained in:
@@ -86,6 +86,10 @@ export interface LaneConsolePaneProps {
|
||||
hiddenRunIds?: string[];
|
||||
/** Reports the run this pane shows (null on the setup form). */
|
||||
onHandleChange?: (runId: string | null) => void;
|
||||
/** A session to resume in this pane, from the Workspace's Sessions tab. A new
|
||||
* `nonce` is a new request; it goes through the same path as a resume from
|
||||
* the run history, live-run fallback included. */
|
||||
resumeRequest?: { sessionId: string; model: string | null; nonce: number } | null;
|
||||
}
|
||||
|
||||
/** The tmux session a lane's split pane owns (see server/lib/pty-run.js). */
|
||||
@@ -109,6 +113,7 @@ export function LaneConsolePane({
|
||||
slot = 0,
|
||||
hiddenRunIds,
|
||||
onHandleChange,
|
||||
resumeRequest,
|
||||
}: LaneConsolePaneProps) {
|
||||
const { t } = useTranslation("run");
|
||||
const { t: tLanes } = useTranslation("lanes");
|
||||
@@ -455,6 +460,23 @@ export function LaneConsolePane({
|
||||
[busy, refreshList, t, lanes, slot, onLaneCreated, onLaneIdChange, attachToRun]
|
||||
);
|
||||
|
||||
const resumeRef = useRef(onResumeFromHistory);
|
||||
resumeRef.current = onResumeFromHistory;
|
||||
useEffect(() => {
|
||||
const lane = latest.current.lanes.find((l) => l.id === laneId);
|
||||
if (!resumeRequest || !lane) return;
|
||||
void resumeRef.current({
|
||||
session_id: resumeRequest.sessionId,
|
||||
cwd: lane.cwd,
|
||||
tool: lane.tool,
|
||||
model: resumeRequest.model,
|
||||
permission_mode: null,
|
||||
effort: null,
|
||||
} as DashboardRunHistoryItem);
|
||||
// Only a new request (nonce) resumes; a lane switch never replays one.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [resumeRequest?.nonce]);
|
||||
|
||||
const onViewFromHistory = useCallback(
|
||||
(item: DashboardRunHistoryItem) => {
|
||||
if (item.session_id) void onResumeFromHistory(item);
|
||||
|
||||
@@ -483,4 +483,47 @@ describe("LaneConsolePane", () => {
|
||||
expect(screen.queryByText("shown")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("resumes a session handed over by the Sessions tab, once per request", async () => {
|
||||
(api.lanes.action as ReturnType<typeof vi.fn>).mockResolvedValue({ run_id: "agenthub-lane-1" });
|
||||
(api.run.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "agenthub-lane-1",
|
||||
laneId: 1,
|
||||
status: "running",
|
||||
cwd: "/workspace/a",
|
||||
model: null,
|
||||
permissionMode: null,
|
||||
effort: null,
|
||||
resumeSessionId: "s-1",
|
||||
sessionId: null,
|
||||
startedAt: null,
|
||||
promptPreview: null,
|
||||
});
|
||||
const request = { sessionId: "s-1", model: "claude-opus-5-5", nonce: 1 };
|
||||
const { rerender } = render(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane {...baseProps()} resumeRequest={request} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(
|
||||
1,
|
||||
"start",
|
||||
expect.objectContaining({
|
||||
slot: 0,
|
||||
prompt: "",
|
||||
resumeSessionId: "s-1",
|
||||
model: "claude-opus-5-5",
|
||||
})
|
||||
)
|
||||
);
|
||||
// A re-render with the same request is not a second resume.
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane {...baseProps()} resumeRequest={{ ...request }} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await screen.findByTestId("terminal-view");
|
||||
expect(api.lanes.action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user