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
+8 -6
View File
@@ -1,7 +1,8 @@
/**
* @file projectTreePrefs — the sidebar project tree's viewer preferences, in
* ONE localStorage key (`agenthub.sidebar.projects`): which projects are
* collapsed, whether a repo's untracked worktrees are listed, and which
* @file projectTreePrefs — the sidebar workspace list's viewer preferences, in
* ONE localStorage key (`agenthub.sidebar.projects`): which workspaces are
* expanded to their sessions (by lane id), whether a repo's untracked
* worktrees are offered, and which
* worktree sources (`claude` = `<repo>/.claude/worktrees/*`, `agenthub` =
* LANES_ROOT) are hidden — globally, or per project root, where a project's own
* list (Settings → Projects) replaces the global one. A `useSyncExternalStore` reader that caches nothing
@@ -15,7 +16,8 @@ import { useSyncExternalStore } from "react";
export const PROJECT_TREE_KEY = "agenthub.sidebar.projects";
export interface ProjectTreePrefs {
collapsed: Record<string, boolean>;
/** Lane id → its sessions are shown under it in the sidebar. */
expanded: Record<string, boolean>;
showUntracked: boolean;
hideSources: string[];
/** Per project root: replaces `hideSources` for that project. */
@@ -23,7 +25,7 @@ export interface ProjectTreePrefs {
}
const DEFAULTS: ProjectTreePrefs = {
collapsed: {},
expanded: {},
showUntracked: true,
hideSources: [],
projectHideSources: {},
@@ -46,7 +48,7 @@ export function readProjectTreePrefs(): ProjectTreePrefs {
try {
const parsed = raw ? (JSON.parse(raw) as Partial<ProjectTreePrefs>) : {};
value = {
collapsed: parsed.collapsed && typeof parsed.collapsed === "object" ? parsed.collapsed : {},
expanded: parsed.expanded && typeof parsed.expanded === "object" ? parsed.expanded : {},
showUntracked: parsed.showUntracked !== false,
hideSources: Array.isArray(parsed.hideSources) ? parsed.hideSources : [],
projectHideSources: