feat(ui): sidebar lists workspaces with their sessions, Claude Desktop style

The sidebar's project tree becomes a flat workspace list (WorkspaceTree):
one row per workspace in the stored order, each expanding to its sessions.
A session that still has a live pane opens /workspace?lane=<id>&run=<runId>,
which only attaches pane 0 to that tmux session; any other session opens
its read-only transcript. Nothing in the sidebar starts or resumes a run.
This commit is contained in:
2026-10-01 22:23:01 +07:00
parent 744e9c6cda
commit 2e051e23b6
16 changed files with 616 additions and 538 deletions
@@ -90,6 +90,9 @@ export interface LaneConsolePaneProps {
* `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;
/** A live run to show in this pane (a session picked in the sidebar). It
* attaches to that tmux session only — nothing is started or resumed. */
attachRequest?: { runId: string; nonce: string } | null;
}
/** The tmux session a lane's split pane owns (see server/lib/pty-run.js). */
@@ -114,6 +117,7 @@ export function LaneConsolePane({
hiddenRunIds,
onHandleChange,
resumeRequest,
attachRequest,
}: LaneConsolePaneProps) {
const { t } = useTranslation("run");
const { t: tLanes } = useTranslation("lanes");
@@ -216,6 +220,29 @@ export function LaneConsolePane({
refreshList();
}, [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.
useEffect(() => {
if (!attachRequest) return;
let live = true;
autoAttachedForLane.current = laneId;
api.run
.get(attachRequest.runId)
.then((h) => live && setHandle(h))
.catch(
(err: unknown) =>
live &&
setError(
t("errors.attachFailed", { message: err instanceof Error ? err.message : "unknown" })
)
);
return () => {
live = false;
};
// Only a new request (nonce) attaches; a lane switch never replays one.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [attachRequest?.nonce]);
// 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
// run starts, which is an extra prompt nobody asked for. The prompt is the