feat: complete reorganize-long-files cleanup
- Extract tool instance lifecycle endpoints (start/stop/restart/delete) from api/tool/tool_instances.py into new api/tool/tool_lifecycle.py. - Register tool_lifecycle_router in main.py and api/tool/__init__.py. - Extract inline WorkspaceDetailPage components into components/features/workspace/: detail header, tab bars, file/git/tools/settings panels. Slim page from ~446 to ~62 lines. - Update OpenSpec reorganize-long-files tasks to reflect completed work and current source state; mark change completed. - Regenerate project maps. Quality gates: python3 -m py_compile (backend clean), npm run typecheck, npm run lint, npm test -- --run (87 passed), pytest workspace integration and unit tests (27 passed, 1 skipped).
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/pages
|
||||
|
||||
## role
|
||||
Contains all top-level page components that serve as route endpoints for the web application's main navigation areas, each handling a distinct functional domain of the development environment platform.
|
||||
Contains top-level React page components that serve as route endpoints for the web application's primary feature areas, each handling a distinct domain of the workspace management platform.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,10 +4,10 @@ dir: apps/web/src/pages
|
||||
index: apps/web/src/pages/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains all top-level page components that serve as route endpoints for the web application's main navigation areas, each handling a distinct functional domain of the development environment platform.
|
||||
Contains top-level React page components that serve as route endpoints for the web application's primary feature areas, each handling a distinct domain of the workspace management platform.
|
||||
## files
|
||||
- ConfigProfilesPage.tsx | Renders a responsive configuration profiles management page with sidebar list and editor panel for desktop, and a dedicated mobile view for creating, editing, and managing config profiles. | exp: ConfigProfilesPage | dep: react, ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-config-profiles, ../components/features/config-profiles/ConfigProfileListSidebar, ../components/features/config-profiles/ConfigProfileEditorPanel, ../components/features/config-profiles/ConfigProfilesMobileView
|
||||
- DashboardPage.test.tsx | Unit tests for the DashboardPage/ HomePage component verifying overview loading and error retry behavior. | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, ../state/session-operations, DashboardPage, SessionsProvider, SessionOperationsProvider
|
||||
- DashboardPage.test.tsx | Tests the DashboardPage (HomePage) component's rendering, loading states, and error handling with retry functionality | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, ../state/session-operations, ../api/dashboard, ../api/sessions, ../api/projects, ../api/git-repositories, ../api/tool-types
|
||||
- DashboardPage.tsx | Renders a dashboard homepage that displays workspace overview, active/recent sessions, summary statistics, and polling health checks for running instances. | exp: HomePage | dep: react, react-router-dom, ../api/dashboard, ../api/sessions, ../components/data-states, ../components/features/session/session-list, ../hooks/use-instance-actions, ../state/sessions
|
||||
- GitHistoryPage.tsx | Renders a Git commit history page with branch selection, commit list with graph visualization, and a detail panel showing commit metadata, stats, and diffs. | exp: GitHistoryPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../hooks/use-async-data
|
||||
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
||||
@@ -21,13 +21,13 @@ Contains all top-level page components that serve as route endpoints for the web
|
||||
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
||||
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
||||
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
||||
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering workspace headers, tabs, and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
||||
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tabbed navigation for files, git, tools, and settings management. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../components/icon, ../hooks/use-workspaces, ../hooks/use-workspace-files, ../hooks/use-workspace-git, ../hooks/use-workspace-instances, ../hooks/use-mobile-viewport, ../components/features/tool/tool-starter, ../api/workspace-files, ../types/workspace
|
||||
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
||||
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tabbed navigation for files, git, tools, and settings panels, adapting layout for mobile viewports. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-mobile-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
||||
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile (list/detail/create views) and desktop (grid with cards) layouts, supporting CRUD operations and tool launching. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:workspaces.find, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||
## arch
|
||||
Follows a page-based routing architecture with responsive dual-layout pattern (mobile/desktop variants), heavy use of tabbed navigation for complex pages, polling-based live data updates, and CRUD-heavy pages with optimistic UI patterns and confirmation dialogs.
|
||||
Flat page-based architecture with co-located tests, where each page is a self-contained route component implementing responsive mobile/desktop adaptive layouts, tabbed navigation, CRUD operations, and real-time data polling; pages compose shared UI patterns (sidebar/editor splits, card grids, detail panels) and delegate to child components or outlets for nested routing.
|
||||
## tags
|
||||
page, components, react, workspace, features, mobile, hooks, settings
|
||||
page, components, workspace, features, mobile, react, settings, hooks
|
||||
## symbols
|
||||
- WorkspaceDetailPage
|
||||
- WorkspacesPage
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { MemoryRouter, Routes, Route } from "react-router-dom";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -117,7 +123,9 @@ describe("WorkspaceDetailPage", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tab", { name: /settings/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("tab", { name: /settings/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: /settings/i }));
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Icon } from "../components/icon";
|
||||
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 { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||
import { ToolStarter } from "../components/features/tool/tool-starter";
|
||||
import type { FileEntry } from "../api/workspace-files";
|
||||
import type { Workspace } from "../types/workspace";
|
||||
|
||||
type Tab = "files" | "git" | "tools" | "settings";
|
||||
import { WorkspaceDetailHeader } from "../components/features/workspace/workspace-detail-header";
|
||||
import {
|
||||
WorkspaceMobileTabBar,
|
||||
WorkspaceTabBar,
|
||||
type WorkspaceTab,
|
||||
} from "../components/features/workspace/workspace-tab-bar";
|
||||
import { WorkspaceFilePanel } from "../components/features/workspace/workspace-file-panel";
|
||||
import { WorkspaceGitPanel } from "../components/features/workspace/workspace-git-panel";
|
||||
import { WorkspaceToolsPanel } from "../components/features/workspace/workspace-tools-panel";
|
||||
import { WorkspaceSettingsPanel } from "../components/features/workspace/workspace-settings-panel";
|
||||
|
||||
export function WorkspaceDetailPage() {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const [activeTab, setActiveTab] = useState<Tab>("files");
|
||||
const [activeTab, setActiveTab] = useState<WorkspaceTab>("files");
|
||||
const isMobile = useMobileViewport();
|
||||
|
||||
const { workspaces, loading: wsLoading } = useWorkspaces();
|
||||
@@ -37,410 +38,25 @@ export function WorkspaceDetailPage() {
|
||||
|
||||
return (
|
||||
<div className={`workspace-detail ${isMobile ? "mobile" : ""}`}>
|
||||
<WorkspaceHeader workspace={workspace} />
|
||||
<TabBar active={activeTab} onChange={setActiveTab} />
|
||||
<WorkspaceDetailHeader workspace={workspace} />
|
||||
<WorkspaceTabBar active={activeTab} onChange={setActiveTab} />
|
||||
<div className="workspace-content">
|
||||
{activeTab === "files" && <FilesTab workspaceId={workspace.id} />}
|
||||
{activeTab === "git" && <GitTab workspaceId={workspace.id} />}
|
||||
{activeTab === "tools" && <ToolsTab workspace={workspace} />}
|
||||
{activeTab === "settings" && <SettingsTab workspace={workspace} />}
|
||||
{activeTab === "files" && (
|
||||
<WorkspaceFilePanel workspaceId={workspace.id} />
|
||||
)}
|
||||
{activeTab === "git" && (
|
||||
<WorkspaceGitPanel workspaceId={workspace.id} />
|
||||
)}
|
||||
{activeTab === "tools" && (
|
||||
<WorkspaceToolsPanel workspace={workspace} />
|
||||
)}
|
||||
{activeTab === "settings" && (
|
||||
<WorkspaceSettingsPanel workspace={workspace} />
|
||||
)}
|
||||
</div>
|
||||
{isMobile && <MobileTabBar active={activeTab} onChange={setActiveTab} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceHeader({
|
||||
workspace,
|
||||
}: {
|
||||
workspace: {
|
||||
name: string;
|
||||
repo_name: string;
|
||||
project_name: string;
|
||||
branch: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<header className="workspace-header">
|
||||
<div className="workspace-breadcrumb">
|
||||
<span>{workspace.project_name}</span>
|
||||
<span className="sep">/</span>
|
||||
<span>{workspace.repo_name}</span>
|
||||
<span className="sep">/</span>
|
||||
<strong>{workspace.name}</strong>
|
||||
</div>
|
||||
<div className="workspace-actions">
|
||||
<span className="branch-badge">
|
||||
<Icon name="branch" size="sm" /> {workspace.branch}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function TabBar({
|
||||
active,
|
||||
onChange,
|
||||
}: {
|
||||
active: Tab;
|
||||
onChange: (t: Tab) => void;
|
||||
}) {
|
||||
const tabs: { id: Tab; label: string; icon: string }[] = [
|
||||
{ id: "files", label: "Files", icon: "folder" },
|
||||
{ id: "git", label: "Git", icon: "branch" },
|
||||
{ id: "tools", label: "Tools", icon: "terminal" },
|
||||
{ id: "settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="tab-bar" role="tablist">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab ${active === tab.id ? "active" : ""}`}
|
||||
onClick={() => onChange(tab.id)}
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
>
|
||||
<Icon
|
||||
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
|
||||
size="sm"
|
||||
/>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileTabBar({
|
||||
active,
|
||||
onChange,
|
||||
}: {
|
||||
active: Tab;
|
||||
onChange: (t: Tab) => void;
|
||||
}) {
|
||||
const tabs: { id: Tab; label: string; icon: string }[] = [
|
||||
{ id: "files", label: "Files", icon: "folder" },
|
||||
{ id: "git", label: "Git", icon: "branch" },
|
||||
{ id: "tools", label: "Tools", icon: "terminal" },
|
||||
{ id: "settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="mobile-tab-bar" role="tablist">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`mobile-tab ${active === tab.id ? "active" : ""}`}
|
||||
onClick={() => onChange(tab.id)}
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
>
|
||||
<Icon
|
||||
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
|
||||
/>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Files Tab ─── */
|
||||
|
||||
function FilesTab({ workspaceId }: { workspaceId: string }) {
|
||||
const {
|
||||
entries,
|
||||
content,
|
||||
currentPath,
|
||||
loadFile,
|
||||
saveFile,
|
||||
loading,
|
||||
error,
|
||||
navigateTo,
|
||||
} = useWorkspaceFiles(workspaceId);
|
||||
const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId);
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [editContent, setEditContent] = useState<string | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
|
||||
const handleSelect = (entry: FileEntry) => {
|
||||
if (entry.type === "directory") {
|
||||
setSelectedPath(null);
|
||||
setIsEditing(false);
|
||||
setEditContent(null);
|
||||
navigateTo(entry.path);
|
||||
return;
|
||||
}
|
||||
setSelectedPath(entry.path);
|
||||
setIsEditing(false);
|
||||
setEditContent(null);
|
||||
loadFile(entry.path);
|
||||
};
|
||||
|
||||
const navigateUp = () => {
|
||||
if (!currentPath) return;
|
||||
const parentPath = currentPath.split("/").slice(0, -1).join("/");
|
||||
navigateTo(parentPath);
|
||||
setSelectedPath(null);
|
||||
setIsEditing(false);
|
||||
setEditContent(null);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
if (content !== null) {
|
||||
setEditContent(content);
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (selectedPath && editContent !== null) {
|
||||
await saveFile(selectedPath, editContent, commitMessage || undefined);
|
||||
setIsEditing(false);
|
||||
setCommitMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="files-tab">
|
||||
{status && (
|
||||
<div className="git-toolbar">
|
||||
<div className="git-toolbar-status">
|
||||
{status.modified.length > 0 && (
|
||||
<span className="status-modified">
|
||||
M {status.modified.length}
|
||||
</span>
|
||||
)}
|
||||
{status.added.length > 0 && (
|
||||
<span className="status-added">A {status.added.length}</span>
|
||||
)}
|
||||
{status.deleted.length > 0 && (
|
||||
<span className="status-deleted">D {status.deleted.length}</span>
|
||||
)}
|
||||
{status.untracked.length > 0 && (
|
||||
<span className="status-untracked">
|
||||
? {status.untracked.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="git-toolbar-actions">
|
||||
<input
|
||||
type="text"
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
placeholder="Commit message"
|
||||
/>
|
||||
<button
|
||||
onClick={() => commit(commitMessage)}
|
||||
disabled={!commitMessage}
|
||||
>
|
||||
Commit
|
||||
</button>
|
||||
<button onClick={push}>Push</button>
|
||||
<button onClick={pull}>Pull</button>
|
||||
<button onClick={fetch}>Fetch</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="files-split">
|
||||
<div className="file-tree">
|
||||
{currentPath && (
|
||||
<button
|
||||
className="tree-entry tree-up"
|
||||
onClick={navigateUp}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="folder" size="sm" /> ..
|
||||
</button>
|
||||
)}
|
||||
{loading && <p className="muted">Loading...</p>}
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
{entries.map((entry) => (
|
||||
<button
|
||||
key={entry.path}
|
||||
className={`tree-entry ${entry.type} ${selectedPath === entry.path ? "selected" : ""}`}
|
||||
onClick={() => handleSelect(entry)}
|
||||
type="button"
|
||||
>
|
||||
<Icon
|
||||
name={entry.type === "directory" ? "folder" : "file"}
|
||||
size="sm"
|
||||
/>
|
||||
{entry.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="file-viewer">
|
||||
{selectedPath ? (
|
||||
<>
|
||||
<div className="file-viewer-header">
|
||||
<span>{selectedPath}</span>
|
||||
{!isEditing && <button onClick={handleEdit}>Edit</button>}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<textarea
|
||||
className="file-editor"
|
||||
value={editContent || ""}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
/>
|
||||
<div className="file-editor-actions">
|
||||
<button onClick={() => setIsEditing(false)}>Cancel</button>
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<pre className="file-content">{content || "Loading..."}</pre>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="muted">Select a file to view</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Git Tab ─── */
|
||||
|
||||
function GitTab({ workspaceId }: { workspaceId: string }) {
|
||||
const { history, branches, currentBranch, checkout, loading, error } =
|
||||
useWorkspaceGit(workspaceId);
|
||||
|
||||
return (
|
||||
<div className="git-tab">
|
||||
<div className="git-tab-header">
|
||||
<select
|
||||
value={currentBranch}
|
||||
onChange={(e) => checkout(e.target.value)}
|
||||
>
|
||||
{branches.map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{loading && <p className="muted">Loading history...</p>}
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
<div className="commit-history">
|
||||
{history.map((commit) => (
|
||||
<div key={commit.hash} className="commit-row">
|
||||
<span className="commit-hash">{commit.hash.slice(0, 7)}</span>
|
||||
<span className="commit-message">{commit.message}</span>
|
||||
<span className="commit-author">{commit.author}</span>
|
||||
<span className="commit-date">{commit.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Tools Tab ─── */
|
||||
|
||||
function ToolsTab({ workspace }: { workspace: Workspace }) {
|
||||
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="tools-tab">
|
||||
{loading && <p className="muted">Loading instances...</p>}
|
||||
{instances.length === 0 ? (
|
||||
<div className="empty-state-card">
|
||||
<Icon name="terminal" size="lg" />
|
||||
<h3>No tools running</h3>
|
||||
<p>Start a tool to begin coding in this workspace</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Start Tool
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="instances-grid">
|
||||
{instances.map((instance) => (
|
||||
<div
|
||||
key={instance.id}
|
||||
className={`instance-card ${instance.status}`}
|
||||
>
|
||||
<h4>{instance.display_name}</h4>
|
||||
<span className="status-badge">{instance.status}</span>
|
||||
{instance.url && (
|
||||
<a
|
||||
href={instance.url}
|
||||
target={`instance-${instance.id}`}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Start Another Tool
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{showModal && (
|
||||
<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>
|
||||
{isMobile && (
|
||||
<WorkspaceMobileTabBar active={activeTab} onChange={setActiveTab} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Settings Tab ─── */
|
||||
|
||||
function SettingsTab({ workspace }: { workspace: Workspace }) {
|
||||
return (
|
||||
<div className="settings-tab">
|
||||
<div className="settings-section">
|
||||
<h3>Workspace Info</h3>
|
||||
<div className="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" value={workspace.name} readOnly />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Branch</label>
|
||||
<input type="text" value={workspace.branch} readOnly />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Path</label>
|
||||
<input type="text" value={workspace.path} readOnly />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Status</label>
|
||||
<span className={`status-badge ${workspace.status}`}>
|
||||
{workspace.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Created</label>
|
||||
<span>{workspace.created_at}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user