mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
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,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Home,
|
Home,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
|
CornerDownLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -18,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;
|
||||||
@@ -48,16 +50,18 @@ 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",
|
||||||
initialPath,
|
initialPath,
|
||||||
}: 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 [warning, setWarning] = useState("");
|
const [warning, setWarning] = useState("");
|
||||||
|
const pathInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const browseDirectory = async (dirPath?: string) => {
|
const browseDirectory = async (dirPath?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -79,6 +83,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 || []);
|
||||||
@@ -99,6 +104,7 @@ export function FileBrowserDialog({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setCurrentPath("");
|
setCurrentPath("");
|
||||||
|
setPathInput("");
|
||||||
setParentPath(null);
|
setParentPath(null);
|
||||||
setDirectories([]);
|
setDirectories([]);
|
||||||
setError("");
|
setError("");
|
||||||
@@ -131,6 +137,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);
|
||||||
@@ -152,6 +172,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">
|
||||||
@@ -251,8 +296,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
|
Paste a full path above, or click on folders to navigate. Press
|
||||||
to a subfolder.
|
Enter or click Go to jump to a path.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,16 @@ export function Sidebar() {
|
|||||||
setSpecCreatingForProject,
|
setSpecCreatingForProject,
|
||||||
} = 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();
|
||||||
|
|
||||||
@@ -949,54 +959,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(() => {
|
||||||
@@ -1627,108 +1658,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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1533,7 +1533,7 @@ export function BoardView() {
|
|||||||
? features.filter(
|
? features.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
f.description.toLowerCase().includes(normalizedQuery) ||
|
f.description.toLowerCase().includes(normalizedQuery) ||
|
||||||
f.category.toLowerCase().includes(normalizedQuery)
|
f.category?.toLowerCase().includes(normalizedQuery)
|
||||||
)
|
)
|
||||||
: features;
|
: features;
|
||||||
|
|
||||||
|
|||||||
@@ -1004,7 +1004,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}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
247
package-lock.json
generated
247
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",
|
||||||
@@ -9863,6 +9867,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",
|
||||||
@@ -9887,6 +9900,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",
|
||||||
@@ -10890,6 +11010,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",
|
||||||
@@ -11767,6 +11928,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",
|
||||||
@@ -12250,6 +12464,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",
|
||||||
@@ -12317,6 +12546,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",
|
||||||
@@ -14669,6 +14904,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",
|
||||||
@@ -15587,6 +15828,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