mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: implement E2E testing workflow and enhance XML syntax editor
- Added a new GitHub Actions workflow for end-to-end (E2E) testing, including setup for Node.js, Playwright, and server initialization. - Introduced a setup script for E2E test fixtures to create necessary directories and files. - Integrated CodeMirror for XML syntax editing in the XmlSyntaxEditor component, improving code highlighting and editing experience. - Updated package dependencies in package.json and package-lock.json to include new libraries for XML handling and theming. - Refactored various components for improved readability and consistency, including the sidebar and file browser dialog. - Added tests for spec editor persistence to ensure data integrity across sessions.
This commit is contained in:
91
.github/workflows/e2e-tests.yml
vendored
Normal file
91
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
# Use npm install instead of npm ci to correctly resolve platform-specific
|
||||||
|
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Linux native bindings
|
||||||
|
# Workaround for npm optional dependencies bug (npm/cli#4828)
|
||||||
|
# Explicitly install Linux bindings needed for build tools
|
||||||
|
run: |
|
||||||
|
npm install --no-save --force \
|
||||||
|
@rollup/rollup-linux-x64-gnu@4.53.3 \
|
||||||
|
@tailwindcss/oxide-linux-x64-gnu@4.1.17
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
working-directory: apps/app
|
||||||
|
|
||||||
|
- name: Build server
|
||||||
|
run: npm run build --workspace=apps/server
|
||||||
|
|
||||||
|
- name: Start backend server
|
||||||
|
run: npm run start --workspace=apps/server &
|
||||||
|
env:
|
||||||
|
PORT: 3008
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
|
- name: Wait for backend server
|
||||||
|
run: |
|
||||||
|
echo "Waiting for backend server to be ready..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
|
||||||
|
echo "Backend server is ready!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... ($i/30)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Backend server failed to start!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
# Playwright automatically starts the Next.js frontend via webServer config
|
||||||
|
# (see apps/app/playwright.config.ts) - no need to start it manually
|
||||||
|
run: npm run test --workspace=apps/app
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
NEXT_PUBLIC_SERVER_URL: http://localhost:3008
|
||||||
|
NEXT_PUBLIC_SKIP_SETUP: "true"
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: apps/app/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: apps/app/test-results/
|
||||||
|
retention-days: 7
|
||||||
@@ -27,15 +27,19 @@
|
|||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
|
"pretest": "node scripts/setup-e2e-fixtures.js",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:headed": "playwright test --headed",
|
"test:headed": "playwright test --headed",
|
||||||
"dev:electron:wsl": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron . --no-sandbox --disable-gpu\"",
|
"dev:electron:wsl": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron . --no-sandbox --disable-gpu\"",
|
||||||
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
|
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -46,6 +50,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
|
"@uiw/react-codemirror": "^4.25.4",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
@@ -122,7 +127,9 @@
|
|||||||
{
|
{
|
||||||
"from": "../../.env",
|
"from": "../../.env",
|
||||||
"to": ".env",
|
"to": ".env",
|
||||||
"filter": ["**/*"]
|
"filter": [
|
||||||
|
"**/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default defineConfig({
|
|||||||
url: `http://localhost:${port}`,
|
url: `http://localhost:${port}`,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NEXT_PUBLIC_SKIP_SETUP: "true",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
49
apps/app/scripts/setup-e2e-fixtures.js
Normal file
49
apps/app/scripts/setup-e2e-fixtures.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup script for E2E test fixtures
|
||||||
|
* Creates the necessary test fixture directories and files before running Playwright tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Resolve workspace root (apps/app/scripts -> workspace root)
|
||||||
|
const WORKSPACE_ROOT = path.resolve(__dirname, "../../..");
|
||||||
|
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
|
||||||
|
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");
|
||||||
|
|
||||||
|
const SPEC_CONTENT = `<app_spec>
|
||||||
|
<name>Test Project A</name>
|
||||||
|
<description>A test fixture project for Playwright testing</description>
|
||||||
|
<tech_stack>
|
||||||
|
<item>TypeScript</item>
|
||||||
|
<item>React</item>
|
||||||
|
</tech_stack>
|
||||||
|
</app_spec>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function setupFixtures() {
|
||||||
|
console.log("Setting up E2E test fixtures...");
|
||||||
|
console.log(`Workspace root: ${WORKSPACE_ROOT}`);
|
||||||
|
console.log(`Fixture path: ${FIXTURE_PATH}`);
|
||||||
|
|
||||||
|
// Create fixture directory
|
||||||
|
const specDir = path.dirname(SPEC_FILE_PATH);
|
||||||
|
if (!fs.existsSync(specDir)) {
|
||||||
|
fs.mkdirSync(specDir, { recursive: true });
|
||||||
|
console.log(`Created directory: ${specDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create app_spec.txt
|
||||||
|
fs.writeFileSync(SPEC_FILE_PATH, SPEC_CONTENT);
|
||||||
|
console.log(`Created fixture file: ${SPEC_FILE_PATH}`);
|
||||||
|
|
||||||
|
console.log("E2E test fixtures setup complete!");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFixtures();
|
||||||
@@ -222,12 +222,6 @@ function HomeContent() {
|
|||||||
return (
|
return (
|
||||||
<main className="h-screen overflow-hidden" data-testid="app-container">
|
<main className="h-screen overflow-hidden" data-testid="app-container">
|
||||||
<SetupView />
|
<SetupView />
|
||||||
{/* Environment indicator */}
|
|
||||||
{isMounted && !isElectron() && (
|
|
||||||
<div className="fixed bottom-4 right-4 px-3 py-1.5 bg-blue-500/10 text-blue-500 text-xs rounded-full border border-blue-500/20 pointer-events-none">
|
|
||||||
Web Mode
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -242,13 +236,6 @@ function HomeContent() {
|
|||||||
{renderView()}
|
{renderView()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Environment indicator - only show after mount to prevent hydration issues */}
|
|
||||||
{isMounted && !isElectron() && (
|
|
||||||
<div className="fixed bottom-4 right-4 px-3 py-1.5 bg-blue-500/10 text-blue-500 text-xs rounded-full border border-blue-500/20 pointer-events-none">
|
|
||||||
Web Mode
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive } from "lucide-react";
|
import {
|
||||||
|
FolderOpen,
|
||||||
|
Folder,
|
||||||
|
ChevronRight,
|
||||||
|
Home,
|
||||||
|
ArrowLeft,
|
||||||
|
HardDrive,
|
||||||
|
CornerDownLeft,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,6 +19,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
interface DirectoryEntry {
|
interface DirectoryEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -39,14 +48,16 @@ export function FileBrowserDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
title = "Select Project Directory",
|
title = "Select Project Directory",
|
||||||
description = "Navigate to your project folder",
|
description = "Navigate to your project folder or paste a path directly",
|
||||||
}: FileBrowserDialogProps) {
|
}: FileBrowserDialogProps) {
|
||||||
const [currentPath, setCurrentPath] = useState<string>("");
|
const [currentPath, setCurrentPath] = useState<string>("");
|
||||||
|
const [pathInput, setPathInput] = useState<string>("");
|
||||||
const [parentPath, setParentPath] = useState<string | null>(null);
|
const [parentPath, setParentPath] = useState<string | null>(null);
|
||||||
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
||||||
const [drives, setDrives] = useState<string[]>([]);
|
const [drives, setDrives] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const pathInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const browseDirectory = async (dirPath?: string) => {
|
const browseDirectory = async (dirPath?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -54,7 +65,8 @@ export function FileBrowserDialog({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get server URL from environment or default
|
// Get server URL from environment or default
|
||||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
const serverUrl =
|
||||||
|
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
||||||
|
|
||||||
const response = await fetch(`${serverUrl}/api/fs/browse`, {
|
const response = await fetch(`${serverUrl}/api/fs/browse`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -66,6 +78,7 @@ export function FileBrowserDialog({
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setCurrentPath(result.currentPath);
|
setCurrentPath(result.currentPath);
|
||||||
|
setPathInput(result.currentPath);
|
||||||
setParentPath(result.parentPath);
|
setParentPath(result.parentPath);
|
||||||
setDirectories(result.directories);
|
setDirectories(result.directories);
|
||||||
setDrives(result.drives || []);
|
setDrives(result.drives || []);
|
||||||
@@ -73,7 +86,9 @@ export function FileBrowserDialog({
|
|||||||
setError(result.error || "Failed to browse directory");
|
setError(result.error || "Failed to browse directory");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to load directories");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load directories"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -104,6 +119,20 @@ export function FileBrowserDialog({
|
|||||||
browseDirectory(drivePath);
|
browseDirectory(drivePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGoToPath = () => {
|
||||||
|
const trimmedPath = pathInput.trim();
|
||||||
|
if (trimmedPath) {
|
||||||
|
browseDirectory(trimmedPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePathInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleGoToPath();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
if (currentPath) {
|
if (currentPath) {
|
||||||
onSelect(currentPath);
|
onSelect(currentPath);
|
||||||
@@ -125,6 +154,31 @@ export function FileBrowserDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 min-h-[400px] flex-1 overflow-hidden py-2">
|
<div className="flex flex-col gap-3 min-h-[400px] flex-1 overflow-hidden py-2">
|
||||||
|
{/* Direct path input */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
ref={pathInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder="Paste or type a full path (e.g., /home/user/projects/myapp)"
|
||||||
|
value={pathInput}
|
||||||
|
onChange={(e) => setPathInput(e.target.value)}
|
||||||
|
onKeyDown={handlePathInputKeyDown}
|
||||||
|
className="flex-1 font-mono text-sm"
|
||||||
|
data-testid="path-input"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGoToPath}
|
||||||
|
disabled={loading || !pathInput.trim()}
|
||||||
|
data-testid="go-to-path-button"
|
||||||
|
>
|
||||||
|
<CornerDownLeft className="w-4 h-4 mr-1" />
|
||||||
|
Go
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Drives selector (Windows only) */}
|
{/* Drives selector (Windows only) */}
|
||||||
{drives.length > 0 && (
|
{drives.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
||||||
@@ -135,7 +189,9 @@ export function FileBrowserDialog({
|
|||||||
{drives.map((drive) => (
|
{drives.map((drive) => (
|
||||||
<Button
|
<Button
|
||||||
key={drive}
|
key={drive}
|
||||||
variant={currentPath.startsWith(drive) ? "default" : "outline"}
|
variant={
|
||||||
|
currentPath.startsWith(drive) ? "default" : "outline"
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleSelectDrive(drive)}
|
onClick={() => handleSelectDrive(drive)}
|
||||||
className="h-7 px-3 text-xs"
|
className="h-7 px-3 text-xs"
|
||||||
@@ -178,7 +234,9 @@ export function FileBrowserDialog({
|
|||||||
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
|
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
<div className="flex items-center justify-center h-full p-8">
|
||||||
<div className="text-sm text-muted-foreground">Loading directories...</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Loading directories...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -190,7 +248,9 @@ export function FileBrowserDialog({
|
|||||||
|
|
||||||
{!loading && !error && directories.length === 0 && (
|
{!loading && !error && directories.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
<div className="flex items-center justify-center h-full p-8">
|
||||||
<div className="text-sm text-muted-foreground">No subdirectories found</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No subdirectories found
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -212,7 +272,8 @@ export function FileBrowserDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Click on a folder to navigate. Select the current folder or navigate to a subfolder.
|
Paste a full path above, or click on folders to navigate. Press
|
||||||
|
Enter or click Go to jump to a path.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,16 @@ export function Sidebar() {
|
|||||||
moveProjectToTrash,
|
moveProjectToTrash,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
|
// Environment variable flags for hiding sidebar items
|
||||||
|
// Note: Next.js requires static access to process.env variables (no dynamic keys)
|
||||||
|
const hideTerminal = process.env.NEXT_PUBLIC_HIDE_TERMINAL === "true";
|
||||||
|
const hideWiki = process.env.NEXT_PUBLIC_HIDE_WIKI === "true";
|
||||||
|
const hideRunningAgents =
|
||||||
|
process.env.NEXT_PUBLIC_HIDE_RUNNING_AGENTS === "true";
|
||||||
|
const hideContext = process.env.NEXT_PUBLIC_HIDE_CONTEXT === "true";
|
||||||
|
const hideSpecEditor = process.env.NEXT_PUBLIC_HIDE_SPEC_EDITOR === "true";
|
||||||
|
const hideAiProfiles = process.env.NEXT_PUBLIC_HIDE_AI_PROFILES === "true";
|
||||||
|
|
||||||
// Get customizable keyboard shortcuts
|
// Get customizable keyboard shortcuts
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
|
|
||||||
@@ -590,54 +600,75 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [emptyTrash, trashedProjects.length]);
|
}, [emptyTrash, trashedProjects.length]);
|
||||||
|
|
||||||
const navSections: NavSection[] = [
|
const navSections: NavSection[] = useMemo(() => {
|
||||||
{
|
const allToolsItems: NavItem[] = [
|
||||||
label: "Project",
|
{
|
||||||
items: [
|
id: "spec",
|
||||||
{
|
label: "Spec Editor",
|
||||||
id: "board",
|
icon: FileText,
|
||||||
label: "Kanban Board",
|
shortcut: shortcuts.spec,
|
||||||
icon: LayoutGrid,
|
},
|
||||||
shortcut: shortcuts.board,
|
{
|
||||||
},
|
id: "context",
|
||||||
{
|
label: "Context",
|
||||||
id: "agent",
|
icon: BookOpen,
|
||||||
label: "Agent Runner",
|
shortcut: shortcuts.context,
|
||||||
icon: Bot,
|
},
|
||||||
shortcut: shortcuts.agent,
|
{
|
||||||
},
|
id: "profiles",
|
||||||
],
|
label: "AI Profiles",
|
||||||
},
|
icon: UserCircle,
|
||||||
{
|
shortcut: shortcuts.profiles,
|
||||||
label: "Tools",
|
},
|
||||||
items: [
|
{
|
||||||
{
|
id: "terminal",
|
||||||
id: "spec",
|
label: "Terminal",
|
||||||
label: "Spec Editor",
|
icon: Terminal,
|
||||||
icon: FileText,
|
shortcut: shortcuts.terminal,
|
||||||
shortcut: shortcuts.spec,
|
},
|
||||||
},
|
];
|
||||||
{
|
|
||||||
id: "context",
|
// Filter out hidden items
|
||||||
label: "Context",
|
const visibleToolsItems = allToolsItems.filter((item) => {
|
||||||
icon: BookOpen,
|
if (item.id === "spec" && hideSpecEditor) {
|
||||||
shortcut: shortcuts.context,
|
return false;
|
||||||
},
|
}
|
||||||
{
|
if (item.id === "context" && hideContext) {
|
||||||
id: "profiles",
|
return false;
|
||||||
label: "AI Profiles",
|
}
|
||||||
icon: UserCircle,
|
if (item.id === "profiles" && hideAiProfiles) {
|
||||||
shortcut: shortcuts.profiles,
|
return false;
|
||||||
},
|
}
|
||||||
{
|
if (item.id === "terminal" && hideTerminal) {
|
||||||
id: "terminal",
|
return false;
|
||||||
label: "Terminal",
|
}
|
||||||
icon: Terminal,
|
return true;
|
||||||
shortcut: shortcuts.terminal,
|
});
|
||||||
},
|
|
||||||
],
|
return [
|
||||||
},
|
{
|
||||||
];
|
label: "Project",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "board",
|
||||||
|
label: "Kanban Board",
|
||||||
|
icon: LayoutGrid,
|
||||||
|
shortcut: shortcuts.board,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agent",
|
||||||
|
label: "Agent Runner",
|
||||||
|
icon: Bot,
|
||||||
|
shortcut: shortcuts.agent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tools",
|
||||||
|
items: visibleToolsItems,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles]);
|
||||||
|
|
||||||
// Handle selecting the currently highlighted project
|
// Handle selecting the currently highlighted project
|
||||||
const selectHighlightedProject = useCallback(() => {
|
const selectHighlightedProject = useCallback(() => {
|
||||||
@@ -1268,108 +1299,112 @@ export function Sidebar() {
|
|||||||
{/* Course Promo Badge */}
|
{/* Course Promo Badge */}
|
||||||
<CoursePromoBadge sidebarOpen={sidebarOpen} />
|
<CoursePromoBadge sidebarOpen={sidebarOpen} />
|
||||||
{/* Wiki Link */}
|
{/* Wiki Link */}
|
||||||
<div className="p-2 pb-0">
|
{!hideWiki && (
|
||||||
<button
|
<div className="p-2 pb-0">
|
||||||
onClick={() => setCurrentView("wiki")}
|
<button
|
||||||
className={cn(
|
onClick={() => setCurrentView("wiki")}
|
||||||
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
|
||||||
isActiveRoute("wiki")
|
|
||||||
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
|
||||||
sidebarOpen ? "justify-start" : "justify-center"
|
|
||||||
)}
|
|
||||||
title={!sidebarOpen ? "Wiki" : undefined}
|
|
||||||
data-testid="wiki-link"
|
|
||||||
>
|
|
||||||
{isActiveRoute("wiki") && (
|
|
||||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
|
||||||
)}
|
|
||||||
<BookOpen
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-4 h-4 shrink-0 transition-colors",
|
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
||||||
isActiveRoute("wiki")
|
isActiveRoute("wiki")
|
||||||
? "text-brand-500"
|
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
||||||
: "group-hover:text-brand-400"
|
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
||||||
)}
|
sidebarOpen ? "justify-start" : "justify-center"
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-2.5 font-medium text-sm flex-1 text-left",
|
|
||||||
sidebarOpen ? "hidden lg:block" : "hidden"
|
|
||||||
)}
|
)}
|
||||||
|
title={!sidebarOpen ? "Wiki" : undefined}
|
||||||
|
data-testid="wiki-link"
|
||||||
>
|
>
|
||||||
Wiki
|
{isActiveRoute("wiki") && (
|
||||||
</span>
|
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
||||||
{!sidebarOpen && (
|
)}
|
||||||
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
|
<BookOpen
|
||||||
Wiki
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Running Agents Link */}
|
|
||||||
<div className="p-2 pb-0">
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentView("running-agents")}
|
|
||||||
className={cn(
|
|
||||||
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
|
||||||
isActiveRoute("running-agents")
|
|
||||||
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
|
||||||
sidebarOpen ? "justify-start" : "justify-center"
|
|
||||||
)}
|
|
||||||
title={!sidebarOpen ? "Running Agents" : undefined}
|
|
||||||
data-testid="running-agents-link"
|
|
||||||
>
|
|
||||||
{isActiveRoute("running-agents") && (
|
|
||||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<Activity
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-4 h-4 shrink-0 transition-colors",
|
"w-4 h-4 shrink-0 transition-colors",
|
||||||
isActiveRoute("running-agents")
|
isActiveRoute("wiki")
|
||||||
? "text-brand-500"
|
? "text-brand-500"
|
||||||
: "group-hover:text-brand-400"
|
: "group-hover:text-brand-400"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Running agents count badge - shown in collapsed state */}
|
<span
|
||||||
{!sidebarOpen && runningAgentsCount > 0 && (
|
className={cn(
|
||||||
|
"ml-2.5 font-medium text-sm flex-1 text-left",
|
||||||
|
sidebarOpen ? "hidden lg:block" : "hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Wiki
|
||||||
|
</span>
|
||||||
|
{!sidebarOpen && (
|
||||||
|
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
|
||||||
|
Wiki
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Running Agents Link */}
|
||||||
|
{!hideRunningAgents && (
|
||||||
|
<div className="p-2 pb-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentView("running-agents")}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
||||||
|
isActiveRoute("running-agents")
|
||||||
|
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
||||||
|
sidebarOpen ? "justify-start" : "justify-center"
|
||||||
|
)}
|
||||||
|
title={!sidebarOpen ? "Running Agents" : undefined}
|
||||||
|
data-testid="running-agents-link"
|
||||||
|
>
|
||||||
|
{isActiveRoute("running-agents") && (
|
||||||
|
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
||||||
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<Activity
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 shrink-0 transition-colors",
|
||||||
|
isActiveRoute("running-agents")
|
||||||
|
? "text-brand-500"
|
||||||
|
: "group-hover:text-brand-400"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Running agents count badge - shown in collapsed state */}
|
||||||
|
{!sidebarOpen && runningAgentsCount > 0 && (
|
||||||
|
<span
|
||||||
|
className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-semibold rounded-full bg-brand-500 text-white"
|
||||||
|
data-testid="running-agents-count-collapsed"
|
||||||
|
>
|
||||||
|
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-2.5 font-medium text-sm flex-1 text-left",
|
||||||
|
sidebarOpen ? "hidden lg:block" : "hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Running Agents
|
||||||
|
</span>
|
||||||
|
{/* Running agents count badge - shown in expanded state */}
|
||||||
|
{sidebarOpen && runningAgentsCount > 0 && (
|
||||||
<span
|
<span
|
||||||
className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-semibold rounded-full bg-brand-500 text-white"
|
className={cn(
|
||||||
data-testid="running-agents-count-collapsed"
|
"hidden lg:flex items-center justify-center min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full bg-brand-500 text-white",
|
||||||
|
isActiveRoute("running-agents") && "bg-brand-600"
|
||||||
|
)}
|
||||||
|
data-testid="running-agents-count"
|
||||||
>
|
>
|
||||||
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
|
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
{!sidebarOpen && (
|
||||||
<span
|
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
|
||||||
className={cn(
|
Running Agents
|
||||||
"ml-2.5 font-medium text-sm flex-1 text-left",
|
</span>
|
||||||
sidebarOpen ? "hidden lg:block" : "hidden"
|
|
||||||
)}
|
)}
|
||||||
>
|
</button>
|
||||||
Running Agents
|
</div>
|
||||||
</span>
|
)}
|
||||||
{/* Running agents count badge - shown in expanded state */}
|
|
||||||
{sidebarOpen && runningAgentsCount > 0 && (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"hidden lg:flex items-center justify-center min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full bg-brand-500 text-white",
|
|
||||||
isActiveRoute("running-agents") && "bg-brand-600"
|
|
||||||
)}
|
|
||||||
data-testid="running-agents-count"
|
|
||||||
>
|
|
||||||
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!sidebarOpen && (
|
|
||||||
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
|
|
||||||
Running Agents
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Settings Link */}
|
{/* Settings Link */}
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ interface ValidationErrors {
|
|||||||
interface NewProjectModalProps {
|
interface NewProjectModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onCreateBlankProject: (projectName: string, parentDir: string) => Promise<void>;
|
onCreateBlankProject: (
|
||||||
|
projectName: string,
|
||||||
|
parentDir: string
|
||||||
|
) => Promise<void>;
|
||||||
onCreateFromTemplate: (
|
onCreateFromTemplate: (
|
||||||
template: StarterTemplate,
|
template: StarterTemplate,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
@@ -67,7 +70,8 @@ export function NewProjectModal({
|
|||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
const [workspaceDir, setWorkspaceDir] = useState<string>("");
|
const [workspaceDir, setWorkspaceDir] = useState<string>("");
|
||||||
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
|
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<StarterTemplate | null>(null);
|
const [selectedTemplate, setSelectedTemplate] =
|
||||||
|
useState<StarterTemplate | null>(null);
|
||||||
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
||||||
const [customUrl, setCustomUrl] = useState("");
|
const [customUrl, setCustomUrl] = useState("");
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||||
@@ -78,7 +82,8 @@ export function NewProjectModal({
|
|||||||
if (open) {
|
if (open) {
|
||||||
setIsLoadingWorkspace(true);
|
setIsLoadingWorkspace(true);
|
||||||
const httpClient = getHttpApiClient();
|
const httpClient = getHttpApiClient();
|
||||||
httpClient.workspace.getConfig()
|
httpClient.workspace
|
||||||
|
.getConfig()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.success && result.workspaceDir) {
|
if (result.success && result.workspaceDir) {
|
||||||
setWorkspaceDir(result.workspaceDir);
|
setWorkspaceDir(result.workspaceDir);
|
||||||
@@ -113,7 +118,10 @@ export function NewProjectModal({
|
|||||||
}, [projectName, errors.projectName]);
|
}, [projectName, errors.projectName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) {
|
if (
|
||||||
|
(selectedTemplate || (useCustomUrl && customUrl)) &&
|
||||||
|
errors.templateSelection
|
||||||
|
) {
|
||||||
setErrors((prev) => ({ ...prev, templateSelection: false }));
|
setErrors((prev) => ({ ...prev, templateSelection: false }));
|
||||||
}
|
}
|
||||||
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
|
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
|
||||||
@@ -187,7 +195,8 @@ export function NewProjectModal({
|
|||||||
const handleBrowseDirectory = async () => {
|
const handleBrowseDirectory = async () => {
|
||||||
const selectedPath = await openFileBrowser({
|
const selectedPath = await openFileBrowser({
|
||||||
title: "Select Base Project Directory",
|
title: "Select Base Project Directory",
|
||||||
description: "Choose the parent directory where your project will be created",
|
description:
|
||||||
|
"Choose the parent directory where your project will be created",
|
||||||
});
|
});
|
||||||
if (selectedPath) {
|
if (selectedPath) {
|
||||||
setWorkspaceDir(selectedPath);
|
setWorkspaceDir(selectedPath);
|
||||||
@@ -199,9 +208,16 @@ export function NewProjectModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Use platform-specific path separator
|
// Use platform-specific path separator
|
||||||
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
const pathSep =
|
||||||
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
typeof window !== "undefined" && (window as any).electronAPI
|
||||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : "";
|
? navigator.platform.indexOf("Win") !== -1
|
||||||
|
? "\\"
|
||||||
|
: "/"
|
||||||
|
: "/";
|
||||||
|
const projectPath =
|
||||||
|
workspaceDir && projectName
|
||||||
|
? `${workspaceDir}${pathSep}${projectName}`
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -210,7 +226,9 @@ export function NewProjectModal({
|
|||||||
data-testid="new-project-modal"
|
data-testid="new-project-modal"
|
||||||
>
|
>
|
||||||
<DialogHeader className="pb-2">
|
<DialogHeader className="pb-2">
|
||||||
<DialogTitle className="text-foreground">Create New Project</DialogTitle>
|
<DialogTitle className="text-foreground">
|
||||||
|
Create New Project
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Start with a blank project or choose from a starter template.
|
Start with a blank project or choose from a starter template.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -219,8 +237,15 @@ export function NewProjectModal({
|
|||||||
{/* Project Name Input - Always visible at top */}
|
{/* Project Name Input - Always visible at top */}
|
||||||
<div className="space-y-3 pb-4 border-b border-border">
|
<div className="space-y-3 pb-4 border-b border-border">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="project-name" className={cn("text-foreground", errors.projectName && "text-red-500")}>
|
<Label
|
||||||
Project Name {errors.projectName && <span className="text-red-500">*</span>}
|
htmlFor="project-name"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground",
|
||||||
|
errors.projectName && "text-red-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Project Name{" "}
|
||||||
|
{errors.projectName && <span className="text-red-500">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="project-name"
|
id="project-name"
|
||||||
@@ -242,16 +267,23 @@ export function NewProjectModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workspace Directory Display */}
|
{/* Workspace Directory Display */}
|
||||||
<div className={cn(
|
<div
|
||||||
"flex items-center gap-2 text-sm",
|
className={cn(
|
||||||
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
"flex items-center gap-2 text-sm",
|
||||||
)}>
|
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Folder className="w-4 h-4 shrink-0" />
|
<Folder className="w-4 h-4 shrink-0" />
|
||||||
<span className="flex-1 min-w-0">
|
<span className="flex-1 min-w-0">
|
||||||
{isLoadingWorkspace ? (
|
{isLoadingWorkspace ? (
|
||||||
"Loading workspace..."
|
"Loading workspace..."
|
||||||
) : workspaceDir ? (
|
) : workspaceDir ? (
|
||||||
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">{projectPath || "..."}</code></>
|
<>
|
||||||
|
Will be created at:{" "}
|
||||||
|
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
|
||||||
|
{projectPath || "..."}
|
||||||
|
</code>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-red-500">No workspace configured</span>
|
<span className="text-red-500">No workspace configured</span>
|
||||||
)}
|
)}
|
||||||
@@ -302,14 +334,18 @@ export function NewProjectModal({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Error message for template selection */}
|
{/* Error message for template selection */}
|
||||||
{errors.templateSelection && (
|
{errors.templateSelection && (
|
||||||
<p className="text-sm text-red-500">Please select a template or enter a custom GitHub URL</p>
|
<p className="text-sm text-red-500">
|
||||||
|
Please select a template or enter a custom GitHub URL
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preset Templates */}
|
{/* Preset Templates */}
|
||||||
<div className={cn(
|
<div
|
||||||
"space-y-3 rounded-lg p-1 -m-1",
|
className={cn(
|
||||||
errors.templateSelection && "ring-2 ring-red-500/50"
|
"space-y-3 rounded-lg p-1 -m-1",
|
||||||
)}>
|
errors.templateSelection && "ring-2 ring-red-500/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{starterTemplates.map((template) => (
|
{starterTemplates.map((template) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
@@ -328,9 +364,10 @@ export function NewProjectModal({
|
|||||||
<h4 className="font-medium text-foreground">
|
<h4 className="font-medium text-foreground">
|
||||||
{template.name}
|
{template.name}
|
||||||
</h4>
|
</h4>
|
||||||
{selectedTemplate?.id === template.id && !useCustomUrl && (
|
{selectedTemplate?.id === template.id &&
|
||||||
<Check className="w-4 h-4 text-brand-500" />
|
!useCustomUrl && (
|
||||||
)}
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
{template.description}
|
{template.description}
|
||||||
@@ -391,15 +428,22 @@ export function NewProjectModal({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Link className="w-4 h-4 text-muted-foreground" />
|
<Link className="w-4 h-4 text-muted-foreground" />
|
||||||
<h4 className="font-medium text-foreground">Custom GitHub URL</h4>
|
<h4 className="font-medium text-foreground">
|
||||||
{useCustomUrl && <Check className="w-4 h-4 text-brand-500" />}
|
Custom GitHub URL
|
||||||
|
</h4>
|
||||||
|
{useCustomUrl && (
|
||||||
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
Clone any public GitHub repository as a starting point.
|
Clone any public GitHub repository as a starting point.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{useCustomUrl && (
|
{useCustomUrl && (
|
||||||
<div onClick={(e) => e.stopPropagation()} className="space-y-1">
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="space-y-1"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://github.com/username/repository"
|
placeholder="https://github.com/username/repository"
|
||||||
value={customUrl}
|
value={customUrl}
|
||||||
@@ -413,7 +457,9 @@ export function NewProjectModal({
|
|||||||
data-testid="custom-url-input"
|
data-testid="custom-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.customUrl && (
|
{errors.customUrl && (
|
||||||
<p className="text-xs text-red-500">GitHub URL is required</p>
|
<p className="text-xs text-red-500">
|
||||||
|
GitHub URL is required
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useCallback, useMemo } from "react";
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
|
import { xml } from "@codemirror/lang-xml";
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
|
import { Extension } from "@codemirror/state";
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||||
|
import { tags as t } from "@lezer/highlight";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface XmlSyntaxEditorProps {
|
interface XmlSyntaxEditorProps {
|
||||||
@@ -11,199 +16,78 @@ interface XmlSyntaxEditorProps {
|
|||||||
"data-testid"?: string;
|
"data-testid"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenize XML content into parts for highlighting
|
// Syntax highlighting that uses CSS variables from the app's theme system
|
||||||
interface Token {
|
// This automatically adapts to any theme (dark, light, dracula, nord, etc.)
|
||||||
type:
|
const syntaxColors = HighlightStyle.define([
|
||||||
| "tag-bracket"
|
// XML tags - use primary color
|
||||||
| "tag-name"
|
{ tag: t.tagName, color: "var(--primary)" },
|
||||||
| "attribute-name"
|
{ tag: t.angleBracket, color: "var(--muted-foreground)" },
|
||||||
| "attribute-equals"
|
|
||||||
| "attribute-value"
|
|
||||||
| "text"
|
|
||||||
| "comment"
|
|
||||||
| "cdata"
|
|
||||||
| "doctype";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeXml(text: string): Token[] {
|
// Attributes
|
||||||
const tokens: Token[] = [];
|
{ tag: t.attributeName, color: "var(--chart-2, oklch(0.6 0.118 184.704))" },
|
||||||
let i = 0;
|
{ tag: t.attributeValue, color: "var(--chart-1, oklch(0.646 0.222 41.116))" },
|
||||||
|
|
||||||
while (i < text.length) {
|
// Strings and content
|
||||||
// Comment: <!-- ... -->
|
{ tag: t.string, color: "var(--chart-1, oklch(0.646 0.222 41.116))" },
|
||||||
if (text.slice(i, i + 4) === "<!--") {
|
{ tag: t.content, color: "var(--foreground)" },
|
||||||
const end = text.indexOf("-->", i + 4);
|
|
||||||
if (end !== -1) {
|
|
||||||
tokens.push({ type: "comment", value: text.slice(i, end + 3) });
|
|
||||||
i = end + 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CDATA: <![CDATA[ ... ]]>
|
// Comments
|
||||||
if (text.slice(i, i + 9) === "<![CDATA[") {
|
{ tag: t.comment, color: "var(--muted-foreground)", fontStyle: "italic" },
|
||||||
const end = text.indexOf("]]>", i + 9);
|
|
||||||
if (end !== -1) {
|
|
||||||
tokens.push({ type: "cdata", value: text.slice(i, end + 3) });
|
|
||||||
i = end + 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOCTYPE: <!DOCTYPE ... >
|
// Special
|
||||||
if (text.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
|
{ tag: t.processingInstruction, color: "var(--muted-foreground)" },
|
||||||
const end = text.indexOf(">", i + 9);
|
{ tag: t.documentMeta, color: "var(--muted-foreground)" },
|
||||||
if (end !== -1) {
|
]);
|
||||||
tokens.push({ type: "doctype", value: text.slice(i, end + 1) });
|
|
||||||
i = end + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag: < ... >
|
// Editor theme using CSS variables
|
||||||
if (text[i] === "<") {
|
const editorTheme = EditorView.theme({
|
||||||
// Find the end of the tag
|
"&": {
|
||||||
let tagEnd = i + 1;
|
height: "100%",
|
||||||
let inString: string | null = null;
|
fontSize: "0.875rem",
|
||||||
|
fontFamily: "ui-monospace, monospace",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
},
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
fontFamily: "ui-monospace, monospace",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
padding: "1rem",
|
||||||
|
minHeight: "100%",
|
||||||
|
caretColor: "var(--primary)",
|
||||||
|
},
|
||||||
|
".cm-cursor, .cm-dropCursor": {
|
||||||
|
borderLeftColor: "var(--primary)",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||||
|
{
|
||||||
|
backgroundColor: "oklch(0.55 0.25 265 / 0.3)",
|
||||||
|
},
|
||||||
|
".cm-activeLine": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
".cm-line": {
|
||||||
|
padding: "0",
|
||||||
|
},
|
||||||
|
"&.cm-focused": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
".cm-placeholder": {
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
while (tagEnd < text.length) {
|
// Combine all extensions
|
||||||
const char = text[tagEnd];
|
const extensions: Extension[] = [
|
||||||
|
xml(),
|
||||||
if (inString) {
|
syntaxHighlighting(syntaxColors),
|
||||||
if (char === inString && text[tagEnd - 1] !== "\\") {
|
editorTheme,
|
||||||
inString = null;
|
];
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (char === '"' || char === "'") {
|
|
||||||
inString = char;
|
|
||||||
} else if (char === ">") {
|
|
||||||
tagEnd++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tagEnd++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagContent = text.slice(i, tagEnd);
|
|
||||||
const tagTokens = tokenizeTag(tagContent);
|
|
||||||
tokens.push(...tagTokens);
|
|
||||||
i = tagEnd;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text content between tags
|
|
||||||
const nextTag = text.indexOf("<", i);
|
|
||||||
if (nextTag === -1) {
|
|
||||||
tokens.push({ type: "text", value: text.slice(i) });
|
|
||||||
break;
|
|
||||||
} else if (nextTag > i) {
|
|
||||||
tokens.push({ type: "text", value: text.slice(i, nextTag) });
|
|
||||||
i = nextTag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeTag(tag: string): Token[] {
|
|
||||||
const tokens: Token[] = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
// Opening bracket (< or </ or <?)
|
|
||||||
if (tag.startsWith("</")) {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "</" });
|
|
||||||
i = 2;
|
|
||||||
} else if (tag.startsWith("<?")) {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "<?" });
|
|
||||||
i = 2;
|
|
||||||
} else {
|
|
||||||
tokens.push({ type: "tag-bracket", value: "<" });
|
|
||||||
i = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag name
|
|
||||||
let tagName = "";
|
|
||||||
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
|
|
||||||
tagName += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (tagName) {
|
|
||||||
tokens.push({ type: "tag-name", value: tagName });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes and closing
|
|
||||||
while (i < tag.length) {
|
|
||||||
// Skip whitespace
|
|
||||||
if (/\s/.test(tag[i])) {
|
|
||||||
let ws = "";
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
ws += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ type: "text", value: ws });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing bracket
|
|
||||||
if (tag[i] === ">" || tag.slice(i, i + 2) === "/>" || tag.slice(i, i + 2) === "?>") {
|
|
||||||
tokens.push({ type: "tag-bracket", value: tag.slice(i) });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute name
|
|
||||||
let attrName = "";
|
|
||||||
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
|
|
||||||
attrName += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (attrName) {
|
|
||||||
tokens.push({ type: "attribute-name", value: attrName });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace around =
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals sign
|
|
||||||
if (tag[i] === "=") {
|
|
||||||
tokens.push({ type: "attribute-equals", value: "=" });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip whitespace after =
|
|
||||||
while (i < tag.length && /\s/.test(tag[i])) {
|
|
||||||
tokens.push({ type: "text", value: tag[i] });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute value
|
|
||||||
if (tag[i] === '"' || tag[i] === "'") {
|
|
||||||
const quote = tag[i];
|
|
||||||
let value = quote;
|
|
||||||
i++;
|
|
||||||
while (i < tag.length && tag[i] !== quote) {
|
|
||||||
value += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i < tag.length) {
|
|
||||||
value += tag[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ type: "attribute-value", value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function XmlSyntaxEditor({
|
export function XmlSyntaxEditor({
|
||||||
value,
|
value,
|
||||||
@@ -212,78 +96,24 @@ export function XmlSyntaxEditor({
|
|||||||
className,
|
className,
|
||||||
"data-testid": testId,
|
"data-testid": testId,
|
||||||
}: XmlSyntaxEditorProps) {
|
}: XmlSyntaxEditorProps) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const highlightRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Sync scroll between textarea and highlight layer
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (textareaRef.current && highlightRef.current) {
|
|
||||||
highlightRef.current.scrollTop = textareaRef.current.scrollTop;
|
|
||||||
highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle tab key for indentation
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
e.preventDefault();
|
|
||||||
const textarea = e.currentTarget;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const newValue =
|
|
||||||
value.substring(0, start) + " " + value.substring(end);
|
|
||||||
onChange(newValue);
|
|
||||||
// Reset cursor position after state update
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize the highlighted content
|
|
||||||
const highlightedContent = useMemo(() => {
|
|
||||||
const tokens = tokenizeXml(value);
|
|
||||||
|
|
||||||
return tokens.map((token, index) => {
|
|
||||||
const className = `xml-${token.type}`;
|
|
||||||
// React handles escaping automatically, just render the raw value
|
|
||||||
return (
|
|
||||||
<span key={index} className={className}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative w-full h-full xml-editor", className)}>
|
<div className={cn("w-full h-full", className)} data-testid={testId}>
|
||||||
{/* Syntax highlighted layer (read-only, behind textarea) */}
|
<CodeMirror
|
||||||
<div
|
|
||||||
ref={highlightRef}
|
|
||||||
className="absolute inset-0 overflow-auto pointer-events-none font-mono text-sm p-4 whitespace-pre-wrap break-words"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{value ? (
|
|
||||||
<code className="xml-highlight">{highlightedContent}</code>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground opacity-50">{placeholder}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actual textarea (transparent text, handles input) */}
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={onChange}
|
||||||
onScroll={handleScroll}
|
extensions={extensions}
|
||||||
onKeyDown={handleKeyDown}
|
theme="none"
|
||||||
placeholder=""
|
placeholder={placeholder}
|
||||||
spellCheck={false}
|
className="h-full [&_.cm-editor]:h-full"
|
||||||
className="absolute inset-0 w-full h-full font-mono text-sm p-4 bg-transparent resize-none focus:outline-none text-transparent caret-foreground selection:bg-primary/30"
|
basicSetup={{
|
||||||
data-testid={testId}
|
lineNumbers: false,
|
||||||
|
foldGutter: false,
|
||||||
|
highlightActiveLine: false,
|
||||||
|
highlightSelectionMatches: true,
|
||||||
|
autocompletion: true,
|
||||||
|
bracketMatching: true,
|
||||||
|
indentOnInput: true,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus, CheckCircle2 } from "lucide-react";
|
import {
|
||||||
|
Save,
|
||||||
|
RefreshCw,
|
||||||
|
FileText,
|
||||||
|
Sparkles,
|
||||||
|
Loader2,
|
||||||
|
FilePlus2,
|
||||||
|
AlertCircle,
|
||||||
|
ListPlus,
|
||||||
|
CheckCircle2,
|
||||||
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
||||||
@@ -43,14 +53,14 @@ export function SpecView() {
|
|||||||
const [projectOverview, setProjectOverview] = useState("");
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
|
||||||
// Generate features only state
|
// Generate features only state
|
||||||
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
||||||
|
|
||||||
// Logs state (kept for internal tracking, but UI removed)
|
// Logs state (kept for internal tracking, but UI removed)
|
||||||
const [logs, setLogs] = useState<string>("");
|
const [logs, setLogs] = useState<string>("");
|
||||||
const logsRef = useRef<string>("");
|
const logsRef = useRef<string>("");
|
||||||
|
|
||||||
// Phase tracking and status
|
// Phase tracking and status
|
||||||
const [currentPhase, setCurrentPhase] = useState<string>("");
|
const [currentPhase, setCurrentPhase] = useState<string>("");
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||||
@@ -107,28 +117,33 @@ export function SpecView() {
|
|||||||
|
|
||||||
if (status.success && status.isRunning) {
|
if (status.success && status.isRunning) {
|
||||||
// Something is running - restore state using backend's authoritative phase
|
// Something is running - restore state using backend's authoritative phase
|
||||||
console.log("[SpecView] Spec generation is running - restoring state", { phase: status.currentPhase });
|
console.log(
|
||||||
|
"[SpecView] Spec generation is running - restoring state",
|
||||||
|
{ phase: status.currentPhase }
|
||||||
|
);
|
||||||
|
|
||||||
if (!stateRestoredRef.current) {
|
if (!stateRestoredRef.current) {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
stateRestoredRef.current = true;
|
stateRestoredRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the backend's currentPhase directly - single source of truth
|
// Use the backend's currentPhase directly - single source of truth
|
||||||
if (status.currentPhase) {
|
if (status.currentPhase) {
|
||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
} else {
|
} else {
|
||||||
setCurrentPhase("in progress");
|
setCurrentPhase("in progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add resume message to logs if needed
|
// Add resume message to logs if needed
|
||||||
if (!logsRef.current) {
|
if (!logsRef.current) {
|
||||||
const resumeMessage = "[Status] Resumed monitoring existing spec generation process...\n";
|
const resumeMessage =
|
||||||
|
"[Status] Resumed monitoring existing spec generation process...\n";
|
||||||
logsRef.current = resumeMessage;
|
logsRef.current = resumeMessage;
|
||||||
setLogs(resumeMessage);
|
setLogs(resumeMessage);
|
||||||
} else if (!logsRef.current.includes("Resumed monitoring")) {
|
} else if (!logsRef.current.includes("Resumed monitoring")) {
|
||||||
const resumeMessage = "\n[Status] Resumed monitoring existing spec generation process...\n";
|
const resumeMessage =
|
||||||
|
"\n[Status] Resumed monitoring existing spec generation process...\n";
|
||||||
logsRef.current = logsRef.current + resumeMessage;
|
logsRef.current = logsRef.current + resumeMessage;
|
||||||
setLogs(logsRef.current);
|
setLogs(logsRef.current);
|
||||||
}
|
}
|
||||||
@@ -154,7 +169,11 @@ export function SpecView() {
|
|||||||
// Sync state when tab becomes visible (user returns to spec editor)
|
// Sync state when tab becomes visible (user returns to spec editor)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = async () => {
|
const handleVisibilityChange = async () => {
|
||||||
if (!document.hidden && currentProject && (isCreating || isRegenerating || isGeneratingFeatures)) {
|
if (
|
||||||
|
!document.hidden &&
|
||||||
|
currentProject &&
|
||||||
|
(isCreating || isRegenerating || isGeneratingFeatures)
|
||||||
|
) {
|
||||||
// Tab became visible and we think we're still generating - verify status from backend
|
// Tab became visible and we think we're still generating - verify status from backend
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -162,10 +181,12 @@ export function SpecView() {
|
|||||||
|
|
||||||
const status = await api.specRegeneration.status();
|
const status = await api.specRegeneration.status();
|
||||||
console.log("[SpecView] Visibility change - status check:", status);
|
console.log("[SpecView] Visibility change - status check:", status);
|
||||||
|
|
||||||
if (!status.isRunning) {
|
if (!status.isRunning) {
|
||||||
// Backend says not running - clear state
|
// Backend says not running - clear state
|
||||||
console.log("[SpecView] Visibility change: Backend indicates generation complete - clearing state");
|
console.log(
|
||||||
|
"[SpecView] Visibility change: Backend indicates generation complete - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
@@ -177,7 +198,10 @@ export function SpecView() {
|
|||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecView] Failed to check status on visibility change:", error);
|
console.error(
|
||||||
|
"[SpecView] Failed to check status on visibility change:",
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -186,11 +210,21 @@ export function SpecView() {
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
|
}, [
|
||||||
|
currentProject,
|
||||||
|
isCreating,
|
||||||
|
isRegenerating,
|
||||||
|
isGeneratingFeatures,
|
||||||
|
loadSpec,
|
||||||
|
]);
|
||||||
|
|
||||||
// Periodic status check to ensure state stays in sync (only when we think we're running)
|
// Periodic status check to ensure state stays in sync (only when we think we're running)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
|
if (
|
||||||
|
!currentProject ||
|
||||||
|
(!isCreating && !isRegenerating && !isGeneratingFeatures)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -198,21 +232,26 @@ export function SpecView() {
|
|||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
const status = await api.specRegeneration.status();
|
const status = await api.specRegeneration.status();
|
||||||
|
|
||||||
if (!status.isRunning) {
|
if (!status.isRunning) {
|
||||||
// Backend says not running - clear state
|
// Backend says not running - clear state
|
||||||
console.log("[SpecView] Periodic check: Backend indicates generation complete - clearing state");
|
console.log(
|
||||||
|
"[SpecView] Periodic check: Backend indicates generation complete - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("");
|
setCurrentPhase("");
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
loadSpec();
|
loadSpec();
|
||||||
} else if (status.currentPhase && status.currentPhase !== currentPhase) {
|
} else if (
|
||||||
|
status.currentPhase &&
|
||||||
|
status.currentPhase !== currentPhase
|
||||||
|
) {
|
||||||
// Still running but phase changed - update from backend
|
// Still running but phase changed - update from backend
|
||||||
console.log("[SpecView] Periodic check: Phase updated from backend", {
|
console.log("[SpecView] Periodic check: Phase updated from backend", {
|
||||||
old: currentPhase,
|
old: currentPhase,
|
||||||
new: status.currentPhase
|
new: status.currentPhase,
|
||||||
});
|
});
|
||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
}
|
}
|
||||||
@@ -224,173 +263,203 @@ export function SpecView() {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
|
}, [
|
||||||
|
currentProject,
|
||||||
|
isCreating,
|
||||||
|
isRegenerating,
|
||||||
|
isGeneratingFeatures,
|
||||||
|
currentPhase,
|
||||||
|
loadSpec,
|
||||||
|
]);
|
||||||
|
|
||||||
// Subscribe to spec regeneration events
|
// Subscribe to spec regeneration events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
|
const unsubscribe = api.specRegeneration.onEvent(
|
||||||
console.log("[SpecView] Regeneration event:", event.type);
|
(event: SpecRegenerationEvent) => {
|
||||||
|
console.log("[SpecView] Regeneration event:", event.type);
|
||||||
|
|
||||||
if (event.type === "spec_regeneration_progress") {
|
if (event.type === "spec_regeneration_progress") {
|
||||||
// Extract phase from content if present
|
// Extract phase from content if present
|
||||||
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
|
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
|
||||||
if (phaseMatch) {
|
if (phaseMatch) {
|
||||||
const phase = phaseMatch[1];
|
const phase = phaseMatch[1];
|
||||||
setCurrentPhase(phase);
|
setCurrentPhase(phase);
|
||||||
console.log(`[SpecView] Phase updated: ${phase}`);
|
console.log(`[SpecView] Phase updated: ${phase}`);
|
||||||
|
|
||||||
// If phase is "complete", clear running state immediately
|
// If phase is "complete", clear running state immediately
|
||||||
if (phase === "complete") {
|
if (phase === "complete") {
|
||||||
console.log("[SpecView] Phase is complete - clearing state");
|
console.log("[SpecView] Phase is complete - clearing state");
|
||||||
|
setIsCreating(false);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
stateRestoredRef.current = false;
|
||||||
|
// Small delay to ensure spec file is written
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSpec();
|
||||||
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completion indicators in content
|
||||||
|
if (
|
||||||
|
event.content.includes("All tasks completed") ||
|
||||||
|
event.content.includes("✓ All tasks completed")
|
||||||
|
) {
|
||||||
|
// This indicates everything is done - clear state immediately
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Detected completion in progress message - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
|
setCurrentPhase("");
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
// Small delay to ensure spec file is written
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadSpec();
|
loadSpec();
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for completion indicators in content
|
// Append progress to logs
|
||||||
if (event.content.includes("All tasks completed") ||
|
const newLog = logsRef.current + event.content;
|
||||||
event.content.includes("✓ All tasks completed")) {
|
logsRef.current = newLog;
|
||||||
// This indicates everything is done - clear state immediately
|
setLogs(newLog);
|
||||||
console.log("[SpecView] Detected completion in progress message - clearing state");
|
console.log("[SpecView] Progress:", event.content.substring(0, 100));
|
||||||
setIsCreating(false);
|
|
||||||
setIsRegenerating(false);
|
|
||||||
setCurrentPhase("");
|
|
||||||
stateRestoredRef.current = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
loadSpec();
|
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append progress to logs
|
// Clear error message when we get new progress
|
||||||
const newLog = logsRef.current + event.content;
|
if (errorMessage) {
|
||||||
logsRef.current = newLog;
|
setErrorMessage("");
|
||||||
setLogs(newLog);
|
}
|
||||||
console.log("[SpecView] Progress:", event.content.substring(0, 100));
|
} else if (event.type === "spec_regeneration_tool") {
|
||||||
|
// Check if this is a feature creation tool
|
||||||
// Clear error message when we get new progress
|
const isFeatureTool =
|
||||||
if (errorMessage) {
|
event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
|
||||||
setErrorMessage("");
|
event.tool === "UpdateFeatureStatus" ||
|
||||||
}
|
event.tool?.includes("Feature");
|
||||||
} else if (event.type === "spec_regeneration_tool") {
|
|
||||||
// Check if this is a feature creation tool
|
if (isFeatureTool) {
|
||||||
const isFeatureTool = event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
|
// Ensure we're in feature generation phase
|
||||||
event.tool === "UpdateFeatureStatus" ||
|
if (currentPhase !== "feature_generation") {
|
||||||
event.tool?.includes("Feature");
|
setCurrentPhase("feature_generation");
|
||||||
|
setIsCreating(true);
|
||||||
if (isFeatureTool) {
|
setIsRegenerating(true);
|
||||||
// Ensure we're in feature generation phase
|
console.log(
|
||||||
if (currentPhase !== "feature_generation") {
|
"[SpecView] Detected feature creation tool - setting phase to feature_generation"
|
||||||
setCurrentPhase("feature_generation");
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log tool usage with details
|
||||||
|
const toolInput = event.input
|
||||||
|
? ` (${JSON.stringify(event.input).substring(0, 100)}...)`
|
||||||
|
: "";
|
||||||
|
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
|
||||||
|
const newLog = logsRef.current + toolLog;
|
||||||
|
logsRef.current = newLog;
|
||||||
|
setLogs(newLog);
|
||||||
|
console.log("[SpecView] Tool:", event.tool, event.input);
|
||||||
|
} else if (event.type === "spec_regeneration_complete") {
|
||||||
|
// Add completion message to logs first
|
||||||
|
const completionLog =
|
||||||
|
logsRef.current + `\n[Complete] ${event.message}\n`;
|
||||||
|
logsRef.current = completionLog;
|
||||||
|
setLogs(completionLog);
|
||||||
|
|
||||||
|
// --- Completion Detection Logic ---
|
||||||
|
// The backend sends explicit signals for completion:
|
||||||
|
// 1. "All tasks completed" in the message
|
||||||
|
// 2. [Phase: complete] marker in logs
|
||||||
|
// 3. "Spec regeneration complete!" for regeneration
|
||||||
|
// 4. "Initial spec creation complete!" for creation without features
|
||||||
|
const isFinalCompletionMessage =
|
||||||
|
event.message?.includes("All tasks completed") ||
|
||||||
|
event.message === "All tasks completed!" ||
|
||||||
|
event.message === "All tasks completed" ||
|
||||||
|
event.message === "Spec regeneration complete!" ||
|
||||||
|
event.message === "Initial spec creation complete!";
|
||||||
|
|
||||||
|
const hasCompletePhase =
|
||||||
|
logsRef.current.includes("[Phase: complete]");
|
||||||
|
|
||||||
|
// Intermediate completion means features are being generated after spec creation
|
||||||
|
const isIntermediateCompletion =
|
||||||
|
event.message?.includes("Features are being generated") ||
|
||||||
|
event.message?.includes("features are being generated");
|
||||||
|
|
||||||
|
// Rely solely on explicit backend signals
|
||||||
|
const shouldComplete =
|
||||||
|
(isFinalCompletionMessage || hasCompletePhase) &&
|
||||||
|
!isIntermediateCompletion;
|
||||||
|
|
||||||
|
if (shouldComplete) {
|
||||||
|
// Fully complete - clear all states immediately
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Final completion detected - clearing state",
|
||||||
|
{
|
||||||
|
isFinalCompletionMessage,
|
||||||
|
hasCompletePhase,
|
||||||
|
message: event.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
setIsCreating(false);
|
||||||
|
setIsGeneratingFeatures(false);
|
||||||
|
setCurrentPhase("");
|
||||||
|
setShowRegenerateDialog(false);
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
setProjectDefinition("");
|
||||||
|
setProjectOverview("");
|
||||||
|
setErrorMessage("");
|
||||||
|
stateRestoredRef.current = false;
|
||||||
|
|
||||||
|
// Reload the spec with delay to ensure file is written to disk
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSpec();
|
||||||
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
|
|
||||||
|
// Show success toast notification
|
||||||
|
const isRegeneration = event.message?.includes("regeneration");
|
||||||
|
const isFeatureGeneration =
|
||||||
|
event.message?.includes("Feature generation");
|
||||||
|
toast.success(
|
||||||
|
isFeatureGeneration
|
||||||
|
? "Feature Generation Complete"
|
||||||
|
: isRegeneration
|
||||||
|
? "Spec Regeneration Complete"
|
||||||
|
: "Spec Creation Complete",
|
||||||
|
{
|
||||||
|
description: isFeatureGeneration
|
||||||
|
? "Features have been created from the app specification."
|
||||||
|
: "Your app specification has been saved.",
|
||||||
|
icon: <CheckCircle2 className="w-4 h-4" />,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (isIntermediateCompletion) {
|
||||||
|
// Intermediate completion - keep state active for feature generation
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
console.log("[SpecView] Detected feature creation tool - setting phase to feature_generation");
|
setCurrentPhase("feature_generation");
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Intermediate completion, continuing with feature generation"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
console.log("[SpecView] Spec generation event:", event.message);
|
||||||
// Log tool usage with details
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
const toolInput = event.input ? ` (${JSON.stringify(event.input).substring(0, 100)}...)` : "";
|
|
||||||
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
|
|
||||||
const newLog = logsRef.current + toolLog;
|
|
||||||
logsRef.current = newLog;
|
|
||||||
setLogs(newLog);
|
|
||||||
console.log("[SpecView] Tool:", event.tool, event.input);
|
|
||||||
} else if (event.type === "spec_regeneration_complete") {
|
|
||||||
// Add completion message to logs first
|
|
||||||
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
|
|
||||||
logsRef.current = completionLog;
|
|
||||||
setLogs(completionLog);
|
|
||||||
|
|
||||||
// --- Completion Detection Logic ---
|
|
||||||
// The backend sends explicit signals for completion:
|
|
||||||
// 1. "All tasks completed" in the message
|
|
||||||
// 2. [Phase: complete] marker in logs
|
|
||||||
// 3. "Spec regeneration complete!" for regeneration
|
|
||||||
// 4. "Initial spec creation complete!" for creation without features
|
|
||||||
const isFinalCompletionMessage = event.message?.includes("All tasks completed") ||
|
|
||||||
event.message === "All tasks completed!" ||
|
|
||||||
event.message === "All tasks completed" ||
|
|
||||||
event.message === "Spec regeneration complete!" ||
|
|
||||||
event.message === "Initial spec creation complete!";
|
|
||||||
|
|
||||||
const hasCompletePhase = logsRef.current.includes("[Phase: complete]");
|
|
||||||
|
|
||||||
// Intermediate completion means features are being generated after spec creation
|
|
||||||
const isIntermediateCompletion = event.message?.includes("Features are being generated") ||
|
|
||||||
event.message?.includes("features are being generated");
|
|
||||||
|
|
||||||
// Rely solely on explicit backend signals
|
|
||||||
const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion;
|
|
||||||
|
|
||||||
if (shouldComplete) {
|
|
||||||
// Fully complete - clear all states immediately
|
|
||||||
console.log("[SpecView] Final completion detected - clearing state", {
|
|
||||||
isFinalCompletionMessage,
|
|
||||||
hasCompletePhase,
|
|
||||||
message: event.message
|
|
||||||
});
|
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("");
|
setCurrentPhase("error");
|
||||||
setShowRegenerateDialog(false);
|
setErrorMessage(event.error);
|
||||||
setShowCreateDialog(false);
|
stateRestoredRef.current = false; // Reset restoration flag
|
||||||
setProjectDefinition("");
|
// Add error to logs
|
||||||
setProjectOverview("");
|
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
|
||||||
setErrorMessage("");
|
logsRef.current = errorLog;
|
||||||
stateRestoredRef.current = false;
|
setLogs(errorLog);
|
||||||
|
console.error("[SpecView] Regeneration error:", event.error);
|
||||||
// Reload the spec with delay to ensure file is written to disk
|
|
||||||
setTimeout(() => {
|
|
||||||
loadSpec();
|
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
|
||||||
|
|
||||||
// Show success toast notification
|
|
||||||
const isRegeneration = event.message?.includes("regeneration");
|
|
||||||
const isFeatureGeneration = event.message?.includes("Feature generation");
|
|
||||||
toast.success(
|
|
||||||
isFeatureGeneration
|
|
||||||
? "Feature Generation Complete"
|
|
||||||
: isRegeneration
|
|
||||||
? "Spec Regeneration Complete"
|
|
||||||
: "Spec Creation Complete",
|
|
||||||
{
|
|
||||||
description: isFeatureGeneration
|
|
||||||
? "Features have been created from the app specification."
|
|
||||||
: "Your app specification has been saved.",
|
|
||||||
icon: <CheckCircle2 className="w-4 h-4" />,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (isIntermediateCompletion) {
|
|
||||||
// Intermediate completion - keep state active for feature generation
|
|
||||||
setIsCreating(true);
|
|
||||||
setIsRegenerating(true);
|
|
||||||
setCurrentPhase("feature_generation");
|
|
||||||
console.log("[SpecView] Intermediate completion, continuing with feature generation");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecView] Spec generation event:", event.message);
|
|
||||||
} else if (event.type === "spec_regeneration_error") {
|
|
||||||
setIsRegenerating(false);
|
|
||||||
setIsCreating(false);
|
|
||||||
setIsGeneratingFeatures(false);
|
|
||||||
setCurrentPhase("error");
|
|
||||||
setErrorMessage(event.error);
|
|
||||||
stateRestoredRef.current = false; // Reset restoration flag
|
|
||||||
// Add error to logs
|
|
||||||
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
|
|
||||||
logsRef.current = errorLog;
|
|
||||||
setLogs(errorLog);
|
|
||||||
console.error("[SpecView] Regeneration error:", event.error);
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
@@ -476,7 +545,10 @@ export function SpecView() {
|
|||||||
// Reset logs when starting new generation
|
// Reset logs when starting new generation
|
||||||
logsRef.current = "";
|
logsRef.current = "";
|
||||||
setLogs("");
|
setLogs("");
|
||||||
console.log("[SpecView] Starting spec creation, generateFeatures:", generateFeatures);
|
console.log(
|
||||||
|
"[SpecView] Starting spec creation, generateFeatures:",
|
||||||
|
generateFeatures
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) {
|
if (!api.specRegeneration) {
|
||||||
@@ -537,7 +609,10 @@ export function SpecView() {
|
|||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errorMsg = result.error || "Unknown error";
|
const errorMsg = result.error || "Unknown error";
|
||||||
console.error("[SpecView] Failed to start feature generation:", errorMsg);
|
console.error(
|
||||||
|
"[SpecView] Failed to start feature generation:",
|
||||||
|
errorMsg
|
||||||
|
);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("error");
|
setCurrentPhase("error");
|
||||||
setErrorMessage(errorMsg);
|
setErrorMessage(errorMsg);
|
||||||
@@ -606,18 +681,31 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||||
{isCreating ? "Generating Specification" : "Regenerating Specification"}
|
{isCreating
|
||||||
|
? "Generating Specification"
|
||||||
|
: "Regenerating Specification"}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && (
|
{currentPhase && (
|
||||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -653,12 +741,23 @@ export function SpecView() {
|
|||||||
<span className="text-sm font-semibold text-primary text-center tracking-tight">
|
<span className="text-sm font-semibold text-primary text-center tracking-tight">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -682,10 +781,7 @@ export function SpecView() {
|
|||||||
)}
|
)}
|
||||||
{!isCreating && (
|
{!isCreating && (
|
||||||
<div className="flex gap-2 justify-center">
|
<div className="flex gap-2 justify-center">
|
||||||
<Button
|
<Button size="lg" onClick={() => setShowCreateDialog(true)}>
|
||||||
size="lg"
|
|
||||||
onClick={() => setShowCreateDialog(true)}
|
|
||||||
>
|
|
||||||
<FilePlus2 className="w-5 h-5 mr-2" />
|
<FilePlus2 className="w-5 h-5 mr-2" />
|
||||||
Create app_spec
|
Create app_spec
|
||||||
</Button>
|
</Button>
|
||||||
@@ -695,8 +791,8 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Dialog */}
|
{/* Create Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showCreateDialog}
|
open={showCreateDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open && !isCreating) {
|
if (!open && !isCreating) {
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
@@ -707,20 +803,20 @@ export function SpecView() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create App Specification</DialogTitle>
|
<DialogTitle>Create App Specification</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt
|
We didn't find an app_spec.txt file. Let us help you
|
||||||
to help describe your project for our system. We'll analyze your project's
|
generate your app_spec.txt to help describe your project for our
|
||||||
tech stack and create a comprehensive specification.
|
system. We'll analyze your project's tech stack and
|
||||||
|
create a comprehensive specification.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">
|
<label className="text-sm font-medium">Project Overview</label>
|
||||||
Project Overview
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Describe what your project does and what features you want to build.
|
Describe what your project does and what features you want to
|
||||||
Be as detailed as you want - this will help us create a better specification.
|
build. Be as detailed as you want - this will help us create a
|
||||||
|
better specification.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
@@ -736,19 +832,23 @@ export function SpecView() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="generate-features"
|
id="generate-features"
|
||||||
checked={generateFeatures}
|
checked={generateFeatures}
|
||||||
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
|
onCheckedChange={(checked) =>
|
||||||
|
setGenerateFeatures(checked === true)
|
||||||
|
}
|
||||||
disabled={isCreating}
|
disabled={isCreating}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label
|
<label
|
||||||
htmlFor="generate-features"
|
htmlFor="generate-features"
|
||||||
className={`text-sm font-medium ${isCreating ? "" : "cursor-pointer"}`}
|
className={`text-sm font-medium ${
|
||||||
|
isCreating ? "" : "cursor-pointer"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Generate feature list
|
Generate feature list
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Automatically create features in the features folder from the
|
Automatically create features in the features folder from
|
||||||
implementation roadmap after the spec is generated.
|
the implementation roadmap after the spec is generated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -812,18 +912,33 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||||
{isGeneratingFeatures ? "Generating Features" : isCreating ? "Generating Specification" : "Regenerating Specification"}
|
{isGeneratingFeatures
|
||||||
|
? "Generating Features"
|
||||||
|
: isCreating
|
||||||
|
? "Generating Specification"
|
||||||
|
: "Regenerating Specification"}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && (
|
{currentPhase && (
|
||||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -833,8 +948,12 @@ export function SpecView() {
|
|||||||
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
|
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
|
||||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
|
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">Error</span>
|
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">
|
||||||
<span className="text-xs text-destructive/90 leading-tight font-medium">{errorMessage}</span>
|
Error
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-destructive/90 leading-tight font-medium">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -856,7 +975,13 @@ export function SpecView() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={saveSpec}
|
onClick={saveSpec}
|
||||||
disabled={!hasChanges || isSaving || isCreating || isRegenerating || isGeneratingFeatures}
|
disabled={
|
||||||
|
!hasChanges ||
|
||||||
|
isSaving ||
|
||||||
|
isCreating ||
|
||||||
|
isRegenerating ||
|
||||||
|
isGeneratingFeatures
|
||||||
|
}
|
||||||
data-testid="save-spec"
|
data-testid="save-spec"
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
@@ -868,7 +993,7 @@ export function SpecView() {
|
|||||||
|
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div className="flex-1 p-4 overflow-hidden">
|
<div className="flex-1 p-4 overflow-hidden">
|
||||||
<Card className="h-full overflow-hidden">
|
<Card className="h-full">
|
||||||
<XmlSyntaxEditor
|
<XmlSyntaxEditor
|
||||||
value={appSpec}
|
value={appSpec}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -879,8 +1004,8 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Regenerate Dialog */}
|
{/* Regenerate Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showRegenerateDialog}
|
open={showRegenerateDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open && !isRegenerating) {
|
if (!open && !isRegenerating) {
|
||||||
setShowRegenerateDialog(false);
|
setShowRegenerateDialog(false);
|
||||||
@@ -891,9 +1016,10 @@ export function SpecView() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Regenerate App Specification</DialogTitle>
|
<DialogTitle>Regenerate App Specification</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
We will regenerate your app spec based on a short project definition and the
|
We will regenerate your app spec based on a short project
|
||||||
current tech stack found in your project. The agent will analyze your codebase
|
definition and the current tech stack found in your project. The
|
||||||
to understand your existing technologies and create a comprehensive specification.
|
agent will analyze your codebase to understand your existing
|
||||||
|
technologies and create a comprehensive specification.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -903,8 +1029,9 @@ export function SpecView() {
|
|||||||
Describe your project
|
Describe your project
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Provide a clear description of what your app should do. Be as detailed as you
|
Provide a clear description of what your app should do. Be as
|
||||||
want - the more context you provide, the more comprehensive the spec will be.
|
detailed as you want - the more context you provide, the more
|
||||||
|
comprehensive the spec will be.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
@@ -945,9 +1072,17 @@ export function SpecView() {
|
|||||||
</Button>
|
</Button>
|
||||||
<HotkeyButton
|
<HotkeyButton
|
||||||
onClick={handleRegenerate}
|
onClick={handleRegenerate}
|
||||||
disabled={!projectDefinition.trim() || isRegenerating || isGeneratingFeatures}
|
disabled={
|
||||||
|
!projectDefinition.trim() ||
|
||||||
|
isRegenerating ||
|
||||||
|
isGeneratingFeatures
|
||||||
|
}
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||||
hotkeyActive={showRegenerateDialog && !isRegenerating && !isGeneratingFeatures}
|
hotkeyActive={
|
||||||
|
showRegenerateDialog &&
|
||||||
|
!isRegenerating &&
|
||||||
|
!isGeneratingFeatures
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isRegenerating ? (
|
{isRegenerating ? (
|
||||||
<>
|
<>
|
||||||
@@ -965,7 +1100,6 @@ export function SpecView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ export interface CliStatus {
|
|||||||
|
|
||||||
// Claude Auth Method - all possible authentication sources
|
// Claude Auth Method - all possible authentication sources
|
||||||
export type ClaudeAuthMethod =
|
export type ClaudeAuthMethod =
|
||||||
| "oauth_token_env" // CLAUDE_CODE_OAUTH_TOKEN environment variable
|
| "oauth_token_env" // CLAUDE_CODE_OAUTH_TOKEN environment variable
|
||||||
| "oauth_token" // Stored OAuth token from claude login
|
| "oauth_token" // Stored OAuth token from claude login
|
||||||
| "api_key_env" // ANTHROPIC_API_KEY environment variable
|
| "api_key_env" // ANTHROPIC_API_KEY environment variable
|
||||||
| "api_key" // Manually stored API key
|
| "api_key" // Manually stored API key
|
||||||
| "credentials_file" // Generic credentials file detection
|
| "credentials_file" // Generic credentials file detection
|
||||||
| "cli_authenticated" // Claude CLI is installed and has active sessions/activity
|
| "cli_authenticated" // Claude CLI is installed and has active sessions/activity
|
||||||
| "none";
|
| "none";
|
||||||
|
|
||||||
// Claude Auth Status
|
// Claude Auth Status
|
||||||
@@ -86,16 +86,19 @@ const initialInstallProgress: InstallProgress = {
|
|||||||
output: [],
|
output: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if setup should be skipped (for E2E testing)
|
||||||
|
const shouldSkipSetup = process.env.NEXT_PUBLIC_SKIP_SETUP === "true";
|
||||||
|
|
||||||
const initialState: SetupState = {
|
const initialState: SetupState = {
|
||||||
isFirstRun: true,
|
isFirstRun: !shouldSkipSetup,
|
||||||
setupComplete: false,
|
setupComplete: shouldSkipSetup,
|
||||||
currentStep: "welcome",
|
currentStep: shouldSkipSetup ? "complete" : "welcome",
|
||||||
|
|
||||||
claudeCliStatus: null,
|
claudeCliStatus: null,
|
||||||
claudeAuthStatus: null,
|
claudeAuthStatus: null,
|
||||||
claudeInstallProgress: { ...initialInstallProgress },
|
claudeInstallProgress: { ...initialInstallProgress },
|
||||||
|
|
||||||
skipClaudeSetup: false,
|
skipClaudeSetup: shouldSkipSetup,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSetupStore = create<SetupState & SetupActions>()(
|
export const useSetupStore = create<SetupState & SetupActions>()(
|
||||||
@@ -106,12 +109,14 @@ export const useSetupStore = create<SetupState & SetupActions>()(
|
|||||||
// Setup flow
|
// Setup flow
|
||||||
setCurrentStep: (step) => set({ currentStep: step }),
|
setCurrentStep: (step) => set({ currentStep: step }),
|
||||||
|
|
||||||
completeSetup: () => set({ setupComplete: true, currentStep: "complete" }),
|
completeSetup: () =>
|
||||||
|
set({ setupComplete: true, currentStep: "complete" }),
|
||||||
|
|
||||||
resetSetup: () => set({
|
resetSetup: () =>
|
||||||
...initialState,
|
set({
|
||||||
isFirstRun: false, // Don't reset first run flag
|
...initialState,
|
||||||
}),
|
isFirstRun: false, // Don't reset first run flag
|
||||||
|
}),
|
||||||
|
|
||||||
setIsFirstRun: (isFirstRun) => set({ isFirstRun }),
|
setIsFirstRun: (isFirstRun) => set({ isFirstRun }),
|
||||||
|
|
||||||
@@ -120,16 +125,18 @@ export const useSetupStore = create<SetupState & SetupActions>()(
|
|||||||
|
|
||||||
setClaudeAuthStatus: (status) => set({ claudeAuthStatus: status }),
|
setClaudeAuthStatus: (status) => set({ claudeAuthStatus: status }),
|
||||||
|
|
||||||
setClaudeInstallProgress: (progress) => set({
|
setClaudeInstallProgress: (progress) =>
|
||||||
claudeInstallProgress: {
|
set({
|
||||||
...get().claudeInstallProgress,
|
claudeInstallProgress: {
|
||||||
...progress,
|
...get().claudeInstallProgress,
|
||||||
},
|
...progress,
|
||||||
}),
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
resetClaudeInstallProgress: () => set({
|
resetClaudeInstallProgress: () =>
|
||||||
claudeInstallProgress: { ...initialInstallProgress },
|
set({
|
||||||
}),
|
claudeInstallProgress: { ...initialInstallProgress },
|
||||||
|
}),
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
setSkipClaudeSetup: (skip) => set({ skipClaudeSetup: skip }),
|
setSkipClaudeSetup: (skip) => set({ skipClaudeSetup: skip }),
|
||||||
|
|||||||
472
apps/app/tests/spec-editor-persistence.spec.ts
Normal file
472
apps/app/tests/spec-editor-persistence.spec.ts
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
import { test, expect, Page } from "@playwright/test";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
// Resolve the workspace root - handle both running from apps/app and from root
|
||||||
|
function getWorkspaceRoot(): string {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
if (cwd.includes("apps/app")) {
|
||||||
|
return path.resolve(cwd, "../..");
|
||||||
|
}
|
||||||
|
return cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_ROOT = getWorkspaceRoot();
|
||||||
|
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
|
||||||
|
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");
|
||||||
|
|
||||||
|
// Original spec content for resetting between tests
|
||||||
|
const ORIGINAL_SPEC_CONTENT = `<app_spec>
|
||||||
|
<name>Test Project A</name>
|
||||||
|
<description>A test fixture project for Playwright testing</description>
|
||||||
|
<tech_stack>
|
||||||
|
<item>TypeScript</item>
|
||||||
|
<item>React</item>
|
||||||
|
</tech_stack>
|
||||||
|
</app_spec>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the fixture's app_spec.txt to original content
|
||||||
|
*/
|
||||||
|
function resetFixtureSpec() {
|
||||||
|
const dir = path.dirname(SPEC_FILE_PATH);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(SPEC_FILE_PATH, ORIGINAL_SPEC_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up localStorage with a project pointing to our test fixture
|
||||||
|
* Note: In CI, setup wizard is also skipped via NEXT_PUBLIC_SKIP_SETUP env var
|
||||||
|
*/
|
||||||
|
async function setupProjectWithFixture(page: Page, projectPath: string) {
|
||||||
|
await page.addInitScript((path: string) => {
|
||||||
|
const mockProject = {
|
||||||
|
id: "test-project-fixture",
|
||||||
|
name: "projectA",
|
||||||
|
path: path,
|
||||||
|
lastOpened: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
state: {
|
||||||
|
projects: [mockProject],
|
||||||
|
currentProject: mockProject,
|
||||||
|
currentView: "board",
|
||||||
|
theme: "dark",
|
||||||
|
sidebarOpen: true,
|
||||||
|
apiKeys: { anthropic: "", google: "" },
|
||||||
|
chatSessions: [],
|
||||||
|
chatHistoryOpen: false,
|
||||||
|
maxConcurrency: 3,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||||
|
|
||||||
|
// Also mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
|
||||||
|
const setupState = {
|
||||||
|
state: {
|
||||||
|
isFirstRun: false,
|
||||||
|
setupComplete: true,
|
||||||
|
currentStep: "complete",
|
||||||
|
skipClaudeSetup: false,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
|
||||||
|
}, projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to spec editor via sidebar
|
||||||
|
*/
|
||||||
|
async function navigateToSpecEditor(page: Page) {
|
||||||
|
// Click on the Spec Editor nav item in the sidebar
|
||||||
|
const specNavButton = page.locator('[data-testid="nav-spec"]');
|
||||||
|
await specNavButton.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
await specNavButton.click();
|
||||||
|
|
||||||
|
// Wait for the spec view to be visible
|
||||||
|
await page.waitForSelector('[data-testid="spec-view"]', { timeout: 10000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the CodeMirror editor content
|
||||||
|
*/
|
||||||
|
async function getEditorContent(page: Page): Promise<string> {
|
||||||
|
// CodeMirror uses a contenteditable div with class .cm-content
|
||||||
|
const content = await page
|
||||||
|
.locator('[data-testid="spec-editor"] .cm-content')
|
||||||
|
.textContent();
|
||||||
|
return content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the CodeMirror editor content by selecting all and typing
|
||||||
|
*/
|
||||||
|
async function setEditorContent(page: Page, content: string) {
|
||||||
|
// Click on the editor to focus it
|
||||||
|
const editor = page.locator('[data-testid="spec-editor"] .cm-content');
|
||||||
|
await editor.click();
|
||||||
|
|
||||||
|
// Wait for focus
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
// Select all content (Cmd+A on Mac, Ctrl+A on others)
|
||||||
|
const isMac = process.platform === "darwin";
|
||||||
|
await page.keyboard.press(isMac ? "Meta+a" : "Control+a");
|
||||||
|
|
||||||
|
// Wait for selection
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
// Delete the selected content first
|
||||||
|
await page.keyboard.press("Backspace");
|
||||||
|
|
||||||
|
// Wait for deletion
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
// Type the new content
|
||||||
|
await page.keyboard.type(content, { delay: 10 });
|
||||||
|
|
||||||
|
// Wait for typing to complete
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the save button
|
||||||
|
*/
|
||||||
|
async function clickSaveButton(page: Page) {
|
||||||
|
const saveButton = page.locator('[data-testid="save-spec"]');
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Wait for the button text to change to "Saved" indicating save is complete
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const btn = document.querySelector('[data-testid="save-spec"]');
|
||||||
|
return btn?.textContent?.includes("Saved");
|
||||||
|
},
|
||||||
|
{ timeout: 5000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Spec Editor Persistence", () => {
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
// Reset the fixture spec file to original content before each test
|
||||||
|
resetFixtureSpec();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
// Clean up - reset the spec file after each test
|
||||||
|
resetFixtureSpec();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open project, edit spec, save, and persist changes after refresh", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Use the resolved fixture path
|
||||||
|
const fixturePath = FIXTURE_PATH;
|
||||||
|
|
||||||
|
// Step 1: Set up the project in localStorage pointing to our fixture
|
||||||
|
await setupProjectWithFixture(page, fixturePath);
|
||||||
|
|
||||||
|
// Step 2: Navigate to the app
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Step 3: Verify we're on the dashboard with the project loaded
|
||||||
|
// The sidebar should show the project selector
|
||||||
|
const sidebar = page.locator('[data-testid="sidebar"]');
|
||||||
|
await sidebar.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
|
||||||
|
// Step 4: Click on the Spec Editor in the sidebar
|
||||||
|
await navigateToSpecEditor(page);
|
||||||
|
|
||||||
|
// Step 5: Wait for the spec editor to load
|
||||||
|
const specEditor = page.locator('[data-testid="spec-editor"]');
|
||||||
|
await specEditor.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
|
||||||
|
// Step 6: Wait for CodeMirror to initialize (it has a .cm-content element)
|
||||||
|
await page.waitForSelector('[data-testid="spec-editor"] .cm-content', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small delay to ensure editor is fully initialized
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Step 7: Modify the editor content to "hello world"
|
||||||
|
await setEditorContent(page, "hello world");
|
||||||
|
|
||||||
|
// Step 8: Click the save button
|
||||||
|
await clickSaveButton(page);
|
||||||
|
|
||||||
|
// Step 9: Refresh the page
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Step 10: Navigate back to the spec editor
|
||||||
|
// After reload, we need to wait for the app to initialize
|
||||||
|
await page.waitForSelector('[data-testid="sidebar"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Navigate to spec editor again
|
||||||
|
await navigateToSpecEditor(page);
|
||||||
|
|
||||||
|
// Wait for CodeMirror to be ready
|
||||||
|
await page.waitForSelector('[data-testid="spec-editor"] .cm-content', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small delay to ensure editor content is loaded
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Step 11: Verify the content was persisted
|
||||||
|
const persistedContent = await getEditorContent(page);
|
||||||
|
expect(persistedContent.trim()).toBe("hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle opening project via Open Project button and file browser", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// This test covers the flow of:
|
||||||
|
// 1. Clicking Open Project button
|
||||||
|
// 2. Using the file browser to navigate to the fixture directory
|
||||||
|
// 3. Opening the project
|
||||||
|
// 4. Editing the spec
|
||||||
|
|
||||||
|
// Set up without a current project to test the open project flow
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const mockState = {
|
||||||
|
state: {
|
||||||
|
projects: [],
|
||||||
|
currentProject: null,
|
||||||
|
currentView: "welcome",
|
||||||
|
theme: "dark",
|
||||||
|
sidebarOpen: true,
|
||||||
|
apiKeys: { anthropic: "", google: "" },
|
||||||
|
chatSessions: [],
|
||||||
|
chatHistoryOpen: false,
|
||||||
|
maxConcurrency: 3,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||||
|
|
||||||
|
// Mark setup as complete
|
||||||
|
const setupState = {
|
||||||
|
state: {
|
||||||
|
isFirstRun: false,
|
||||||
|
setupComplete: true,
|
||||||
|
currentStep: "complete",
|
||||||
|
skipClaudeSetup: false,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate to the app
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Wait for the sidebar to be visible
|
||||||
|
const sidebar = page.locator('[data-testid="sidebar"]');
|
||||||
|
await sidebar.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Open Project button
|
||||||
|
const openProjectButton = page.locator(
|
||||||
|
'[data-testid="open-project-button"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the button is visible (it might not be in collapsed sidebar)
|
||||||
|
const isButtonVisible = await openProjectButton
|
||||||
|
.isVisible()
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (isButtonVisible) {
|
||||||
|
await openProjectButton.click();
|
||||||
|
|
||||||
|
// The file browser dialog should open
|
||||||
|
// Note: In web mode, this might use the FileBrowserDialog component
|
||||||
|
// which makes requests to the backend server at /api/fs/browse
|
||||||
|
|
||||||
|
// Wait a bit to see if a dialog appears
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Check if a dialog is visible
|
||||||
|
const dialog = page.locator('[role="dialog"]');
|
||||||
|
const dialogVisible = await dialog.isVisible().catch(() => false);
|
||||||
|
|
||||||
|
if (dialogVisible) {
|
||||||
|
// If file browser dialog is open, we need to navigate to the fixture path
|
||||||
|
// This depends on the current directory structure
|
||||||
|
|
||||||
|
// For now, let's verify the dialog appeared and close it
|
||||||
|
// A full test would navigate through directories
|
||||||
|
console.log("File browser dialog opened successfully");
|
||||||
|
|
||||||
|
// Press Escape to close the dialog
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a complete e2e test with file browsing, we'd need to:
|
||||||
|
// 1. Navigate through the directory tree
|
||||||
|
// 2. Select the projectA directory
|
||||||
|
// 3. Click "Select Current Folder"
|
||||||
|
|
||||||
|
// Since this involves actual file system navigation,
|
||||||
|
// and depends on the backend server being properly configured,
|
||||||
|
// we'll verify the basic UI elements are present
|
||||||
|
|
||||||
|
expect(sidebar).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Spec Editor - Full Open Project Flow", () => {
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
// Reset the fixture spec file to original content before each test
|
||||||
|
resetFixtureSpec();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
// Clean up - reset the spec file after each test
|
||||||
|
resetFixtureSpec();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open project via file browser, edit spec, and persist", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Navigate to app first
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Set up localStorage state (without a current project, but mark setup complete)
|
||||||
|
// Using evaluate instead of addInitScript so it only runs once
|
||||||
|
// Note: In CI, setup wizard is also skipped via NEXT_PUBLIC_SKIP_SETUP env var
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const mockState = {
|
||||||
|
state: {
|
||||||
|
projects: [],
|
||||||
|
currentProject: null,
|
||||||
|
currentView: "welcome",
|
||||||
|
theme: "dark",
|
||||||
|
sidebarOpen: true,
|
||||||
|
apiKeys: { anthropic: "", google: "" },
|
||||||
|
chatSessions: [],
|
||||||
|
chatHistoryOpen: false,
|
||||||
|
maxConcurrency: 3,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||||
|
|
||||||
|
// Mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
|
||||||
|
const setupState = {
|
||||||
|
state: {
|
||||||
|
isFirstRun: false,
|
||||||
|
setupComplete: true,
|
||||||
|
currentStep: "complete",
|
||||||
|
skipClaudeSetup: false,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload to apply the localStorage state
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Wait for sidebar
|
||||||
|
await page.waitForSelector('[data-testid="sidebar"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Open Project button
|
||||||
|
const openProjectButton = page.locator(
|
||||||
|
'[data-testid="open-project-button"]'
|
||||||
|
);
|
||||||
|
await openProjectButton.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
await openProjectButton.click();
|
||||||
|
|
||||||
|
// Wait for the file browser dialog to open
|
||||||
|
const dialogTitle = page.locator('text="Select Project Directory"');
|
||||||
|
await dialogTitle.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
|
||||||
|
// Wait for the dialog to fully load (loading to complete)
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !document.body.textContent?.includes("Loading directories..."),
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the path input to directly navigate to the fixture directory
|
||||||
|
const pathInput = page.locator('[data-testid="path-input"]');
|
||||||
|
await pathInput.waitFor({ state: "visible", timeout: 5000 });
|
||||||
|
|
||||||
|
// Clear the input and type the full path to the fixture
|
||||||
|
await pathInput.fill(FIXTURE_PATH);
|
||||||
|
|
||||||
|
// Click the Go button to navigate to the path
|
||||||
|
const goButton = page.locator('[data-testid="go-to-path-button"]');
|
||||||
|
await goButton.click();
|
||||||
|
|
||||||
|
// Wait for loading to complete
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !document.body.textContent?.includes("Loading directories..."),
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify we're in the right directory by checking the path display
|
||||||
|
const pathDisplay = page.locator(".font-mono.text-sm.truncate");
|
||||||
|
await expect(pathDisplay).toContainText("projectA");
|
||||||
|
|
||||||
|
// Click "Select Current Folder" button
|
||||||
|
const selectFolderButton = page.locator(
|
||||||
|
'button:has-text("Select Current Folder")'
|
||||||
|
);
|
||||||
|
await selectFolderButton.click();
|
||||||
|
|
||||||
|
// Wait for dialog to close and project to load
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !document.querySelector('[role="dialog"]'),
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Navigate to spec editor
|
||||||
|
const specNav = page.locator('[data-testid="nav-spec"]');
|
||||||
|
await specNav.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
await specNav.click();
|
||||||
|
|
||||||
|
// Wait for spec view with the editor (not the empty state)
|
||||||
|
await page.waitForSelector('[data-testid="spec-view"]', { timeout: 10000 });
|
||||||
|
await page.waitForSelector('[data-testid="spec-editor"] .cm-content', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Edit the content
|
||||||
|
await setEditorContent(page, "hello world");
|
||||||
|
|
||||||
|
// Click save button
|
||||||
|
await clickSaveButton(page);
|
||||||
|
|
||||||
|
// Refresh and verify persistence
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Navigate back to spec editor
|
||||||
|
await specNav.waitFor({ state: "visible", timeout: 10000 });
|
||||||
|
await specNav.click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="spec-editor"] .cm-content', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify the content persisted
|
||||||
|
const persistedContent = await getEditorContent(page);
|
||||||
|
expect(persistedContent.trim()).toBe("hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
88
apps/server/src/lib/app-spec-format.ts
Normal file
88
apps/server/src/lib/app-spec-format.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* XML Template Format Specification for app_spec.txt
|
||||||
|
*
|
||||||
|
* This format must be included in all prompts that generate, modify, or regenerate
|
||||||
|
* app specifications to ensure consistency across the application.
|
||||||
|
*/
|
||||||
|
export const APP_SPEC_XML_FORMAT = `
|
||||||
|
The app_spec.txt file MUST follow this exact XML format:
|
||||||
|
|
||||||
|
<project_specification>
|
||||||
|
<project_name>Project Name</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
A comprehensive description of what the project does, its purpose, and key goals.
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<technology>Technology 1</technology>
|
||||||
|
<technology>Technology 2</technology>
|
||||||
|
<!-- List all technologies, frameworks, libraries, and tools used -->
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<capability>Core capability 1</capability>
|
||||||
|
<capability>Core capability 2</capability>
|
||||||
|
<!-- List main features and capabilities the project provides -->
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implemented_features>
|
||||||
|
<!-- Features that have been implemented (populated by AI agent based on code analysis) -->
|
||||||
|
</implemented_features>
|
||||||
|
|
||||||
|
<!-- Optional sections that may be included: -->
|
||||||
|
<additional_requirements>
|
||||||
|
<!-- Any additional requirements or constraints -->
|
||||||
|
</additional_requirements>
|
||||||
|
|
||||||
|
<development_guidelines>
|
||||||
|
<guideline>Guideline 1</guideline>
|
||||||
|
<guideline>Guideline 2</guideline>
|
||||||
|
<!-- Development standards and practices -->
|
||||||
|
</development_guidelines>
|
||||||
|
|
||||||
|
<implementation_roadmap>
|
||||||
|
<!-- Phases or roadmap items for implementation -->
|
||||||
|
</implementation_roadmap>
|
||||||
|
</project_specification>
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
- All content must be wrapped in valid XML tags
|
||||||
|
- Use proper XML escaping for special characters (<, >, &)
|
||||||
|
- Maintain proper indentation (2 spaces)
|
||||||
|
- All sections should be populated based on project analysis
|
||||||
|
- The format must be strictly followed - do not use markdown, JSON, or any other format
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a prompt suffix that instructs the AI to format the response as XML
|
||||||
|
* following the app_spec.txt template format.
|
||||||
|
*/
|
||||||
|
export function getAppSpecFormatInstruction(): string {
|
||||||
|
return `
|
||||||
|
${APP_SPEC_XML_FORMAT}
|
||||||
|
|
||||||
|
CRITICAL FORMATTING REQUIREMENTS:
|
||||||
|
- Your ENTIRE response MUST be valid XML following the exact template structure above
|
||||||
|
- Do NOT use markdown formatting (no # headers, no **bold**, no - lists, etc.)
|
||||||
|
- Do NOT include any explanatory text, prefix, or suffix outside the XML tags
|
||||||
|
- Do NOT include phrases like "Based on my analysis..." or "I'll create..." before the XML
|
||||||
|
- Do NOT include any text before <project_specification> or after </project_specification>
|
||||||
|
- Your response must start IMMEDIATELY with <project_specification> with no preceding text
|
||||||
|
- Your response must end IMMEDIATELY with </project_specification> with no following text
|
||||||
|
- Use ONLY XML tags as shown in the template
|
||||||
|
- Properly escape XML special characters (< for <, > for >, & for &)
|
||||||
|
- Maintain 2-space indentation for readability
|
||||||
|
- The output will be saved directly to app_spec.txt and must be parseable as valid XML
|
||||||
|
- The response must contain exactly ONE root XML element: <project_specification>
|
||||||
|
- Do not include code blocks, markdown fences, or any other formatting
|
||||||
|
|
||||||
|
VERIFICATION: Before responding, verify that:
|
||||||
|
1. Your response starts with <project_specification> (no spaces, no text before it)
|
||||||
|
2. Your response ends with </project_specification> (no spaces, no text after it)
|
||||||
|
3. There is exactly one root XML element
|
||||||
|
4. There is no explanatory text, analysis, or commentary outside the XML tags
|
||||||
|
|
||||||
|
Your response should be ONLY the XML content, nothing else.
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -15,13 +15,29 @@ let currentAbortController: AbortController | null = null;
|
|||||||
function logAuthStatus(context: string): void {
|
function logAuthStatus(context: string): void {
|
||||||
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] ${context} - Auth Status:`);
|
console.log(`[SpecRegeneration] ${context} - Auth Status:`);
|
||||||
console.log(`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${hasOAuthToken ? 'SET (' + process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...)' : 'NOT SET'}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] ANTHROPIC_API_KEY: ${hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'}`);
|
`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${
|
||||||
|
hasOAuthToken
|
||||||
|
? "SET (" +
|
||||||
|
process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) +
|
||||||
|
"...)"
|
||||||
|
: "NOT SET"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] ANTHROPIC_API_KEY: ${
|
||||||
|
hasApiKey
|
||||||
|
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
|
||||||
|
: "NOT SET"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!hasOAuthToken && !hasApiKey) {
|
if (!hasOAuthToken && !hasApiKey) {
|
||||||
console.error(`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`);
|
console.error(
|
||||||
|
`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +46,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Create project spec from overview
|
// Create project spec from overview
|
||||||
router.post("/create", async (req: Request, res: Response) => {
|
router.post("/create", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /create endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /create endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectOverview, generateFeatures } = req.body as {
|
const { projectPath, projectOverview, generateFeatures } = req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -42,7 +63,11 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
console.log(`[SpecRegeneration] Parsed params:`);
|
console.log(`[SpecRegeneration] Parsed params:`);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview?.length || 0} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectOverview length: ${
|
||||||
|
projectOverview?.length || 0
|
||||||
|
} chars`
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
if (!projectPath || !projectOverview) {
|
if (!projectPath || !projectOverview) {
|
||||||
@@ -55,7 +80,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: "Spec generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,19 +106,26 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_error",
|
type: "spec_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -103,9 +137,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Generate from project definition
|
// Generate from project definition
|
||||||
router.post("/generate", async (req: Request, res: Response) => {
|
router.post("/generate", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /generate endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /generate endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectDefinition } = req.body as {
|
const { projectPath, projectDefinition } = req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -114,7 +153,11 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
console.log(`[SpecRegeneration] Parsed params:`);
|
console.log(`[SpecRegeneration] Parsed params:`);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectDefinition length: ${projectDefinition?.length || 0} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectDefinition length: ${
|
||||||
|
projectDefinition?.length || 0
|
||||||
|
} chars`
|
||||||
|
);
|
||||||
|
|
||||||
if (!projectPath || !projectDefinition) {
|
if (!projectPath || !projectDefinition) {
|
||||||
console.error("[SpecRegeneration] Missing required parameters");
|
console.error("[SpecRegeneration] Missing required parameters");
|
||||||
@@ -126,7 +169,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: "Spec generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,19 +194,26 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_error",
|
type: "spec_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -173,9 +225,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Generate features from existing spec
|
// Generate features from existing spec
|
||||||
router.post("/generate-features", async (req: Request, res: Response) => {
|
router.post("/generate-features", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /generate-features endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /generate-features endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
@@ -188,7 +245,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Generation already running" });
|
res.json({ success: false, error: "Generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,27 +256,38 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
currentAbortController = new AbortController();
|
currentAbortController = new AbortController();
|
||||||
console.log("[SpecRegeneration] Starting background feature generation task...");
|
console.log(
|
||||||
|
"[SpecRegeneration] Starting background feature generation task..."
|
||||||
|
);
|
||||||
|
|
||||||
generateFeaturesFromSpec(projectPath, events, currentAbortController)
|
generateFeaturesFromSpec(projectPath, events, currentAbortController)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[SpecRegeneration] ❌ Feature generation failed with error:");
|
console.error(
|
||||||
|
"[SpecRegeneration] ❌ Feature generation failed with error:"
|
||||||
|
);
|
||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_error",
|
type: "features_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Feature generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Feature generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -261,11 +331,15 @@ async function generateSpec(
|
|||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
generateFeatures?: boolean
|
generateFeatures?: boolean
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== generateSpec() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateSpec() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
const prompt = `You are helping to define a software project specification.
|
const prompt = `You are helping to define a software project specification.
|
||||||
|
|
||||||
Project Overview:
|
Project Overview:
|
||||||
@@ -281,19 +355,23 @@ Based on this overview, analyze the project and create a comprehensive specifica
|
|||||||
6. **API Design** - Main endpoints/interfaces needed
|
6. **API Design** - Main endpoints/interfaces needed
|
||||||
7. **User Experience** - Key user flows and interactions
|
7. **User Experience** - Key user flows and interactions
|
||||||
|
|
||||||
${generateFeatures ? `
|
${
|
||||||
|
generateFeatures
|
||||||
|
? `
|
||||||
Also generate a list of features to implement. For each feature provide:
|
Also generate a list of features to implement. For each feature provide:
|
||||||
- ID (lowercase-hyphenated)
|
- ID (lowercase-hyphenated)
|
||||||
- Title
|
- Title
|
||||||
- Description
|
- Description
|
||||||
- Priority (1=high, 2=medium, 3=low)
|
- Priority (1=high, 2=medium, 3=low)
|
||||||
- Estimated complexity (simple, moderate, complex)
|
- Estimated complexity (simple, moderate, complex)
|
||||||
` : ""}
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
Format your response as markdown. Be specific and actionable.`;
|
Format your response as markdown. Be specific and actionable.`;
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_progress",
|
||||||
content: "Starting spec generation...\n",
|
content: "Starting spec generation...\n",
|
||||||
@@ -308,9 +386,12 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
|
console.log(
|
||||||
|
"[SpecRegeneration] SDK Options:",
|
||||||
|
JSON.stringify(options, null, 2)
|
||||||
|
);
|
||||||
console.log("[SpecRegeneration] Calling Claude Agent SDK query()...");
|
console.log("[SpecRegeneration] Calling Claude Agent SDK query()...");
|
||||||
|
|
||||||
// Log auth status right before the SDK call
|
// Log auth status right before the SDK call
|
||||||
logAuthStatus("Right before SDK query()");
|
logAuthStatus("Right before SDK query()");
|
||||||
|
|
||||||
@@ -332,13 +413,22 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
console.log(`[SpecRegeneration] Stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
|
console.log(
|
||||||
|
`[SpecRegeneration] Stream message #${messageCount}:`,
|
||||||
|
JSON.stringify(
|
||||||
|
{ type: msg.type, subtype: (msg as any).subtype },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant" && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText = block.text;
|
responseText = block.text;
|
||||||
console.log(`[SpecRegeneration] Text block received (${block.text.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Text block received (${block.text.length} chars)`
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
@@ -356,8 +446,13 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
console.log("[SpecRegeneration] Received success result");
|
console.log("[SpecRegeneration] Received success result");
|
||||||
responseText = (msg as any).result || responseText;
|
responseText = (msg as any).result || responseText;
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
console.error("[SpecRegeneration] ❌ Received error message from stream:");
|
console.error(
|
||||||
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
|
"[SpecRegeneration] ❌ Received error message from stream:"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"[SpecRegeneration] Error message:",
|
||||||
|
JSON.stringify(msg, null, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
@@ -366,15 +461,19 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] Response text length: ${responseText.length} chars`);
|
`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] Response text length: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
|
||||||
// Save spec
|
// Save spec
|
||||||
const specDir = path.join(projectPath, ".automaker");
|
const specDir = path.join(projectPath, ".automaker");
|
||||||
const specPath = path.join(specDir, "app_spec.txt");
|
const specPath = path.join(specDir, "app_spec.txt");
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Saving spec to: ${specPath}`);
|
console.log(`[SpecRegeneration] Saving spec to: ${specPath}`);
|
||||||
|
|
||||||
await fs.mkdir(specDir, { recursive: true });
|
await fs.mkdir(specDir, { recursive: true });
|
||||||
await fs.writeFile(specPath, responseText);
|
await fs.writeFile(specPath, responseText);
|
||||||
|
|
||||||
@@ -391,8 +490,10 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
console.log("[SpecRegeneration] Starting feature generation...");
|
console.log("[SpecRegeneration] Starting feature generation...");
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== generateSpec() completed ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateSpec() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateFeaturesFromSpec(
|
async function generateFeaturesFromSpec(
|
||||||
@@ -400,9 +501,11 @@ async function generateFeaturesFromSpec(
|
|||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
abortController: AbortController
|
abortController: AbortController
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateFeaturesFromSpec() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
|
|
||||||
// Read existing spec
|
// Read existing spec
|
||||||
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
||||||
let spec: string;
|
let spec: string;
|
||||||
@@ -411,7 +514,9 @@ async function generateFeaturesFromSpec(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
spec = await fs.readFile(specPath, "utf-8");
|
spec = await fs.readFile(specPath, "utf-8");
|
||||||
console.log(`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`
|
||||||
|
);
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError);
|
console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
@@ -466,9 +571,14 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
|
console.log(
|
||||||
console.log("[SpecRegeneration] Calling Claude Agent SDK query() for features...");
|
"[SpecRegeneration] SDK Options:",
|
||||||
|
JSON.stringify(options, null, 2)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Calling Claude Agent SDK query() for features..."
|
||||||
|
);
|
||||||
|
|
||||||
logAuthStatus("Right before SDK query() for features");
|
logAuthStatus("Right before SDK query() for features");
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
@@ -489,13 +599,22 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
console.log(`[SpecRegeneration] Feature stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature stream message #${messageCount}:`,
|
||||||
|
JSON.stringify(
|
||||||
|
{ type: msg.type, subtype: (msg as any).subtype },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant" && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText = block.text;
|
responseText = block.text;
|
||||||
console.log(`[SpecRegeneration] Feature text block received (${block.text.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature text block received (${block.text.length} chars)`
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_progress",
|
type: "features_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
@@ -506,22 +625,35 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
console.log("[SpecRegeneration] Received success result for features");
|
console.log("[SpecRegeneration] Received success result for features");
|
||||||
responseText = (msg as any).result || responseText;
|
responseText = (msg as any).result || responseText;
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
console.error("[SpecRegeneration] ❌ Received error message from feature stream:");
|
console.error(
|
||||||
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
|
"[SpecRegeneration] ❌ Received error message from feature stream:"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"[SpecRegeneration] Error message:",
|
||||||
|
JSON.stringify(msg, null, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
console.error("[SpecRegeneration] ❌ Error while iterating feature stream:");
|
console.error(
|
||||||
|
"[SpecRegeneration] ❌ Error while iterating feature stream:"
|
||||||
|
);
|
||||||
console.error("[SpecRegeneration] Stream error:", streamError);
|
console.error("[SpecRegeneration] Stream error:", streamError);
|
||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] Feature response length: ${responseText.length} chars`);
|
`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature response length: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() completed ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateFeaturesFromSpec() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseAndCreateFeatures(
|
async function parseAndCreateFeatures(
|
||||||
@@ -529,24 +661,33 @@ async function parseAndCreateFeatures(
|
|||||||
content: string,
|
content: string,
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== parseAndCreateFeatures() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] Content length: ${content.length} chars`);
|
console.log(`[SpecRegeneration] Content length: ${content.length} chars`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract JSON from response
|
// Extract JSON from response
|
||||||
console.log("[SpecRegeneration] Extracting JSON from response...");
|
console.log("[SpecRegeneration] Extracting JSON from response...");
|
||||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||||
if (!jsonMatch) {
|
if (!jsonMatch) {
|
||||||
console.error("[SpecRegeneration] ❌ No valid JSON found in response");
|
console.error("[SpecRegeneration] ❌ No valid JSON found in response");
|
||||||
console.error("[SpecRegeneration] Content preview:", content.substring(0, 500));
|
console.error(
|
||||||
|
"[SpecRegeneration] Content preview:",
|
||||||
|
content.substring(0, 500)
|
||||||
|
);
|
||||||
throw new Error("No valid JSON found in response");
|
throw new Error("No valid JSON found in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`
|
||||||
|
);
|
||||||
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
console.log(`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`
|
||||||
|
);
|
||||||
|
|
||||||
const featuresDir = path.join(projectPath, ".automaker", "features");
|
const featuresDir = path.join(projectPath, ".automaker", "features");
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
await fs.mkdir(featuresDir, { recursive: true });
|
||||||
|
|
||||||
@@ -561,7 +702,7 @@ async function parseAndCreateFeatures(
|
|||||||
id: feature.id,
|
id: feature.id,
|
||||||
title: feature.title,
|
title: feature.title,
|
||||||
description: feature.description,
|
description: feature.description,
|
||||||
status: "backlog", // Features go to backlog - user must manually start them
|
status: "backlog", // Features go to backlog - user must manually start them
|
||||||
priority: feature.priority || 2,
|
priority: feature.priority || 2,
|
||||||
complexity: feature.complexity || "moderate",
|
complexity: feature.complexity || "moderate",
|
||||||
dependencies: feature.dependencies || [],
|
dependencies: feature.dependencies || [],
|
||||||
@@ -577,7 +718,9 @@ async function parseAndCreateFeatures(
|
|||||||
createdFeatures.push({ id: feature.id, title: feature.title });
|
createdFeatures.push({ id: feature.id, title: feature.title });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`);
|
console.log(
|
||||||
|
`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`
|
||||||
|
);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_complete",
|
type: "features_complete",
|
||||||
@@ -592,6 +735,8 @@ async function parseAndCreateFeatures(
|
|||||||
error: (error as Error).message,
|
error: (error as Error).message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() completed ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== parseAndCreateFeatures() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
557
package-lock.json
generated
557
package-lock.json
generated
@@ -19,9 +19,12 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -32,6 +35,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
|
"@uiw/react-codemirror": "^4.25.4",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
@@ -2517,13 +2521,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/@types/hast": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/@types/json-schema": {
|
"apps/app/node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2555,13 +2552,6 @@
|
|||||||
"xmlbuilder": ">=11.0.1"
|
"xmlbuilder": ">=11.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/@types/react": {
|
|
||||||
"version": "19.2.7",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"csstype": "^3.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/@types/react-dom": {
|
"apps/app/node_modules/@types/react-dom": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
@@ -2570,10 +2560,6 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/@types/unist": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/@types/verror": {
|
"apps/app/node_modules/@types/verror": {
|
||||||
"version": "1.10.11",
|
"version": "1.10.11",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3737,14 +3723,6 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/character-entities": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/character-entities-html4": {
|
"apps/app/node_modules/character-entities-html4": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3753,22 +3731,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/character-entities-legacy": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/character-reference-invalid": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/chownr": {
|
"apps/app/node_modules/chownr": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3905,14 +3867,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/comma-separated-tokens": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/commander": {
|
"apps/app/node_modules/commander": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -4051,10 +4005,6 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/csstype": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/damerau-levenshtein": {
|
"apps/app/node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -4123,17 +4073,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/decode-named-character-reference": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"character-entities": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/deep-is": {
|
"apps/app/node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5662,26 +5601,6 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/is-alphabetical": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/is-alphanumerical": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-alphabetical": "^2.0.0",
|
|
||||||
"is-decimal": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/is-array-buffer": {
|
"apps/app/node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5831,14 +5750,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/is-decimal": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/is-extglob": {
|
"apps/app/node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5890,14 +5801,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/is-hexadecimal": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/is-interactive": {
|
"apps/app/node_modules/is-interactive": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7544,27 +7447,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/parse-entities": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^2.0.0",
|
|
||||||
"character-entities-legacy": "^3.0.0",
|
|
||||||
"character-reference-invalid": "^2.0.0",
|
|
||||||
"decode-named-character-reference": "^1.0.0",
|
|
||||||
"is-alphanumerical": "^2.0.0",
|
|
||||||
"is-decimal": "^2.0.0",
|
|
||||||
"is-hexadecimal": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/parse-entities/node_modules/@types/unist": {
|
|
||||||
"version": "2.0.11",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/path-exists": {
|
"apps/app/node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7783,14 +7665,6 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/property-information": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/proxy-from-env": {
|
"apps/app/node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8345,14 +8219,6 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/space-separated-tokens": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/ssri": {
|
"apps/app/node_modules/ssri": {
|
||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9861,6 +9727,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.28.5",
|
"version": "7.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||||
@@ -9885,6 +9760,113 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
|
||||||
|
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
|
||||||
|
"integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.4.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-xml": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.4.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/xml": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||||
|
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.1.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
|
||||||
|
"integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.35.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/search": {
|
||||||
|
"version": "6.5.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
|
||||||
|
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/theme-one-dark": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.39.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
|
||||||
|
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.5.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/get": {
|
"node_modules/@electron/get": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||||
@@ -10888,6 +10870,47 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/xml": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.0.10",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||||
@@ -11655,6 +11678,15 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hast": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/unist": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
||||||
@@ -11703,6 +11735,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/responselike": {
|
"node_modules/@types/responselike": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||||
@@ -11734,6 +11775,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/unist": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -11755,6 +11802,59 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||||
|
"version": "4.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.4.tgz",
|
||||||
|
"integrity": "sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/autocomplete": ">=6.0.0",
|
||||||
|
"@codemirror/commands": ">=6.0.0",
|
||||||
|
"@codemirror/language": ">=6.0.0",
|
||||||
|
"@codemirror/lint": ">=6.0.0",
|
||||||
|
"@codemirror/search": ">=6.0.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uiw/react-codemirror": {
|
||||||
|
"version": "4.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.4.tgz",
|
||||||
|
"integrity": "sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.6",
|
||||||
|
"@codemirror/commands": "^6.1.0",
|
||||||
|
"@codemirror/state": "^6.1.1",
|
||||||
|
"@codemirror/theme-one-dark": "^6.0.0",
|
||||||
|
"@uiw/codemirror-extensions-basic-setup": "4.25.4",
|
||||||
|
"codemirror": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/runtime": ">=7.11.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0",
|
||||||
|
"codemirror": ">=6.0.0",
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "4.0.15",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz",
|
||||||
@@ -12201,6 +12301,36 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/character-entities": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/character-entities-legacy": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/character-reference-invalid": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
@@ -12220,6 +12350,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -12240,6 +12385,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/comma-separated-tokens": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -12287,6 +12442,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -12302,6 +12463,25 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/decode-named-character-reference": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"character-entities": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -13225,6 +13405,40 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-alphabetical": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-alphanumerical": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-alphabetical": "^2.0.0",
|
||||||
|
"is-decimal": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-decimal": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -13235,6 +13449,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-hexadecimal": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-promise": {
|
"node_modules/is-promise": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
@@ -13864,6 +14088,31 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-entities": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/unist": "^2.0.0",
|
||||||
|
"character-entities-legacy": "^3.0.0",
|
||||||
|
"character-reference-invalid": "^2.0.0",
|
||||||
|
"decode-named-character-reference": "^1.0.0",
|
||||||
|
"is-alphanumerical": "^2.0.0",
|
||||||
|
"is-decimal": "^2.0.0",
|
||||||
|
"is-hexadecimal": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse-entities/node_modules/@types/unist": {
|
||||||
|
"version": "2.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||||
|
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -13954,6 +14203,16 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/property-information": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -14556,6 +14815,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/space-separated-tokens": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
@@ -14587,6 +14856,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/styled-jsx": {
|
"node_modules/styled-jsx": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||||
@@ -15505,6 +15780,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
2
test/fixtures/projectA/.gitkeep
vendored
Normal file
2
test/fixtures/projectA/.gitkeep
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# This file ensures the test fixture directory is tracked by git.
|
||||||
|
# The .automaker directory is created at test runtime.
|
||||||
Reference in New Issue
Block a user