feat: new ToolStarter component — unified workspace-first tool starting

- New ToolStarter component: workspace context, fetches real tool types,
  auto-fetches config profiles per tool type, shows SSH key status
- Backend: add repo_ssh_key_id to workspace list responses
- WorkspacesPage: uses ToolStarter in modal instead of StartToolModal
- WorkspaceDetailPage ToolsTab: uses ToolStarter in modal
- Removed old inline StartToolModal from workspace-detail.tsx
- Styles: .tool-starter-context, .context-row, .ssh-key-status

Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
2026-06-01 21:27:21 +02:00
parent ff8aa2a4f5
commit c1445976d7
6 changed files with 351 additions and 50 deletions
+16 -23
View File
@@ -7,9 +7,8 @@ import { useWorkspaces } from "../hooks/use-workspaces";
import { useWorkspaceFiles } from "../hooks/use-workspace-files";
import { useWorkspaceGit } from "../hooks/use-workspace-git";
import { useWorkspaceInstances } from "../hooks/use-workspace-instances";
import { useStartTool } from "../hooks/use-start-tool";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { StartToolModal } from "../components/start-tool-modal";
import { ToolStarter } from "../components/tool-starter";
import type { FileEntry } from "../api/workspace-files";
import type { Workspace } from "../types/workspace";
@@ -314,7 +313,6 @@ function GitTab({ workspaceId }: { workspaceId: string }) {
function ToolsTab({ workspace }: { workspace: Workspace }) {
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
const { startTool, starting } = useStartTool();
const [showModal, setShowModal] = useState(false);
return (
@@ -328,9 +326,8 @@ function ToolsTab({ workspace }: { workspace: Workspace }) {
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
disabled={starting}
>
{starting ? "Starting..." : "Start Tool"}
Start Tool
</button>
</div>
) : (
@@ -358,29 +355,25 @@ function ToolsTab({ workspace }: { workspace: Workspace }) {
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
disabled={starting}
>
{starting ? "Starting..." : "Start Another Tool"}
Start Another Tool
</button>
</>
)}
{showModal && (
<StartToolModal
workspace={workspace}
onClose={() => setShowModal(false)}
onStart={async (toolTypeId, configProfileId) => {
const instance = await startTool(
workspace,
toolTypeId,
undefined,
configProfileId,
);
if (instance) {
setShowModal(false);
await refresh();
}
}}
/>
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Start Tool</h3>
<ToolStarter
workspace={workspace}
onStarted={() => {
setShowModal(false);
void refresh();
}}
onCancel={() => setShowModal(false)}
/>
</div>
</div>
)}
</div>
);
+14 -26
View File
@@ -4,10 +4,9 @@ import { useState } from "react";
import { Icon } from "../components/icon";
import { useWorkspaces } from "../hooks/use-workspaces";
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
import { useStartTool } from "../hooks/use-start-tool";
import { WorkspaceCard } from "../components/workspace-card";
import { WorkspaceCreateForm } from "../components/workspace-create-form";
import { StartToolModal } from "../components/start-tool-modal";
import { ToolStarter } from "../components/tool-starter";
import type { Workspace } from "../types/workspace";
export function WorkspacesPage() {
@@ -16,8 +15,6 @@ export function WorkspacesPage() {
const { workspaces, loading, error, refresh } = useWorkspaces();
const actions = useWorkspaceActions();
const { startTool } = useStartTool();
const handleDelete = async (workspace: Workspace) => {
await actions.delete(workspace, refresh);
};
@@ -31,23 +28,6 @@ export function WorkspacesPage() {
);
};
const handleStartTool = async (
toolTypeId: string,
configProfileId?: string,
) => {
if (!startWorkspace) return;
const instance = await startTool(
startWorkspace,
toolTypeId,
undefined,
configProfileId,
);
if (instance) {
setStartWorkspace(null);
await refresh();
}
};
return (
<div className="page workspaces-page">
<header className="page-header">
@@ -109,11 +89,19 @@ export function WorkspacesPage() {
)}
{startWorkspace && (
<StartToolModal
workspace={startWorkspace}
onClose={() => setStartWorkspace(null)}
onStart={handleStartTool}
/>
<div className="modal-overlay" onClick={() => setStartWorkspace(null)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Start Tool</h3>
<ToolStarter
workspace={startWorkspace}
onStarted={() => {
setStartWorkspace(null);
void refresh();
}}
onCancel={() => setStartWorkspace(null)}
/>
</div>
</div>
)}
</div>
);