15 KiB
Projects → Workspaces → Tabs (Full-Stack Plan)
This plan replaces the current “agents list + single chat view” IA with:
- Projects (grouped by a stable
projectKey) - Workspaces per project (today: local Paseo worktrees + main checkout; future: cloud VM)
- A workspace main view that is a tabbed panel where each tab is either an agent chat or a terminal
No legacy UI is kept: we redirect and fully rework the current routes and RPCs that don’t make sense anymore.
TL;DR
- Sidebar becomes Projects → Workspaces; still supports drag reorder (projects + workspaces) persisted on-device.
- Project key: remote when available; otherwise local repo root / workspace root (CWD fallback).
- Workspaces: show main checkout + all Paseo worktrees, including empty workspaces.
- Main view: opening a workspace shows a tab bar with all agents in that workspace; horizontally scrollable.
- Tabs are a discriminated union:
agentorterminal(terminals are not special). - Right sidebar stays Changes + Files, scoped to the current workspace; terminals removed from the right sidebar.
- File Explorer RPC: replace agent-scoped explorer/download-token RPC with workspace-scoped versions.
- Agent data is loaded once at daemon startup and kept queryable in-memory (no expensive directory rescans at runtime).
- Splits: not implemented now, but abstractions should not block future VSCode-style splits.
Definitions & IDs
Project
A Project is a grouping container keyed by projectKey.
projectKey derivation (must match user decisions):
- If git remote is available: use normalized remote key (e.g.
remote:github.com/org/repo). - Else (git with no remote OR not git): use the workspace/repo root path (local key).
Notes:
- For Paseo-owned worktrees, prefer grouping via the shared repo root when available (so multiple workspaces still group under one project even without a remote).
- For plain directories, the root path is the directory itself.
Workspace
A Workspace is an environment you can “open” and run agents/terminals within.
Today:
- Local main checkout (the normal repo root) is a workspace and cannot be archived.
- Local Paseo worktrees are workspaces (including empty ones).
Future:
- Workspace could point to a cloud VM or other remote compute target.
Workspace identity:
workspaceIdshould be stable and unique within a server.- For local: derive from canonical absolute
workspaceRoot(and/or from the worktree metadata id when we add one).
Tab
Tabs are the only top-level “things” in the workspace main view.
Discriminated union (locked):
type WorkspaceTab =
| { kind: "agent"; tabId: string; agentId: string }
| { kind: "terminal"; tabId: string; terminalId: string }
Terminals are not special: they are just kind: "terminal" tabs.
Panel (future splits)
We do not implement splits now. We still design tab/panel state so it can evolve into a layout model:
type PanelLayout =
| { kind: "single"; panelId: string }
// later: | { kind: "split"; direction: "row"|"column"; a: PanelLayout; b: PanelLayout; sizes: number[] }
For this milestone, PanelLayout is always { kind: "single" }.
UX (Agreed)
Sidebar (Left)
- Shows Projects (project icon + name + status dot).
- Each project expands to show Workspaces.
- Workspaces list includes:
- Main checkout workspace (always present, not archivable)
- All Paseo worktrees for that project (show all, including empty workspaces)
- Workspace row shows:
- Branch name
- Created at
- No path
- Drag reorder:
- Projects reorder globally
- Workspaces reorder within a project
- Persisted on-device
- No agents are shown in the sidebar (agents live in tabs + in the All Agents list).
Main View (Workspace)
- Opening a workspace shows a workspace-scoped screen.
- Top area contains a tab bar with all agent tabs in that workspace:
- horizontally scrollable
- switching tabs focuses the selected agent/terminal
- Remember last focus:
- When returning to a workspace, restore the last focused tab (agent or terminal).
- When opening a workspace for the first time: pick the most recent agent, else open the draft agent flow.
Tabs + Icons
- Each tab type provides its own icon.
- Terminal tabs: terminal icon.
- Agent tabs: status icon (running/idle/needs-attention/error) or provider icon if needed.
Right Sidebar (Explorer Sidebar)
- Remains Changes + Files only.
- Scope is the workspace you are looking at.
- Selection of agent/terminal tab does not change the right sidebar scope.
- Terminals are removed from the right sidebar entirely.
- Gestures/overlay behavior are unchanged (mobile).
Mobile-specific
- Keep existing overlay sidebars and gestures unchanged.
- Use a tab switcher pattern (bottom sheet / switcher) for tab navigation.
- Header is simplified:
- shows only workspace name (branch name)
- has space for a plus button
- Plus button:
- opens the draft agent flow, pre-scoped to the current workspace.
Data Model (Client)
On-device persisted state
Persist (per serverId):
projectOrder: projectKey[]workspaceOrderByProject: Record<projectKey, workspaceId[]>lastOpenedWorkspaceByProject: Record<projectKey, workspaceId>lastFocusedTabByWorkspace: Record<workspaceId, tabId>openTabsByWorkspace: Record<workspaceId, WorkspaceTab[]>(optional; can be derived initially from agents/terminals list but should be persistable)
Default ordering:
- If no persisted order:
- Projects sorted by
createdAt/firstSeenAt(or by name as fallback) - Workspaces sorted by createdAt (locked decision)
- Projects sorted by
Derived view models
Compute:
projects[]from workspace + checkout metadataprojectStatusDotaggregated from agent statuses in all workspaces in that projectworkspaceStatusDotaggregated from agent statuses in that workspace
Data Model (Server)
Agents: load once, query often
Current pain:
- Hydrating agents by scanning directories is expensive.
Target behavior:
- On daemon startup, perform a single load/scan and build an in-memory index:
- by
agentId - by
workspaceRoot - by
projectKey - by status (running, attention, errored, etc.)
- by
- No periodic rescans at runtime.
- When an agent is created/updated/removed, update the in-memory index incrementally.
- This aligns with the PID lock: we don’t support concurrent disk mutation anyway.
- Future: replace with local SQL DB, but not in this milestone.
Workspaces (local)
Source of truth:
- Main checkout: derived from repo root / current connection cwd policy.
- Paseo worktrees:
paseo_worktree_list_*plus metadata.
Worktree metadata needs createdAt:
- Add
createdAtto Paseo worktree metadata (new version). - Backfill for existing worktrees using filesystem stat of metadata file or worktree directory.
RPC / Protocol Changes (Server ↔ App)
Replace: File Explorer request (agent-scoped → workspace-scoped)
Current (to remove):
file_explorer_requestrequiresagentId.
New (replacement; same message type name or updated schema, but old behavior removed):
file_explorer_requestkeyed byworkspaceId(orworkspaceRoot) instead ofagentId.- Requests can succeed even if there are no agents in the workspace (empty workspace is valid).
Rules:
- All explorer operations are rooted at
workspaceRoot. - Path traversal is prevented (no
.., enforce prefix).
Replace: File download token request (agent-scoped → workspace-scoped)
Current (to remove):
file_download_token_requestrequiresagentId.
New:
file_download_token_requestrequiresworkspaceId(orworkspaceRoot) andpath.- Token resolution is workspace-rooted with the same traversal protections.
Worktree list RPC: include createdAt
Update paseo_worktree_list_response to include:
createdAt(ISO string)workspaceId(optional if client can derive; preferred to include for stability)
Ensure the list includes:
- all Paseo worktrees for the requested repo/project
- enough info to render workspace row: branch name + createdAt
Checkout lite payload: ensure it contains enough for project grouping
For workspace/project grouping on the client, ensure we have:
isGitremoteUrl(nullable)currentBranchisPaseoOwnedWorktreemainRepoRoot(when worktree-owned)- (optional)
repoRootfor non-worktree git repos
App Changes (packages/app)
Routes
Replace/redirect:
/h/:serverIdshould no longer redirect to “draft agent”; it should land on:- last opened workspace, or
- a workspace picker (the new sidebar is the primary picker)
/h/:serverId/agentand/h/:serverId/agent/:agentIdare replaced with:/h/:serverId/workspace/:workspaceId(main workspace screen)/h/:serverId/workspace/:workspaceId/draft-agent(draft flow, pre-scoped)
No legacy route behavior remains; old routes redirect.
Left Sidebar rewrite
Replace the current agent list sidebar with:
- Project list (icon + name + status dot)
- Nested workspace list (branch name + createdAt)
- Drag reorder persisted on-device
Workspace screen
New top-level screen composition:
- Header: workspace name (branch) + plus button
- Tab strip (horizontal)
- Active tab content (agent chat or terminal UI)
- Right sidebar: changes + file explorer (workspace-scoped)
Tabs system
Implement WorkspaceTab union:
agenttabs map to existing agent chat component (but now hosted within the workspace screen).terminaltabs map to existing terminal component previously shown in the right sidebar.
Remember focus:
- Persist
lastFocusedTabByWorkspace.
Terminal migration
- Remove terminal UI from right sidebar.
- Expose terminals through tabs:
- create terminal -> adds a
terminaltab - show terminal output in the main area
- create terminal -> adds a
File explorer state migration
Update file explorer client state/actions:
- Key state by
workspaceId(notagentId). - Update all call sites to use the new RPC parameters.
Status Dots (Sidebar)
Agent → Workspace → Project aggregation
Define a simple severity ordering, e.g.:
- Error
- Needs attention
- Running
- Idle / none
Compute:
- workspace status = max severity across agents in that workspace
- project status = max severity across all workspaces in that project
No agents are displayed in the sidebar; only aggregated signal.
User Flows (Agreed)
Open app → open a workspace
- User opens app.
- Left sidebar shows Projects.
- User selects a Project.
- User selects a Workspace (main checkout or a worktree).
- Workspace screen opens:
- tab strip shows all agents in that workspace
- restores last focused tab if available
- otherwise picks most recent agent
- otherwise opens draft agent flow pre-scoped to the workspace
Create a new agent in a workspace
- User is inside a workspace.
- User taps plus in header.
- Draft agent flow opens, pre-scoped to that workspace root.
- On create:
- new agent appears as a new
agenttab in the workspace - it becomes focused
- new agent appears as a new
Open a terminal
- User taps plus (or a “new tab” affordance).
- Selects “Terminal”.
- A
terminaltab is created and focused. - Terminal is fully usable inside the tab panel.
Switch tabs (mobile)
- User opens tab switcher.
- Selects an agent/terminal tab.
- Workspace focuses that tab and persists focus.
Reorder projects and workspaces
- User drags a project to reorder.
- Order persists on-device and survives app restart.
- User expands a project and drags a workspace row to reorder within the project.
- Order persists on-device and survives app restart.
Acceptance Criteria
Sidebar / Navigation
- Projects are grouped by
projectKey:- remote when available
- otherwise local repo/workspace root key
- Sidebar shows project icon for each project.
- Each project lists workspaces (main checkout + all Paseo worktrees), including empty workspaces.
- Workspace rows show branch name + createdAt, and do not show path.
- Projects and workspaces can be drag-reordered; order is persisted on-device.
- Sidebar does not list agents anywhere.
Workspace main view
- Opening a workspace shows a tabbed main view (not a single chat screen).
- Tab bar contains all agents in the workspace; horizontally scrollable.
- Last focused tab is restored when returning to a workspace.
- Header shows workspace name and a plus button.
- Plus opens draft agent flow pre-scoped to workspace.
- Mobile uses a tab switcher; sidebars/gestures unchanged.
Tabs / Terminals
- Tabs are a discriminated union:
agentorterminal. - Terminals are accessible only as tabs (no terminal section in the right sidebar).
- Each tab type can render a custom icon; agent tabs can render status.
Explorer / Changes / Files
- Right sidebar contains only Changes + Files, scoped to the opened workspace.
- File explorer and download token requests are workspace-scoped (no agentId in protocol).
- File explorer works for empty workspaces (no agents yet).
Migration / No legacy
- Old agent-centric routes redirect to the new workspace routes.
- Old agent-scoped file explorer RPC is removed/replaced (no legacy behavior left behind).
Implementation Phases (Suggested)
Phase 0 — Scaffolding
- Add shared types:
Project,Workspace,WorkspaceTab. - Add on-device storage keys and migration scaffolding (no UI yet).
Phase 1 — Server protocol + data
- Replace file explorer + download token RPCs to be workspace-scoped.
- Ensure checkout-lite has enough fields for grouping.
- Add worktree
createdAtand include it in worktree list response. - Ensure “main checkout workspace” can be represented as a workspace.
- Ensure agent index loads once at startup and is queryable without rescans.
Phase 2 — App navigation rewrite
- Introduce workspace routes and redirect old routes.
- Add new workspace screen layout (header + tabs + explorer sidebar).
Phase 3 — Sidebar rewrite
- Implement Projects → Workspaces sidebar UI.
- Implement project icon wiring.
- Implement drag reorder + persisted ordering.
- Implement project/workspace status dots from agent status aggregation.
Phase 4 — Tabs + terminals migration
- Implement tab state + last focused persistence.
- Move terminals out of right sidebar and into tabs.
- Implement mobile tab switcher.
Phase 5 — Polish + verification
- Verify flows end-to-end (open workspace, tabs, explorer, plus flows).
- Confirm no legacy route/UI remains.
- Typecheck and targeted runtime smoke tests.
Verification Checklist
npm run typecheckpasses after each phase.- Explorer sidebar still behaves the same (gestures/overlay).
- No references remain to “file explorer by agentId” or “download token by agentId”.
- Terminals are not rendered in the right sidebar.
- Workspace list includes empty worktrees and main checkout.
- Reorder persistence works and survives app restart.