feat: complete working-copies workspace-first cleanup

- Remove clone_mode/branch/new_branch from frontend create session flow.
- Add workspace picker to CreateSessionForm; auto-create default workspace when repo selected.
- Fix tool-starter.tsx and use-start-tool.ts createInstance signatures after API change.
- Remove clone mode badge from SessionCard.
- Delete stale backend unit tests referencing removed clone_mode schema fields.
- Update OpenSpec working-copies tasks and mark change completed.
- Regenerate project maps.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (82 passed),
python3 -m py_compile on changed backend files.
This commit is contained in:
Developer
2026-06-12 15:47:26 +00:00
parent 3da7ea6408
commit 6b947b7593
32 changed files with 182 additions and 1386 deletions
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/session
## role
Provides UI components for managing development environment sessions including creation, listing, monitoring progress, and interacting with individual sessions.
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,16 +4,16 @@ dir: apps/web/src/components/features/session
index: apps/web/src/components/features/session/.pi-map.index.md
## role
Provides UI components for managing development environment sessions including creation, listing, monitoring progress, and interacting with individual sessions.
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
## files
- create-session-form.tsx | React form component for creating development sessions with configurable project, repository, tool type, branch, SSH keys, and config profile options | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, icon component, sessions API, git-repositories API, ssh-keys API, config-profiles API, tool-types API, types
- session-card.tsx | Renders a React card component displaying session information with status badges, inline editing, action buttons, and responsive mobile/desktop layouts. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet, icon, use-mobile-viewport, mobile-action-sheet, sessions api types
- create-session-form.tsx | A React form component for creating and starting a new development session with configurable project, repository, workspace, tool type, config profile, and SSH key options. | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, ../../../api/workspaces, ../../../types/workspace, icon, sessions API, git-repositories API, tool-types API, ssh-keys API, config-profiles API, workspaces API, types
- session-card.tsx | Renders a card component displaying session information with status badges, inline rename editing, action buttons, and responsive mobile/desktop layouts including a dropdown options menu and mobile action sheet. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet
- session-list.tsx | Renders a list of sessions grouped by active/recent status or as a flat grid, delegating to SessionCard for individual session display. | exp: SessionListProps, func:SessionList({ sessions, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, actionBusyId = null, tunnelHealth = {}, showGrouping = true, activeTitle = "Active Sessions", recentTitle = "Recent Sessions", maxRecent = 5, emptyMessage = "No sessions", }: SessionListProps), call:sessions.filter, call:activeStatuses.includes, call:sessions .filter((s) => recentStatuses.includes(s.status)) .slice, call:recentStatuses.includes, call:sessions.map, call:activeSessions.map, call:recentSessions.map | dep: ../../../api/sessions, ./session-card, Session, SessionCard, InstanceHealth
- session-progress-panel.tsx | Renders a panel displaying active and recently completed session operations with step-by-step progress indicators and dismissible notifications. | exp: func:SessionProgressPanel(), call:useSessionOperations, call:useEventContext, call:useEffect, call:updateOperationFromEvent, call:operations.filter, call:Date.now, call:visibleOperations.map, call:dismissOperation | dep: react, ../../../state/session-operations, ../../../state/events, ../../icon, useSessionOperations, useEventContext, Icon
## arch
Feature-based component composition with presentational components following a container/presenter pattern, using status-driven conditional rendering and responsive layout adaptations.
React component composition with feature-specific grouping, responsive design patterns (mobile/desktop layouts), and status-driven conditional rendering with delegated sub-components.
## tags
session, call:use, call:on, card, mobile, call:set, event, api
session, call:use, call:on, api, card, call:set, event, mobile
## symbols
- SessionCard
- SessionList
@@ -13,6 +13,8 @@ import {
listConfigProfiles,
type ConfigProfile,
} from "../../../api/config-profiles";
import { listWorkspaces, createWorkspace } from "../../../api/workspaces";
import type { Workspace } from "../../../types/workspace";
interface CreateSessionFormProps {
projects: Project[];
@@ -54,6 +56,10 @@ export const CreateSessionForm = ({
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -94,6 +100,50 @@ export const CreateSessionForm = ({
void loadProfiles();
}, [selectedToolType, selectedProject, fixedProjectId]);
// Load workspaces when repository is selected
useEffect(() => {
const projectId = fixedProjectId || selectedProject;
const repoId = fixedRepoId || selectedRepo;
if (!projectId || !repoId) {
setWorkspaces([]);
setSelectedWorkspaceId("");
return;
}
const loadWorkspaces = async () => {
setIsLoadingWorkspaces(true);
try {
const data = await listWorkspaces(projectId, repoId);
setWorkspaces(data);
if (data.length > 0) {
setSelectedWorkspaceId(data[0].id);
} else {
// Auto-create a default workspace so the user can start a tool
const workspace = await createWorkspace(projectId, repoId, {
name: "default",
branch: "main",
});
setWorkspaces([workspace]);
setSelectedWorkspaceId(workspace.id);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load workspaces",
);
setWorkspaces([]);
setSelectedWorkspaceId("");
} finally {
setIsLoadingWorkspaces(false);
}
};
void loadWorkspaces();
}, [
fixedProjectId,
selectedProject,
fixedRepoId,
selectedRepo,
repositories,
]);
// Filter repositories by selected project
const availableRepos = selectedProject
? repositories.filter((r) => r.project_id === selectedProject)
@@ -106,6 +156,8 @@ export const CreateSessionForm = ({
setDisplayName("");
setSelectedSshKeyIds([]);
setSelectedConfigProfile("");
setWorkspaces([]);
setSelectedWorkspaceId("");
};
const handleSubmit = async (event: React.FormEvent) => {
@@ -123,12 +175,19 @@ export const CreateSessionForm = ({
setIsSubmitting(true);
try {
const workspaceId = selectedWorkspaceId || undefined;
if (!workspaceId) {
setError("No workspace available for the selected repository");
setIsSubmitting(false);
return;
}
const instance = await createInstance(
projectId,
repoId,
selectedToolType,
displayName || undefined,
undefined,
workspaceId,
selectedConfigProfile || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
);
@@ -214,6 +273,7 @@ export const CreateSessionForm = ({
onChange={(e) => {
setSelectedRepo(e.target.value);
setSelectedToolType("");
setSelectedWorkspaceId("");
}}
disabled={!hasProject || isSubmitting}
>
@@ -228,6 +288,30 @@ export const CreateSessionForm = ({
</div>
)}
{/* Workspace */}
{hasRepo && (
<div className="form-field">
<label>Workspace</label>
{isLoadingWorkspaces ? (
<span className="muted">Loading workspaces...</span>
) : workspaces.length === 0 ? (
<span className="muted">No workspace available</span>
) : (
<select
value={selectedWorkspaceId}
onChange={(e) => setSelectedWorkspaceId(e.target.value)}
disabled={!hasRepo || isSubmitting || isLoadingWorkspaces}
>
{workspaces.map((w) => (
<option key={w.id} value={w.id}>
{w.name} ({w.branch})
</option>
))}
</select>
)}
</div>
)}
{/* Tool Type */}
{hasRepo && (
<div className="form-field">
@@ -176,14 +176,6 @@ export function SessionCard({
)}{" "}
· {session.tool_type_name}
</p>
{session.clone_mode && (
<p className="muted session-card-meta">
<Icon name="branch" size="sm" />
{session.clone_mode === "clone"
? `Clone${session.branch ? ` (${session.branch})` : ""}`
: "Mount"}
</p>
)}
{session.url && (
<p className="session-card-url">
<button