feat: sessions page workspace-first flow + workspace instance chips

- Sessions page: replaced CreateSessionForm with workspace selector + ToolStarter
  - Fetches workspaces, shows dropdown, then renders ToolStarter for selected workspace
  - Removed old project/repo/tool-type/config-profile/clone-mode flow
- Workspace cards: new WorkspaceInstanceChips component fetches and displays
  running instances per workspace with status-colored chips and open links
- Styles: .instance-chip variants (running/starting/error), .tool-starter-header

Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
2026-06-01 21:53:48 +02:00
parent c1445976d7
commit 78e808bc54
5 changed files with 219 additions and 88 deletions
+10 -18
View File
@@ -14,7 +14,11 @@ export interface ToolStarterProps {
onCancel?: () => void;
}
export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps) {
export function ToolStarter({
workspace,
onStarted,
onCancel,
}: ToolStarterProps) {
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [toolTypesLoading, setToolTypesLoading] = useState(true);
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
@@ -105,9 +109,7 @@ export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps
setStarting(true);
setError(null);
try {
const { createInstance, startInstance } = await import(
"../api/sessions"
);
const { createInstance, startInstance } = await import("../api/sessions");
const instance = await createInstance(
workspace.project_id,
workspace.repo_id,
@@ -132,12 +134,7 @@ export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps
} finally {
setStarting(false);
}
}, [
selectedToolTypeId,
selectedProfileId,
workspace,
onStarted,
]);
}, [selectedToolTypeId, selectedProfileId, workspace, onStarted]);
return (
<div className="tool-starter">
@@ -180,12 +177,8 @@ export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps
</option>
))}
</select>
{toolTypesLoading && (
<span className="muted">Loading tools...</span>
)}
{toolTypesError && (
<span className="error-text">{toolTypesError}</span>
)}
{toolTypesLoading && <span className="muted">Loading tools...</span>}
{toolTypesError && <span className="error-text">{toolTypesError}</span>}
</div>
{/* Config Profile */}
@@ -227,8 +220,7 @@ export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps
</span>
) : (
<span className="warning-text">
<Icon name="warning" size="sm" />{" "}
No SSH key assigned to repository
<Icon name="warning" size="sm" /> No SSH key assigned to repository
</span>
)}
</div>
+2 -6
View File
@@ -2,6 +2,7 @@
import { Link } from "react-router-dom";
import { Icon } from "./icon";
import { WorkspaceInstanceChips } from "./workspace-instance-chips";
import type { Workspace } from "../types/workspace";
export interface WorkspaceCardProps {
@@ -46,12 +47,7 @@ export function WorkspaceCard({
<p className="workspace-branch">
<Icon name="branch" size="sm" /> {workspace.branch}
</p>
{workspace.instance_count > 0 && (
<p className="workspace-instances">
{workspace.instance_count} active tool
{workspace.instance_count > 1 ? "s" : ""}
</p>
)}
<WorkspaceInstanceChips workspaceId={workspace.id} />
</div>
<div className="workspace-actions">
<button
@@ -0,0 +1,57 @@
/** Small component showing running instances for a workspace. */
import { useEffect, useState } from "react";
import { listWorkspaceInstances } from "../api/workspace-instances";
import type { ToolInstance } from "../api/sessions";
interface WorkspaceInstanceChipsProps {
workspaceId: string;
}
export function WorkspaceInstanceChips({
workspaceId,
}: WorkspaceInstanceChipsProps) {
const [instances, setInstances] = useState<ToolInstance[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async () => {
try {
const data = await listWorkspaceInstances(workspaceId);
setInstances(data);
} catch {
// ignore
} finally {
setLoading(false);
}
};
void load();
}, [workspaceId]);
if (loading) return <span className="muted">...</span>;
if (instances.length === 0) return null;
return (
<div className="instance-chips">
{instances.map((inst) => (
<span
key={inst.id}
className={`instance-chip ${inst.status}`}
title={inst.display_name}
>
{inst.display_name}
{inst.status === "running" && inst.url && (
<a
href={inst.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
</a>
)}
</span>
))}
</div>
);
}