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:
@@ -8,7 +8,9 @@
|
||||
* file — one file, as a diff or as its whole content (issue #35); a
|
||||
* whole markdown file can also be read rendered (Preview)
|
||||
* history — recent commits; opening one shows that commit's diff
|
||||
* browse — quick-open by path, or the repo's tracked file tree
|
||||
* browse — quick-open by path, or the repo's tracked file tree (the
|
||||
* Workspace right panel's Files tab; the other four are its
|
||||
* Changes tab)
|
||||
*
|
||||
* and three ranges: `work` (uncommitted, what the agent has touched right
|
||||
* now), `branch` (`base...HEAD`, what this lane adds on top of its base) and
|
||||
@@ -417,8 +419,13 @@ export function LaneChangesPanel({
|
||||
laneId,
|
||||
onNewLane,
|
||||
full = false,
|
||||
tab = "changes",
|
||||
}: {
|
||||
laneId: number;
|
||||
/** The Workspace right panel's activity-bar tab: `files` is the browse mode
|
||||
* on its own (no git header, no mode switcher), `changes` the other four.
|
||||
* At full width both show the explorer sidebar beside the changes. */
|
||||
tab?: "files" | "changes";
|
||||
/** Open the Add-lane flow for a sibling worktree; see `LaneGitBar`. */
|
||||
onNewLane?: (branch?: string) => void;
|
||||
/** The panel has the whole width: the file tree becomes a sidebar and a
|
||||
@@ -582,16 +589,19 @@ export function LaneChangesPanel({
|
||||
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||
}, [mode, laneId, reread]);
|
||||
|
||||
// Re-run when a lane switch empties the cache too: the Files tab stays in
|
||||
// `browse` across the switch, so the mode alone never changes.
|
||||
const treeRootLoaded = "" in treeChildren;
|
||||
useEffect(() => {
|
||||
const wanted = full || (mode === "browse" && !query.trim());
|
||||
if (!wanted || "" in treeChildren) return;
|
||||
if (!wanted || treeRootLoaded) return;
|
||||
void api.lanes
|
||||
.gitTree(laneId, "")
|
||||
.then(forLane(laneId, (r) => setTreeChildren((prev) => ({ ...prev, "": r.entries }))))
|
||||
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||
// `treeChildren` is the cache this fills — reading it as a dep would loop.
|
||||
// `treeChildren` itself is the cache this fills — a dep on it would loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode, laneId, query, reread, full]);
|
||||
}, [mode, laneId, query, reread, full, treeRootLoaded]);
|
||||
|
||||
// A folder's children are fetched lazily, the first time it is expanded.
|
||||
const toggleDir = (path: string) => {
|
||||
@@ -706,6 +716,11 @@ export function LaneChangesPanel({
|
||||
returnTo.current = null;
|
||||
pendingScroll.current = null;
|
||||
}, [laneId]);
|
||||
// After the lane reset above: each tab opens on its own mode.
|
||||
useEffect(() => {
|
||||
if (!full) setMode(tab === "files" ? "browse" : "overview");
|
||||
}, [tab, laneId, full]);
|
||||
const filesTab = tab === "files" && !full;
|
||||
|
||||
// Restoring has to wait for the list to be TALL enough again: leaving a file
|
||||
// re-fetches the mode it came from, so the first render back is an empty
|
||||
@@ -1032,7 +1047,9 @@ export function LaneChangesPanel({
|
||||
</aside>
|
||||
)}
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<LaneGitHeader laneId={laneId} git={git} onChanged={rereadAll} onNewLane={onNewLane} />
|
||||
{!filesTab && (
|
||||
<LaneGitHeader laneId={laneId} git={git} onChanged={rereadAll} onNewLane={onNewLane} />
|
||||
)}
|
||||
{/* ONE wrapping row: where you are (mode) then how you are looking at it
|
||||
(range, layout, context, whole file), refresh last. Each group is
|
||||
its own inline box, so a narrow panel wraps BETWEEN groups — never
|
||||
@@ -1048,12 +1065,9 @@ export function LaneChangesPanel({
|
||||
<ChevronLeft className="h-3 w-3" />
|
||||
{t("changes.back")}
|
||||
</button>
|
||||
) : (
|
||||
) : filesTab ? null : (
|
||||
<div className="flex min-w-0 items-center rounded border border-border bg-surface-2 p-0.5">
|
||||
{(full
|
||||
? (["overview", "review", "history"] as const)
|
||||
: (["browse", "overview", "review", "history"] as const)
|
||||
).map((m) => (
|
||||
{(["overview", "review", "history"] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user