refactor: organize frontend components into features/ directories

Moved 43 component files into 9 feature domains:
- features/git/ — commit-dialog, commit-panel, file-editor, git-mount-editor,
  git-toolbar, merge-dialog
- features/project/ — repositories-settings-tab, repository-create-dialog
- features/terminal/ — special-keys-panel, special-keys-strip,
  terminal-session-tabs, terminal
- features/workspace/ — workspace-card, workspace-create-form,
  workspace-header, workspace-instance-chips
- features/session/ — create-session-form, session-card, session-list
- features/tool/ — instance-list, manifest-editor, start-tool-fab,
  start-tool-modal, tool-starter, tools-bottom-sheet
- features/notification/ — event-toast-bridge, notification-center,
  notification-item
- features/settings/ — settings-tab-layout
- features/mobile/ — mobile-action-sheet, mobile-detail-view, mobile-edit-view,
  mobile-fab, mobile-list-view, mobile-nav, mobile-page-header,
  mobile-terminal-header, mobile-terminal-wrapper

Updated all imports across pages and components.
Root components/ now only contains generic UI pieces:
app-shell, code-editor, data-states, icon, protected-route, syntax-highlighter.

Quality gates: verified no remaining old imports.
This commit is contained in:
2026-06-04 12:37:24 +02:00
parent 7224afafd1
commit 1021d61be3
54 changed files with 31 additions and 31 deletions
@@ -0,0 +1,299 @@
/** Unified tool starter — workspace-first, fetches real tool types and config profiles. */
import { useState, useEffect, useCallback } from "react";
import { Icon } from "./icon";
import { listToolTypes, type ToolType } from "../api/tool-types";
import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles";
import { listSSHKeys, type SSHKey } from "../api/ssh-keys";
import type { Workspace } from "../types/workspace";
import type { ToolInstance } from "../api/sessions";
export interface ToolStarterProps {
workspace: Workspace;
onStarted: (instance: ToolInstance) => void;
onCancel?: () => void;
}
export function ToolStarter({
workspace,
onStarted,
onCancel,
}: ToolStarterProps) {
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [toolTypesLoading, setToolTypesLoading] = useState(true);
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
const [selectedToolTypeId, setSelectedToolTypeId] = useState("");
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
const [profilesLoading, setProfilesLoading] = useState(false);
const [selectedProfileId, setSelectedProfileId] = useState("");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [sshKeysLoading, setSshKeysLoading] = useState(true);
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
const [starting, setStarting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fetch tool types on mount
useEffect(() => {
const load = async () => {
try {
const data = await listToolTypes();
setToolTypes(data);
} catch (err) {
setToolTypesError(
err instanceof Error ? err.message : "Failed to load tool types",
);
} finally {
setToolTypesLoading(false);
}
};
void load();
}, []);
// Fetch config profiles when tool type changes
useEffect(() => {
if (!selectedToolTypeId) {
setProfiles([]);
setSelectedProfileId("");
return;
}
const load = async () => {
setProfilesLoading(true);
try {
const data = await listConfigProfiles(
workspace.project_id,
selectedToolTypeId,
);
setProfiles(data);
// Auto-select default profile if available
const defaultProfile = data.find((p) => p.is_default);
if (defaultProfile) {
setSelectedProfileId(defaultProfile.id);
} else {
setSelectedProfileId("");
}
} catch {
setProfiles([]);
} finally {
setProfilesLoading(false);
}
};
void load();
}, [selectedToolTypeId, workspace.project_id]);
// Fetch SSH keys on mount
useEffect(() => {
const load = async () => {
try {
const data = await listSSHKeys();
setSshKeys(data);
// Auto-select the repository's SSH key if available
if (workspace.repo_ssh_key_id) {
setSelectedSshKeyIds([workspace.repo_ssh_key_id]);
}
} catch (err) {
console.error("Failed to load SSH keys:", err);
} finally {
setSshKeysLoading(false);
}
};
void load();
}, [workspace.repo_ssh_key_id]);
const repoHasSshKey = !!workspace.repo_ssh_key_id;
const repoSshKey = sshKeys.find((k) => k.id === workspace.repo_ssh_key_id);
const handleStart = useCallback(async () => {
if (!selectedToolTypeId) {
setError("Please select a tool type");
return;
}
setStarting(true);
setError(null);
try {
const { createInstance, startInstance } = await import("../api/sessions");
const instance = await createInstance(
workspace.project_id,
workspace.repo_id,
selectedToolTypeId,
workspace.name,
undefined,
undefined,
undefined,
selectedProfileId || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
workspace.id,
);
await startInstance(
workspace.project_id,
workspace.repo_id,
instance.id,
selectedProfileId || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
);
onStarted(instance);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start tool");
} finally {
setStarting(false);
}
}, [selectedToolTypeId, selectedProfileId, workspace, onStarted]);
return (
<div className="tool-starter">
{/* Context header — read-only workspace info */}
<div className="tool-starter-context">
<div className="context-row">
<span className="context-label">Project</span>
<span className="context-value">{workspace.project_name}</span>
</div>
<div className="context-row">
<span className="context-label">Repository</span>
<span className="context-value">{workspace.repo_name}</span>
</div>
<div className="context-row">
<span className="context-label">Workspace</span>
<span className="context-value">{workspace.name}</span>
<span className="branch-badge">
<Icon name="branch" size="sm" /> {workspace.branch}
</span>
</div>
</div>
{/* Tool Type */}
<div className="form-group">
<label htmlFor="tool-type">Tool Type</label>
<select
id="tool-type"
value={selectedToolTypeId}
onChange={(e) => {
setSelectedToolTypeId(e.target.value);
setError(null);
}}
disabled={toolTypesLoading || starting}
>
<option value="">Select a tool...</option>
{toolTypes.map((tt) => (
<option key={tt.id} value={tt.id}>
{tt.display_name}
{tt.category && ` (${tt.category})`}
</option>
))}
</select>
{toolTypesLoading && <span className="muted">Loading tools...</span>}
{toolTypesError && <span className="error-text">{toolTypesError}</span>}
</div>
{/* Config Profile */}
{selectedToolTypeId && (
<div className="form-group">
<label htmlFor="config-profile">Config Profile</label>
<select
id="config-profile"
value={selectedProfileId}
onChange={(e) => setSelectedProfileId(e.target.value)}
disabled={profilesLoading || starting}
>
<option value="">Default (no profile)</option>
{profiles.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
{p.is_default && " (default)"}
</option>
))}
</select>
{profilesLoading && (
<span className="muted">Loading profiles...</span>
)}
{profiles.length === 0 && !profilesLoading && (
<span className="muted">No custom profiles for this tool.</span>
)}
</div>
)}
{/* SSH Key Selection */}
<div className="form-group ssh-key-selection">
<label>SSH Keys</label>
{sshKeysLoading ? (
<span className="muted">Loading SSH keys...</span>
) : sshKeys.length === 0 ? (
<span className="muted">No SSH keys configured.</span>
) : (
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
{sshKeys.map((key) => (
<label
key={key.id}
className="checkbox-label"
style={{
display: "flex",
alignItems: "center",
gap: "0.25rem",
padding: "0.375rem 0.75rem",
background: "var(--panel)",
borderRadius: "0.375rem",
border: "1px solid var(--border)",
cursor: "pointer",
}}
>
<input
type="checkbox"
checked={selectedSshKeyIds.includes(key.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedSshKeyIds((prev) => [...prev, key.id]);
} else {
setSelectedSshKeyIds((prev) =>
prev.filter((id) => id !== key.id),
);
}
}}
disabled={starting}
/>
{key.name}
</label>
))}
</div>
)}
{!sshKeysLoading && repoHasSshKey && repoSshKey && (
<div className="hint" style={{ marginTop: "0.5rem" }}>
Repository key <strong>{repoSshKey.name}</strong> is pre-selected.
</div>
)}
</div>
{error && <p className="form-error">{error}</p>}
<div className="form-actions">
{onCancel && (
<button
type="button"
className="btn btn-secondary"
onClick={onCancel}
disabled={starting}
>
Cancel
</button>
)}
<button
type="button"
className="btn btn-primary"
onClick={handleStart}
disabled={!selectedToolTypeId || toolTypesLoading || starting}
>
{starting ? (
<>
<Icon name="loading" size="sm" /> Starting...
</>
) : (
<>
<Icon name="play" size="sm" /> Start Tool
</>
)}
</button>
</div>
</div>
);
}