8c1948d226
Move the following completed changes from openspec/changes/ to openspec/changes/archive/2026-06-12-completed-changes-archive/: - multi-session-terminal-ux - reorganize-long-files - working-copies - workspace-first-ui Update parent and archive .pi-map*.md indexes to reflect the move and remove the transient active-changes-archive grouping. openspec/changes/ now contains only the archive/ directory.
8.0 KiB
8.0 KiB
Spec: Workspace-Based Tool Instances
Status
| Field | Value |
|---|---|
| Phase | Spec |
| Based on | Proposal |
| Next | Design |
Overview
Workspaces are persistent, writable local clones of Git repositories. Users create workspaces explicitly, then start tool instances on them. This replaces the current "mount vs clone" decision with a simple "pick a workspace" flow.
Decisions
| Decision | Value |
|---|---|
| Name | "Workspace" |
| Scope | Unlimited per repository |
| Auto-create | No — explicit creation only |
| Delete with instances | Allowed with confirmation; stops and deletes all associated tool instances |
| Name uniqueness | Unique per project+repo; derived from project and repo names |
| Deleted remote branch | On sync/update, detect and ask for confirmation to delete local workspace/branch |
Database Schema
New Table: workspaces
CREATE TABLE workspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
repo_id UUID NOT NULL REFERENCES git_repositories(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
branch VARCHAR(255) NOT NULL DEFAULT 'main',
path VARCHAR(2048) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'ready',
last_sync_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
UNIQUE (repo_id, name)
);
CREATE INDEX idx_workspaces_repo_id ON workspaces(repo_id);
CREATE INDEX idx_workspaces_user_id ON workspaces(user_id);
CREATE INDEX idx_workspaces_status ON workspaces(status);
Updated Table: tool_instances
ALTER TABLE tool_instances
ADD COLUMN workspace_id UUID REFERENCES workspaces(id) ON DELETE SET NULL,
ADD COLUMN clone_mode VARCHAR(16); -- deprecated, nullable for migration
-- Drop existing clone_mode column after all instances are migrated
-- ALTER TABLE tool_instances DROP COLUMN clone_mode;
Note: tool_instances.branch remains for now but is deprecated; the canonical branch lives on the workspace.
Backend API
Workspaces API
GET /projects/{project_id}/repositories/{repo_id}/workspaces
→ List workspaces for a repository
POST /projects/{project_id}/repositories/{repo_id}/workspaces
→ Create a new workspace
Body: { name: string, branch: string }
GET /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
→ Get workspace details
PATCH /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
→ Update workspace (rename, change branch)
Body: { name?: string, branch?: string }
DELETE /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
→ Delete workspace (with ?force=true to skip confirmation)
→ Stops and deletes all associated tool instances
POST /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}/sync
→ Sync workspace with remote (detect deleted branches)
Tool Instances API (Updated)
POST /projects/{project_id}/repositories/{repo_id}/instances
Body: { tool_type_id, workspace_id, display_name?, config_profile_id? }
→ Create instance on workspace
POST /projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/start
→ Start instance (creates workspace if mount_mode, migrates if clone_mode)
Instance Start Logic
def start_instance(instance, workspace_id=None):
if instance.clone_mode == "clone":
# Migrate: extract clone to workspace
workspace = migrate_clone_to_workspace(instance)
instance.workspace_id = workspace.id
instance.clone_mode = None
elif instance.workspace_id:
# Already using a workspace
workspace = get_workspace(instance.workspace_id)
else:
# Legacy mount_mode: create workspace on first start
workspace = create_workspace_from_repo(instance.repo)
instance.workspace_id = workspace.id
# Mount workspace path into container
mount_path = workspace.path
# ... rest of start logic
Frontend Routes
/workspaces → Workspaces list page
/workspaces/new → Create workspace flow
/workspaces/{id} → Workspace detail page
/workspaces/{id}/tools → Start tool on workspace
UI Components
Sidebar Navigation
Projects
└── [project list]
Workspaces (NEW)
└── All Workspaces
└── [recent workspaces]
Settings
Workspaces Page
- Grid/list of workspace cards
- Each card shows: name, repo, branch, status, active instances count
- Actions: Start Tool, Sync, Settings, Delete
Create Workspace Flow
- Repo picker: Select from existing repositories
- Branch picker: Select branch (default: repo's default branch)
- Name input: Auto-suggested as
{project-name}-{repo-name}-{branch}but editable - Create: Clone repo to
/data/working-copies/{repo-id}/{name}/
Start Tool from Workspace
- Tool picker: Select tool type
- Config: Optional config profile
- Create: Instance created with workspace mounted
File System Layout
/data/working-copies/
└── {repo-id}/
└── {workspace-name}/
└── .git/
└── [repo files]
Workspace Lifecycle
Creation
- Validate name uniqueness per repo
- Clone repo:
git clone --branch {branch} {remote_url} {path} - Set status to
ready - Return workspace record
Deletion
- Check for running tool instances
- If instances exist and no
?force=true:- Return 409 Conflict with
{ instances: [...] } - Frontend shows confirmation dialog
- Return 409 Conflict with
- If confirmed:
- Stop all associated instances
- Delete all associated instances
- Delete workspace directory
- Delete workspace record
Sync
- Fetch from remote:
git fetch origin - Check if workspace branch still exists on remote
- If branch deleted:
- Return 409 with
{ branch_deleted: true } - Frontend asks: "Branch '{branch}' was deleted. Delete this workspace?"
- Return 409 with
- If branch exists:
- Pull changes:
git pull origin {branch} - Update
last_sync_at
- Pull changes:
Migration Strategy
Existing clone_mode Instances
def migrate_clone_to_workspace(instance):
# Find the cloned repo inside the instance directory
clone_path = find_clone_in_instance_dir(instance)
# Create workspace
workspace = Workspace(
name=f"{instance.name}-migrated",
repo_id=instance.repository_id,
user_id=instance.owner_id,
branch=instance.branch or "main",
path=f"/data/working-copies/{instance.repository_id}/{instance.name}-migrated",
)
# Move clone to workspace path
move(clone_path, workspace.path)
return workspace
Existing mount_mode Instances
On first start after deployment:
- Create workspace from canonical repo
- Update instance to use workspace
- Remove clone_mode flag
Acceptance Criteria
- Database migration creates
workspacestable - Database migration adds
workspace_idtotool_instances - API endpoints for CRUD operations on workspaces
- Workspace creation clones repo to
/data/working-copies/... - Workspace deletion stops and deletes associated tool instances
- Workspace sync detects deleted branches and asks for confirmation
- Tool instance creation accepts
workspace_idinstead ofclone_mode - Tool instance start mounts workspace path into container
- Frontend has "Workspaces" sidebar entry
- Frontend workspaces list page
- Frontend create workspace flow
- Frontend start tool from workspace
- Existing clone_mode instances migrate on restart
- Existing mount_mode instances create workspace on restart
Quality Gates
- Backend tests: workspace CRUD, sync, deletion with instances
- Frontend tests: workspace list, create, start tool
- Integration tests: instance creation with workspace
- ruff clean
- TypeScript compilation clean