Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7070867393 | |||
| d472c41092 | |||
| a9e2dd3552 | |||
| 6553a8845b | |||
| 994b1cf3b7 | |||
| 6aea83bf17 | |||
| 8d51877afa |
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Icon } from "../../icon";
|
||||
import { validateGitUrl } from "../../../api/config_profiles";
|
||||
import type { GitMount, GitMountMapping } from "../../../api/config_profiles";
|
||||
import { validateGitUrl } from "../../../api/config-profiles";
|
||||
import type { GitMount, GitMountMapping } from "../../../api/config-profiles";
|
||||
|
||||
interface GitMountEditorProps {
|
||||
mounts: GitMount[];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, act } from "@testing-library/react";
|
||||
import { EventToastBridge } from "./event-toast-bridge";
|
||||
import { useEventContext } from "../state/events";
|
||||
import { useEventContext } from "../../../state/events";
|
||||
import { getUserConfig } from "../../../api/settings";
|
||||
import { handleEventToast } from "./toast-rules";
|
||||
import { handleEventToast } from "../../toast-rules";
|
||||
import type { InstanceEventPayload } from "../../../types/events";
|
||||
|
||||
vi.mock("../state/events", () => ({
|
||||
vi.mock("../../../state/events", () => ({
|
||||
useEventContext: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -14,8 +14,8 @@ vi.mock("../../../api/settings", () => ({
|
||||
getUserConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./toast-rules", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./toast-rules")>();
|
||||
vi.mock("../../toast-rules", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../toast-rules")>();
|
||||
return {
|
||||
...actual,
|
||||
handleEventToast: vi.fn(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { NotificationCenter } from "./notification-center";
|
||||
import { NotificationProvider } from "../state/notifications";
|
||||
import { NotificationProvider } from "../../../state/notifications";
|
||||
|
||||
vi.mock("../../../api/notifications", () => ({
|
||||
getNotifications: vi.fn(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNotifications } from "../../../hooks/use-notifications";
|
||||
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
||||
import { NotificationItem } from "./notification-item";
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
@@ -22,15 +24,39 @@ export function NotificationCenter({
|
||||
setIsDropdownOpen,
|
||||
} = useNotifications();
|
||||
|
||||
const isMobile = useMobileViewport();
|
||||
const bellRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!bellRef.current) return;
|
||||
const rect = bellRef.current.getBoundingClientRect();
|
||||
if (isMobile) {
|
||||
setDropdownStyle({
|
||||
top: rect.bottom + 6,
|
||||
left: "1rem",
|
||||
right: "1rem",
|
||||
});
|
||||
} else {
|
||||
setDropdownStyle({
|
||||
top: rect.bottom + 6,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDropdownOpen) return;
|
||||
|
||||
updatePosition();
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(e.target as Node)
|
||||
!dropdownRef.current.contains(target) &&
|
||||
!bellRef.current?.contains(target)
|
||||
) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
@@ -42,14 +68,20 @@ export function NotificationCenter({
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
updatePosition();
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleMouseDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleMouseDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [isDropdownOpen, setIsDropdownOpen]);
|
||||
}, [isDropdownOpen, setIsDropdownOpen, updatePosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDropdownOpen) {
|
||||
@@ -66,6 +98,7 @@ export function NotificationCenter({
|
||||
return (
|
||||
<div className="notification-center">
|
||||
<button
|
||||
ref={bellRef}
|
||||
type="button"
|
||||
className="notification-bell"
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
@@ -79,56 +112,68 @@ export function NotificationCenter({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="dialog"
|
||||
aria-label="Notifications"
|
||||
className="notification-dropdown"
|
||||
>
|
||||
<div className="notification-dropdown-header">
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
|
||||
<ul className="notification-list">
|
||||
{notifications.length === 0 ? (
|
||||
<li className="notification-empty">No notifications</li>
|
||||
) : (
|
||||
notifications.map((n) => (
|
||||
<NotificationItem
|
||||
key={n.id}
|
||||
notification={n}
|
||||
onMarkRead={markRead}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
))
|
||||
{isDropdownOpen &&
|
||||
createPortal(
|
||||
<>
|
||||
{isMobile && (
|
||||
<div
|
||||
className="notification-dropdown-backdrop"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="dialog"
|
||||
aria-label="Notifications"
|
||||
className="notification-dropdown"
|
||||
style={dropdownStyle}
|
||||
>
|
||||
<div className="notification-dropdown-header">
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
|
||||
{notifications.length > 0 && (
|
||||
<div className="notification-dropdown-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="notification-mark-all"
|
||||
onClick={() => {
|
||||
void markAllRead();
|
||||
}}
|
||||
>
|
||||
Mark all as read
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="notification-clear-all"
|
||||
onClick={() => {
|
||||
void clearAll();
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<ul className="notification-list">
|
||||
{notifications.length === 0 ? (
|
||||
<li className="notification-empty">No notifications</li>
|
||||
) : (
|
||||
notifications.map((n) => (
|
||||
<NotificationItem
|
||||
key={n.id}
|
||||
notification={n}
|
||||
onMarkRead={markRead}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{notifications.length > 0 && (
|
||||
<div className="notification-dropdown-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="notification-mark-all"
|
||||
onClick={() => {
|
||||
void markAllRead();
|
||||
}}
|
||||
>
|
||||
Mark all as read
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="notification-clear-all"
|
||||
onClick={() => {
|
||||
void clearAll();
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Project } from "../../../types";
|
||||
import { listRepositoryBranches, type GitRepository, type Branch } from "../../../api/git-repositories";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
||||
import { listConfigProfiles, type ConfigProfile } from "../../../api/config_profiles";
|
||||
import { listConfigProfiles, type ConfigProfile } from "../../../api/config-profiles";
|
||||
|
||||
interface CreateSessionFormProps {
|
||||
projects: Project[];
|
||||
|
||||
@@ -8,7 +8,6 @@ import React, {
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
import { WebglAddon } from "xterm-addon-webgl";
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
import {
|
||||
@@ -310,25 +309,13 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
term.loadAddon(fitAddon);
|
||||
term.loadAddon(new WebLinksAddon());
|
||||
|
||||
// Load WebGL renderer for GPU acceleration, fall back to DOM
|
||||
let webglAddon: WebglAddon | null = null;
|
||||
try {
|
||||
webglAddon = new WebglAddon();
|
||||
term.loadAddon(webglAddon);
|
||||
webglAddon.onContextLoss(() => {
|
||||
console.warn("WebGL context lost, falling back to DOM renderer");
|
||||
try {
|
||||
webglAddon?.dispose();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
webglAddon = null;
|
||||
// Trigger a refit since cell dimensions may differ
|
||||
requestAnimationFrame(() => fitTerminal());
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("WebGL renderer failed to load, using DOM renderer", e);
|
||||
}
|
||||
// NOTE: WebGL renderer disabled.
|
||||
// The WebGL addon causes black-on-black rendering artifacts with
|
||||
// tmux/vim reverse-video (inverse color) sequences on desktop.
|
||||
// Mobile already uses the DOM renderer (WebGL fails there), which
|
||||
// handles these color attributes correctly. The DOM renderer is
|
||||
// fast enough for typical terminal workloads.
|
||||
// See: xterm.js WebGL known issues with reverse video / minimumContrastRatio
|
||||
|
||||
const container = terminalRef.current;
|
||||
|
||||
@@ -585,16 +572,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
window.clearInterval(heartbeatCheckRef.current);
|
||||
heartbeatCheckRef.current = null;
|
||||
}
|
||||
// Dispose WebGL addon BEFORE the terminal to avoid race with
|
||||
// RenderService.setRenderer accessing a disposed renderer
|
||||
if (webglAddon) {
|
||||
try {
|
||||
webglAddon.dispose();
|
||||
} catch {
|
||||
// Ignore disposal errors from partially torn-down terminal
|
||||
}
|
||||
webglAddon = null;
|
||||
}
|
||||
try {
|
||||
term.dispose();
|
||||
} catch {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "../../../api/sessions";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
import { CreateSessionForm } from "../session/create-session-form";
|
||||
import { listConfigProfiles, type ConfigProfile } from "../../../api/config_profiles";
|
||||
import { listConfigProfiles, type ConfigProfile } from "../../../api/config-profiles";
|
||||
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
||||
import { useEventContext } from "../../../state/events";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { extractErrorMessage } from "../../../utils/errors";
|
||||
import {
|
||||
compileToolDefinition,
|
||||
type ToolDefinitionManifest,
|
||||
} from "../../../api/tool_definitions";
|
||||
} from "../../../api/tool-definitions";
|
||||
|
||||
interface PackageEntry {
|
||||
name: string;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 { 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";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type ConfigProfile,
|
||||
type CreateConfigProfileRequest,
|
||||
type ResolvedProfile,
|
||||
} from "../api/config_profiles";
|
||||
} from "../api/config-profiles";
|
||||
import { listProjects } from "../api/projects";
|
||||
import type { ProjectWithRepos } from "../types";
|
||||
import { listToolTypes, type ToolType } from "../api/tool-types";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { HomePage } from "./dashboard";
|
||||
import { HomePage } from "./DashboardPage";
|
||||
|
||||
const mockDashboard = vi.fn();
|
||||
const mockSessions = vi.fn();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-li
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ProjectsPage } from "./projects";
|
||||
import { ProjectsPage } from "./ProjectsPage";
|
||||
import * as projectsApi from "../api/projects";
|
||||
|
||||
const mockProjects = [
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
listToolDefinitions,
|
||||
updateToolDefinition,
|
||||
type ToolDefinitionManifest,
|
||||
} from "../api/tool_definitions";
|
||||
} from "../api/tool-definitions";
|
||||
import { ManifestEditor } from "../components/features/tool/manifest-editor";
|
||||
type Status = "loading" | "ready" | "error";
|
||||
|
||||
|
||||
+14
-5
@@ -4670,9 +4670,7 @@ a:active,
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
position: fixed;
|
||||
width: 360px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 480px;
|
||||
@@ -4840,9 +4838,20 @@ a:active,
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.notification-dropdown-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
max-width: 360px;
|
||||
width: auto;
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
max-width: none;
|
||||
border-radius: 12px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-04
|
||||
@@ -0,0 +1,73 @@
|
||||
## Why
|
||||
|
||||
After the backend-frontend refactoring (commits `0591b00` through `8c7affc`), the codebase gained proper directory structure but several files grew into monoliths. The `main` branch (pre-refactor baseline at `5ed5e1c`) kept pages thin by delegating to extracted components. On `dev`, new features were added inline, causing pages and routers to absorb responsibilities that belong in components or services.
|
||||
|
||||
### Problem Files (Frontend)
|
||||
|
||||
| File | Lines | Problem |
|
||||
|------|-------|---------|
|
||||
| `pages/ToolWorkshopPage.tsx` | **1,269** | Merged 3 tab components inline (ToolTypes, ToolConfigs, ConfigFolders) |
|
||||
| `pages/ConfigProfilesPage.tsx` | **1,611** | List, detail, edit, create, and mobile views all in one file |
|
||||
| `pages/TerminalPage.tsx` | **571** | Session tabs, keyboard shortcuts, fullscreen, mobile overlay, special keys all inline |
|
||||
| `pages/RepoWorkspacePage.tsx` | **505** | File editor, git toolbar, workspace header, sidebar logic inline |
|
||||
| `pages/SettingsPage.tsx` | **284** | Settings nav + multiple setting sections inline |
|
||||
| `pages/SshKeysPage.tsx` | **277** | List and create inline |
|
||||
|
||||
### Problem Files (Backend)
|
||||
|
||||
| File | Lines | Problem |
|
||||
|------|-------|---------|
|
||||
| `api/tool/tool_instances.py` | **2,900** | CRUD, Docker lifecycle, WebSocket proxy, terminal sessions, instance proxy all in one router |
|
||||
| `api/project/git_repositories.py` | **1,588** | HTTP endpoints mixed with git command orchestration |
|
||||
| `api/config/config_profiles.py` | **842** | CRUD + validation + resolver + mount/include management |
|
||||
|
||||
### What `main` Did Differently
|
||||
|
||||
`main` at `5ed5e1c`:
|
||||
- `ToolWorkshopPage.tsx` = **77 lines** (just a tab switcher, tabs imported from `features/tool-workshop/`)
|
||||
- `TerminalPage.tsx` = **38 lines** (just a wrapper around `TerminalComponent`)
|
||||
- `api/tool_instances.py` = **284 lines** (HTTP endpoints only)
|
||||
- `api/terminal.py` = **158 lines** (separate WebSocket router)
|
||||
|
||||
## What Changes
|
||||
|
||||
Restore the **thin-page / thin-router / fat-component** pattern from `main`, adapted to current `dev` features:
|
||||
|
||||
1. **Frontend page extraction** — Split monolithic pages into:
|
||||
- Page shell (orchestrator, 50-150 lines)
|
||||
- Tab components (for tabbed pages)
|
||||
- List / Detail / Edit / Create components (for CRUD pages)
|
||||
- Mobile-specific views (extracted, not inline)
|
||||
|
||||
2. **Backend router slimming** — Split `tool_instances.py` into:
|
||||
- `tool_instances.py` — CRUD endpoints only
|
||||
- `tool_lifecycle.py` — Start/stop/restart/delete logic
|
||||
- Move terminal WebSocket back to dedicated `terminal.py`
|
||||
|
||||
3. **Git repository router** — Extract git command orchestration into `services/git/`
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- None (pure structural refactor)
|
||||
|
||||
### Modified Capabilities
|
||||
- `frontend-structure`: Pages become orchestrators; components carry the UI logic
|
||||
- `backend-structure`: Routers become HTTP-only; services carry business logic
|
||||
|
||||
## Impact
|
||||
|
||||
- **Frontend**: New `features/tool-workshop/` tab components, new `features/config-profiles/` components, `features/terminal/` session manager, etc.
|
||||
- **Backend**: New `api/tool/tool_lifecycle.py`, `api/tool/terminal.py`, slimmer `api/tool/tool_instances.py`
|
||||
- **Tests**: Test files may need import path updates (component moved → test follows)
|
||||
|
||||
## Exclusions (Already Done / Out of Scope)
|
||||
|
||||
- Directory structure already exists (`features/`, `services/`, etc.)
|
||||
- Schema extraction already done (`schemas/` subpackages)
|
||||
- Model subpackages already done (`models/` subpackages)
|
||||
- API router subpackages already done (`api/tool/`, `api/project/`, etc.)
|
||||
- File naming already done (kebab-case APIs, PascalCase pages)
|
||||
- No behavioral changes to any endpoint or UI flow
|
||||
- No database schema changes
|
||||
- No new features
|
||||
@@ -0,0 +1,128 @@
|
||||
## Scope
|
||||
|
||||
This change is a **pure structural refactoring** to split monolithic pages and routers into focused components and services. No API contracts, database schemas, or user-facing behaviors change.
|
||||
|
||||
### In Scope
|
||||
|
||||
#### 1. Frontend Page Extraction
|
||||
|
||||
Split the following pages into a thin page shell + extracted components:
|
||||
|
||||
**`pages/ToolWorkshopPage.tsx` (1,269 → ~80 lines)**
|
||||
- Extract `ToolTypesTab` → `components/features/tool-workshop/ToolTypesTab.tsx`
|
||||
- Extract `ToolConfigsTab` → `components/features/tool-workshop/ToolConfigsTab.tsx`
|
||||
- Extract `ConfigFoldersTab` → `components/features/tool-workshop/ConfigFoldersTab.tsx`
|
||||
- Page becomes: tab switcher only, imports the 3 tabs
|
||||
|
||||
**`pages/ConfigProfilesPage.tsx` (1,611 → ~80 lines)**
|
||||
- Extract `ConfigProfileListView` → list view + mobile list view
|
||||
- Extract `ConfigProfileDetailView` → detail view with edit toggle
|
||||
- Extract `ConfigProfileEditForm` → edit/create form
|
||||
- Extract `ConfigProfileMobileView` → mobile view state machine wrapper
|
||||
- Page becomes: router between list/detail/edit views
|
||||
|
||||
**`pages/TerminalPage.tsx` (571 → ~80 lines)**
|
||||
- Extract `TerminalSessionManager` → session tabs + auto-create logic
|
||||
- Extract `TerminalKeyboardShortcuts` → shortcut handler hook (already exists, just use it)
|
||||
- Extract `MobileTerminalOverlay` → mobile overlay toolbar + tabs
|
||||
- Page becomes: choose between desktop (`TerminalComponent` + `TerminalSessionTabs`) and mobile (`MobileTerminalOverlay` + `TerminalComponent`) wrappers
|
||||
|
||||
**`pages/SettingsPage.tsx` (284 → ~80 lines)**
|
||||
- Extract `SettingsNavigation` → settings nav sidebar
|
||||
- Extract `GeneralSettingsTab`, `SSHKeysTab` (already separate pages, but move sections into components if inline)
|
||||
- Page becomes: nav + `<Outlet>` for nested routes
|
||||
|
||||
**`pages/SshKeysPage.tsx` (277 → ~80 lines)**
|
||||
- Extract `SSHKeyList` → list with actions
|
||||
- Extract `SSHKeyCreateForm` → create form
|
||||
- Page becomes: layout wrapper + conditionally render list or form
|
||||
|
||||
**`pages/RepoWorkspacePage.tsx` (505 → ~150 lines)**
|
||||
- Extract `WorkspaceLayout` → sidebar + main content layout
|
||||
- Page becomes: data loader + layout wrapper
|
||||
|
||||
**`pages/ProjectsPage.tsx` (433 → ~100 lines)**
|
||||
- Extract `ProjectList` → list with cards
|
||||
- Extract `ProjectCreateDialog` → create form in dialog
|
||||
- Extract `ProjectEditDialog` → edit form in dialog
|
||||
- Page becomes: data loader + layout + dialog state manager
|
||||
|
||||
#### 2. CSS Reorganization
|
||||
|
||||
**`styles.css` (5,683 lines → deleted)**
|
||||
- Restore `styles/` directory with extracted files:
|
||||
- `styles/tokens.css` — CSS custom properties (colors, spacing, typography)
|
||||
- `styles/global.css` — global reset, body, shell layout
|
||||
- `styles/utilities.css` — utility classes (.stack, .card, .muted, etc.)
|
||||
- `styles/syntax-highlight.css` — code highlighting
|
||||
- Restore `styles/pages/*.css` — page-specific styles:
|
||||
- `styles/pages/dashboard.css`
|
||||
- `styles/pages/projects.css`
|
||||
- `styles/pages/sessions.css`
|
||||
- `styles/pages/settings.css`
|
||||
- `styles/pages/ssh-keys.css`
|
||||
- `styles/pages/git-history.css`
|
||||
- `styles/pages/repo-workspace.css`
|
||||
- Restore component CSS modules:
|
||||
- `components/features/terminal/TerminalComponent.module.css`
|
||||
- `components/features/git/GitToolbar.module.css`
|
||||
- `components/features/git/CommitDialog.module.css`
|
||||
- `components/features/git/MergeDialog.module.css`
|
||||
- `components/features/git/FileEditor.module.css`
|
||||
- `components/features/git/FileBrowser.module.css`
|
||||
- `components/features/git/FileViewer.module.css`
|
||||
- `components/features/git/CommitPanel.module.css`
|
||||
- `components/features/session/InstanceList.module.css`
|
||||
- `components/features/settings/SettingsTabLayout.module.css`
|
||||
- `components/layout/AppShell.module.css`
|
||||
- Update all component imports to use `import styles from './ComponentName.module.css'`
|
||||
- Update `main.tsx` to import `styles/tokens.css`, `styles/global.css`, `styles/utilities.css`, `styles/syntax-highlight.css`
|
||||
- Update each page to import its `styles/pages/*.css`
|
||||
- Delete monolithic `styles.css`
|
||||
|
||||
#### 3. Backend Router Slimming
|
||||
|
||||
**`api/tool/tool_instances.py` (2,900 → ~300 lines)**
|
||||
- Extract terminal WebSocket handlers → `api/tool/terminal.py` (~400 lines)
|
||||
- Extract instance lifecycle (create/start/stop/delete/restart) → `api/tool/tool_lifecycle.py` (~600 lines)
|
||||
- Keep in `tool_instances.py`: CRUD endpoints (GET list, GET detail, POST, PATCH, DELETE) + instance proxy endpoint
|
||||
|
||||
**`api/project/git_repositories.py` (1,588 → ~300 lines)**
|
||||
- Extract git command orchestration into `services/git/operations.py`
|
||||
- Router keeps: auth, parameter validation, response building, error handling
|
||||
- Service functions: `clone_repo`, `fetch_repo`, `pull_repo`, `push_repo`, `merge_repo`, etc.
|
||||
|
||||
**`api/config/config_profiles.py` (842 → ~200 lines)**
|
||||
- Extract resolver orchestration into `services/config/resolver_service.py`
|
||||
- Extract CRUD helpers into `services/config/crud_service.py`
|
||||
- Router keeps: endpoint definitions, auth, input validation
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Any new features or behavioral changes
|
||||
- Database schema changes (no migrations)
|
||||
- API contract changes (same endpoints, same request/response shapes)
|
||||
- Frontend UI behavior changes (same components, same interactions)
|
||||
- Moving existing `features/` components (already organized)
|
||||
- Renaming files (naming already done)
|
||||
- Changing any CSS rules (only moving them)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. All pages ≤ 150 lines (except `RepoWorkspacePage` which may stay at ~150)
|
||||
2. All API routers ≤ 400 lines
|
||||
3. No monolithic `styles.css` — all CSS in `styles/` directory or `.module.css` files
|
||||
4. All existing tests pass without modification (behavior unchanged)
|
||||
5. All existing API endpoints return identical responses
|
||||
6. Frontend `npm run typecheck` passes
|
||||
7. Frontend `npm run build` passes
|
||||
8. Backend `py_compile` passes on all files
|
||||
9. No import errors in browser console
|
||||
10. File count increases (more files, smaller files)
|
||||
|
||||
## Preconditions
|
||||
|
||||
- `dev` branch is stable (all fixes from this session are committed)
|
||||
- Backend compiles (`py_compile` pass)
|
||||
- Frontend typechecks and builds (`tsc`, `vite build` pass)
|
||||
- Current tests pass (or known failures are documented)
|
||||
@@ -0,0 +1,144 @@
|
||||
## Phase 0: Preparation
|
||||
|
||||
- [ ] 0.1 Verify `dev` builds cleanly (backend `py_compile`, frontend `tsc` + `vite build`)
|
||||
- [ ] 0.2 Document current file sizes for before/after comparison
|
||||
- [ ] 0.3 Create component directory stubs if missing:
|
||||
- `apps/web/src/components/features/tool-workshop/`
|
||||
- `apps/web/src/components/features/config-profiles/`
|
||||
- `apps/web/src/components/features/terminal/`
|
||||
- `apps/web/src/components/features/settings/`
|
||||
- `apps/web/src/components/features/ssh-keys/`
|
||||
- `apps/api/src/services/git/operations.py` (extract from router)
|
||||
- `apps/api/src/services/config/crud_service.py`
|
||||
- `apps/api/src/services/config/resolver_service.py`
|
||||
|
||||
## Phase 1: Backend — Router Slimming
|
||||
|
||||
### 1.1 Terminal WebSocket Extraction
|
||||
- [ ] 1.1.1 Create `api/tool/terminal.py` from terminal WebSocket handlers in `api/tool/tool_instances.py`
|
||||
- [ ] 1.1.2 Move `_handle_terminal_websocket`, `_get_user_from_websocket`, `SessionRef` class
|
||||
- [ ] 1.1.3 Update `main.py` to include `terminal_router` from `api.tool.terminal`
|
||||
- [ ] 1.1.4 Remove terminal routes from `api/tool/tool_instances.py`
|
||||
- [ ] 1.1.5 Verify `py_compile` passes
|
||||
|
||||
### 1.2 Instance Lifecycle Extraction
|
||||
- [ ] 1.2.1 Create `api/tool/tool_lifecycle.py` for start/stop/restart/delete endpoints
|
||||
- [ ] 1.2.2 Extract lifecycle endpoints from `api/tool/tool_instances.py`
|
||||
- [ ] 1.2.3 Update `main.py` to include lifecycle router
|
||||
- [ ] 1.2.4 Verify `py_compile` passes
|
||||
|
||||
### 1.3 Git Repository Router
|
||||
- [ ] 1.3.1 Create `services/git/operations.py` for git command orchestration
|
||||
- [ ] 1.3.2 Extract `clone_repo`, `fetch_repo`, `pull_repo`, `push_repo`, `merge_repo`, `commit_repo` helpers
|
||||
- [ ] 1.3.3 Update `api/project/git_repositories.py` to call service functions
|
||||
- [ ] 1.3.4 Verify `py_compile` passes
|
||||
|
||||
### 1.4 Config Profile Router
|
||||
- [ ] 1.4.1 Extract CRUD helpers into `services/config/crud_service.py`
|
||||
- [ ] 1.4.2 Extract resolver helpers into `services/config/resolver_service.py`
|
||||
- [ ] 1.4.3 Update `api/config/config_profiles.py` to call services
|
||||
- [ ] 1.4.4 Verify `py_compile` passes
|
||||
|
||||
## Phase 2: CSS Reorganization
|
||||
|
||||
### 2.1 Restore `styles/` Directory Structure
|
||||
- [ ] 2.1.1 Create `styles/` directory
|
||||
- [ ] 2.1.2 Extract `styles/tokens.css` from `styles.css` — CSS custom properties
|
||||
- [ ] 2.1.3 Extract `styles/global.css` from `styles.css` — global reset, body, shell layout
|
||||
- [ ] 2.1.4 Extract `styles/utilities.css` from `styles.css` — utility classes (.stack, .card, .muted, .dialog, etc.)
|
||||
- [ ] 2.1.5 Extract `styles/syntax-highlight.css` from `styles.css` — code highlighting
|
||||
- [ ] 2.1.6 Update `main.tsx` to import: `styles/tokens.css`, `styles/global.css`, `styles/utilities.css`, `styles/syntax-highlight.css`
|
||||
- [ ] 2.1.7 Verify build passes
|
||||
|
||||
### 2.2 Restore Page-Specific CSS
|
||||
- [ ] 2.2.1 Extract `styles/pages/dashboard.css` from `styles.css`
|
||||
- [ ] 2.2.2 Extract `styles/pages/projects.css` from `styles.css`
|
||||
- [ ] 2.2.3 Extract `styles/pages/sessions.css` from `styles.css`
|
||||
- [ ] 2.2.4 Extract `styles/pages/settings.css` from `styles.css`
|
||||
- [ ] 2.2.5 Extract `styles/pages/ssh-keys.css` from `styles.css`
|
||||
- [ ] 2.2.6 Extract `styles/pages/git-history.css` from `styles.css`
|
||||
- [ ] 2.2.7 Extract `styles/pages/repo-workspace.css` from `styles.css`
|
||||
- [ ] 2.2.8 Update each page to import its page CSS
|
||||
- [ ] 2.2.9 Verify build passes
|
||||
|
||||
### 2.3 Restore Component CSS Modules
|
||||
- [ ] 2.3.1 Create `components/features/terminal/TerminalComponent.module.css` from terminal styles in `styles.css`
|
||||
- [ ] 2.3.2 Create `components/features/git/GitToolbar.module.css` from git toolbar styles in `styles.css`
|
||||
- [ ] 2.3.3 Create `components/features/git/CommitDialog.module.css` from commit dialog styles in `styles.css`
|
||||
- [ ] 2.3.4 Create `components/features/git/MergeDialog.module.css` from merge dialog styles in `styles.css`
|
||||
- [ ] 2.3.5 Create `components/features/git/FileEditor.module.css` from file editor styles in `styles.css`
|
||||
- [ ] 2.3.6 Create `components/features/git/FileBrowser.module.css` from file browser styles in `styles.css`
|
||||
- [ ] 2.3.7 Create `components/features/git/FileViewer.module.css` from file viewer styles in `styles.css`
|
||||
- [ ] 2.3.8 Create `components/features/git/CommitPanel.module.css` from commit panel styles in `styles.css`
|
||||
- [ ] 2.3.9 Create `components/features/session/InstanceList.module.css` from instance list styles in `styles.css`
|
||||
- [ ] 2.3.10 Create `components/features/settings/SettingsTabLayout.module.css` from settings tab layout styles in `styles.css`
|
||||
- [ ] 2.3.11 Create `components/layout/AppShell.module.css` from shell styles in `styles.css`
|
||||
- [ ] 2.3.12 Update each component to use `import styles from './ComponentName.module.css'`
|
||||
- [ ] 2.3.13 Remove extracted styles from `styles.css`
|
||||
- [ ] 2.3.14 Verify build passes
|
||||
|
||||
### 2.4 Verify and Delete Monolith
|
||||
- [ ] 2.4.1 Confirm `styles.css` is empty (or only has truly unclassifiable styles)
|
||||
- [ ] 2.4.2 Delete `styles.css`
|
||||
- [ ] 2.4.3 Verify build passes
|
||||
- [ ] 2.4.4 Verify no visual regressions
|
||||
|
||||
## Phase 3: Frontend — Tool Workshop Page
|
||||
|
||||
- [ ] 3.1 Extract `ToolTypesTab` from `pages/ToolWorkshopPage.tsx` into `components/features/tool-workshop/ToolTypesTab.tsx`
|
||||
- [ ] 3.2 Extract `ToolConfigsTab` into `components/features/tool-workshop/ToolConfigsTab.tsx`
|
||||
- [ ] 3.3 Extract `ConfigFoldersTab` into `components/features/tool-workshop/ConfigFoldersTab.tsx`
|
||||
- [ ] 3.4 Slim `pages/ToolWorkshopPage.tsx` to ~80 lines (tab switcher only)
|
||||
- [ ] 3.5 Update imports in all consumers
|
||||
- [ ] 3.6 Verify `tsc --noEmit` and `npm run build` pass
|
||||
|
||||
## Phase 4: Frontend — Config Profiles Page
|
||||
|
||||
- [ ] 4.1 Extract `ConfigProfileListView` into `components/features/config-profiles/ConfigProfileListView.tsx`
|
||||
- [ ] 4.2 Extract `ConfigProfileDetailView` into `components/features/config-profiles/ConfigProfileDetailView.tsx`
|
||||
- [ ] 4.3 Extract `ConfigProfileEditForm` into `components/features/config-profiles/ConfigProfileEditForm.tsx`
|
||||
- [ ] 4.4 Extract `ConfigProfileMobileView` into `components/features/config-profiles/ConfigProfileMobileView.tsx`
|
||||
- [ ] 4.5 Slim `pages/ConfigProfilesPage.tsx` to ~80 lines
|
||||
- [ ] 4.6 Update imports
|
||||
- [ ] 4.7 Verify `tsc --noEmit` and `npm run build` pass
|
||||
|
||||
## Phase 5: Frontend — Terminal Page
|
||||
|
||||
- [ ] 5.1 Extract `TerminalSessionManager` (tabs + auto-create) into `components/features/terminal/TerminalSessionManager.tsx`
|
||||
- [ ] 5.2 Extract `MobileTerminalOverlay` into `components/features/terminal/MobileTerminalOverlay.tsx`
|
||||
- [ ] 5.3 Extract fullscreen keyboard shortcut handler into `hooks/use-terminal-shortcuts.ts`
|
||||
- [ ] 5.4 Slim `pages/TerminalPage.tsx` to ~80 lines
|
||||
- [ ] 5.5 Update imports
|
||||
- [ ] 5.6 Verify `tsc --noEmit` and `npm run build` pass
|
||||
|
||||
## Phase 6: Frontend — Settings & SSH Keys Pages
|
||||
|
||||
- [ ] 6.1 Extract `SettingsNavigation` into `components/features/settings/SettingsNavigation.tsx`
|
||||
- [ ] 6.2 Slim `pages/SettingsPage.tsx` to ~80 lines
|
||||
- [ ] 6.3 Extract `SSHKeyList` into `components/features/ssh-keys/SSHKeyList.tsx`
|
||||
- [ ] 6.4 Extract `SSHKeyCreateForm` into `components/features/ssh-keys/SSHKeyCreateForm.tsx`
|
||||
- [ ] 6.5 Slim `pages/SshKeysPage.tsx` to ~80 lines
|
||||
- [ ] 6.6 Verify `tsc --noEmit` and `npm run build` pass
|
||||
|
||||
## Phase 7: Frontend — Projects & Repo Workspace Pages
|
||||
|
||||
- [ ] 7.1 Extract `ProjectList` into `components/features/project/ProjectList.tsx`
|
||||
- [ ] 7.2 Extract `ProjectCreateDialog` into `components/features/project/ProjectCreateDialog.tsx`
|
||||
- [ ] 7.3 Extract `ProjectEditDialog` into `components/features/project/ProjectEditDialog.tsx`
|
||||
- [ ] 7.4 Slim `pages/ProjectsPage.tsx` to ~100 lines
|
||||
- [ ] 7.5 Extract `WorkspaceLayout` into `components/features/workspace/WorkspaceLayout.tsx`
|
||||
- [ ] 7.6 Slim `pages/RepoWorkspacePage.tsx` to ~150 lines
|
||||
- [ ] 7.7 Verify `tsc --noEmit` and `npm run build` pass
|
||||
|
||||
## Phase 8: Integration and Verification
|
||||
|
||||
- [ ] 8.1 Run backend `py_compile` on all files
|
||||
- [ ] 8.2 Run frontend `npm run typecheck`
|
||||
- [ ] 8.3 Run frontend `npm run build`
|
||||
- [ ] 8.4 Run frontend tests: `npm test`
|
||||
- [ ] 8.5 Verify file size targets met (pages ≤ 150, routers ≤ 400, no `styles.css` monolith)
|
||||
- [ ] 8.6 Verify no 404s or import errors in browser console
|
||||
- [ ] 8.7 Manual smoke test: create project, start terminal, open config profiles
|
||||
- [ ] 8.8 Verify visual regression: colors, spacing, typography unchanged
|
||||
- [ ] 8.9 Verify mobile terminal styles intact
|
||||
- [ ] 8.10 Verify notification dropdown styles intact
|
||||
Reference in New Issue
Block a user