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:
@@ -44,7 +44,7 @@
|
||||
* ----------------------------------------------------------------------------- */
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useLocation, useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Plus,
|
||||
@@ -231,6 +231,7 @@ function ConsoleArea({
|
||||
onLaneIdChange,
|
||||
showToggle = true,
|
||||
resumeRequest,
|
||||
attachRequest,
|
||||
}: {
|
||||
lanes: Lane[];
|
||||
selectedLaneId: number | null;
|
||||
@@ -248,6 +249,8 @@ function ConsoleArea({
|
||||
showToggle?: boolean;
|
||||
/** A session to resume in slot 0, from the Sessions tab. */
|
||||
resumeRequest: { sessionId: string; model: string | null; nonce: number } | null;
|
||||
/** A live run to attach slot 0 to, from the sidebar (`?run=`). */
|
||||
attachRequest: { runId: string; nonce: string } | null;
|
||||
}) {
|
||||
// Every split pane shows the SELECTED lane, each in its own tmux session
|
||||
// (slot = pane index). Tracking what each pane shows lets the others leave
|
||||
@@ -292,6 +295,7 @@ function ConsoleArea({
|
||||
defaultCwd={defaultCwd}
|
||||
onHasActiveRunChange={onHasActiveRunChange}
|
||||
resumeRequest={resumeRequest}
|
||||
attachRequest={attachRequest}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@@ -323,6 +327,7 @@ function ConsoleArea({
|
||||
wsConnected={wsConnected}
|
||||
defaultCwd={defaultCwd}
|
||||
resumeRequest={i === 0 ? resumeRequest : null}
|
||||
attachRequest={i === 0 ? attachRequest : null}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -342,6 +347,14 @@ export function Workspace() {
|
||||
// notification click), falling back to the last one shown, then the first.
|
||||
const { lanes, loaded: lanesLoaded, refresh: refreshLanes } = useLanes();
|
||||
const laneParam = searchParams.get("lane");
|
||||
// `?run=` — a live session picked in the sidebar; the location key makes a
|
||||
// second pick of the same session a new request.
|
||||
const location = useLocation();
|
||||
const runParam = searchParams.get("run");
|
||||
const attachRequest = useMemo(
|
||||
() => (runParam ? { runId: runParam, nonce: location.key } : null),
|
||||
[runParam, location.key]
|
||||
);
|
||||
const paramId = laneParam === null ? null : Number(laneParam);
|
||||
const fallbackId = (() => {
|
||||
if (!lanesLoaded) return null;
|
||||
@@ -948,6 +961,7 @@ export function Workspace() {
|
||||
onLaneIdChange={setSelectedLaneId}
|
||||
showToggle={!currentLane}
|
||||
resumeRequest={resumeRequest}
|
||||
attachRequest={attachRequest}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user