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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user