Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a1af3314 | ||
|
|
2c4a977b4a | ||
|
|
28cc1400e9 | ||
|
|
193627e166 | ||
|
|
72560cbd36 | ||
|
|
8b62bf9817 | ||
|
|
95913714cc | ||
|
|
9f0c648391 | ||
|
|
0b3ffb642a | ||
|
|
7282113a0f | ||
|
|
ada2703d3e | ||
|
|
2cc9e47747 | ||
|
|
e6857c6feb | ||
|
|
b5d31db57c | ||
|
|
1bb20e5070 | ||
|
|
7cb5a6a4df | ||
|
|
e1e84226c9 | ||
|
|
7ff97a42cb | ||
|
|
502ac9e100 | ||
|
|
e6e2fa70e2 | ||
|
|
067f051ff7 | ||
|
|
13de3131b7 | ||
|
|
c8a9ae64b6 | ||
|
|
7383babd68 | ||
|
|
9dc5d64a26 | ||
|
|
b39a88ba15 | ||
|
|
0510ab31e3 | ||
|
|
e6a6b4cd78 | ||
|
|
1e6412e90e | ||
|
|
7419288189 | ||
|
|
f757270198 | ||
|
|
ca57b9e3ca | ||
|
|
6352a1df19 | ||
|
|
f460e689f1 | ||
|
|
74efaadb59 | ||
|
|
a602b1b519 | ||
|
|
4b1f4576ae | ||
|
|
60065806f4 | ||
|
|
9381162a8e | ||
|
|
626219fa62 | ||
|
|
8d6f825c5c | ||
|
|
c198c10244 | ||
|
|
acae5526b7 | ||
|
|
d6ce71702e |
8
.github/workflows/pr-check.yml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: app/package-lock.json
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
run: npm ci
|
||||
# Use npm install instead of npm ci to correctly resolve platform-specific
|
||||
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
||||
run: npm install
|
||||
|
||||
- name: Run build:electron
|
||||
working-directory: ./app
|
||||
run: npm run build:electron
|
||||
|
||||
34
.github/workflows/release.yml
vendored
@@ -3,13 +3,13 @@ name: Build and Release Electron App
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*' # Triggers on version tags like v1.0.0
|
||||
workflow_dispatch: # Allows manual triggering
|
||||
- "v*.*.*" # Triggers on version tags like v1.0.0
|
||||
workflow_dispatch: # Allows manual triggering
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., v1.0.0)'
|
||||
description: "Version to release (e.g., v1.0.0)"
|
||||
required: true
|
||||
default: 'v0.1.0'
|
||||
default: "v0.1.0"
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
@@ -36,31 +36,29 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: app/package-lock.json
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
run: npm ci
|
||||
# Use npm install instead of npm ci to correctly resolve platform-specific
|
||||
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
||||
run: npm install
|
||||
|
||||
- name: Build Electron App (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
working-directory: ./app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run build:electron -- --mac --x64 --arm64
|
||||
|
||||
- name: Build Electron App (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
working-directory: ./app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run build:electron -- --win --x64
|
||||
|
||||
- name: Build Electron App (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: ./app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run build:electron -- --linux --x64
|
||||
@@ -70,12 +68,12 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
||||
files: |
|
||||
app/dist/*.exe
|
||||
app/dist/*.dmg
|
||||
app/dist/*.AppImage
|
||||
app/dist/*.zip
|
||||
app/dist/*.deb
|
||||
app/dist/*.rpm
|
||||
apps/app/dist/*.exe
|
||||
apps/app/dist/*.dmg
|
||||
apps/app/dist/*.AppImage
|
||||
apps/app/dist/*.zip
|
||||
apps/app/dist/*.deb
|
||||
apps/app/dist/*.rpm
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
|
||||
9
.gitignore
vendored
@@ -1,2 +1,9 @@
|
||||
#added by trueheads > will remove once supercombo adds multi-os support
|
||||
#added by trueheads > will remove once supercombo adds multi-os support
|
||||
launch.sh
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
.next/
|
||||
|
||||
10
.npmrc
Normal file
@@ -0,0 +1,10 @@
|
||||
# Cross-platform compatibility for Tailwind CSS v4 and lightningcss
|
||||
# These packages use platform-specific optional dependencies that npm
|
||||
# automatically resolves based on your OS (macOS, Linux, Windows, WSL)
|
||||
#
|
||||
# IMPORTANT: When switching platforms or getting platform mismatch errors:
|
||||
# 1. Delete node_modules: rm -rf node_modules apps/*/node_modules
|
||||
# 2. Run: npm install
|
||||
#
|
||||
# In CI/CD: Use "npm install" instead of "npm ci" to allow npm to resolve
|
||||
# the correct platform-specific binaries at install time.
|
||||
@@ -1,76 +0,0 @@
|
||||
const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk");
|
||||
const { z } = require("zod");
|
||||
const featureLoader = require("./feature-loader");
|
||||
|
||||
/**
|
||||
* MCP Server Factory - Creates custom MCP servers with tools
|
||||
*/
|
||||
class McpServerFactory {
|
||||
/**
|
||||
* Create a custom MCP server with the UpdateFeatureStatus tool
|
||||
* This tool allows Claude Code to safely update feature status without
|
||||
* directly modifying feature files, preventing race conditions
|
||||
* and accidental state corruption.
|
||||
*/
|
||||
createFeatureToolsServer(updateFeatureStatusCallback, projectPath) {
|
||||
return createSdkMcpServer({
|
||||
name: "automaker-tools",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"UpdateFeatureStatus",
|
||||
"Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.",
|
||||
{
|
||||
featureId: z.string().describe("The ID of the feature to update"),
|
||||
status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."),
|
||||
summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'")
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`);
|
||||
|
||||
// Load the feature to check skipTests flag
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
const feature = features.find((f) => f.id === args.featureId);
|
||||
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${args.featureId} not found`);
|
||||
}
|
||||
|
||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
||||
let finalStatus = args.status;
|
||||
if (args.status === "verified" && feature.skipTests === true) {
|
||||
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
|
||||
finalStatus = "waiting_approval";
|
||||
}
|
||||
|
||||
// Call the provided callback to update feature status with summary
|
||||
await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary);
|
||||
|
||||
const statusMessage = finalStatus !== args.status
|
||||
? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}`
|
||||
: `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: statusMessage
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to update feature status: ${error.message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new McpServerFactory();
|
||||
@@ -1,467 +0,0 @@
|
||||
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* XML template for app_spec.txt
|
||||
*/
|
||||
const APP_SPEC_XML_TEMPLATE = `<project_specification>
|
||||
<project_name></project_name>
|
||||
|
||||
<overview>
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<frontend>
|
||||
<framework></framework>
|
||||
<ui_library></ui_library>
|
||||
<styling></styling>
|
||||
<state_management></state_management>
|
||||
<drag_drop></drag_drop>
|
||||
<icons></icons>
|
||||
</frontend>
|
||||
<desktop_shell>
|
||||
<framework></framework>
|
||||
<language></language>
|
||||
<inter_process_communication></inter_process_communication>
|
||||
<file_system></file_system>
|
||||
</desktop_shell>
|
||||
<ai_engine>
|
||||
<logic_model></logic_model>
|
||||
<design_model></design_model>
|
||||
<orchestration></orchestration>
|
||||
</ai_engine>
|
||||
<testing>
|
||||
<framework></framework>
|
||||
<unit></unit>
|
||||
</testing>
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<project_management>
|
||||
</project_management>
|
||||
|
||||
<intelligent_analysis>
|
||||
</intelligent_analysis>
|
||||
|
||||
<kanban_workflow>
|
||||
</kanban_workflow>
|
||||
|
||||
<autonomous_agent_engine>
|
||||
</autonomous_agent_engine>
|
||||
|
||||
<extensibility>
|
||||
</extensibility>
|
||||
</core_capabilities>
|
||||
|
||||
<ui_layout>
|
||||
<window_structure>
|
||||
</window_structure>
|
||||
<theme>
|
||||
</theme>
|
||||
</ui_layout>
|
||||
|
||||
<development_workflow>
|
||||
<local_testing>
|
||||
</local_testing>
|
||||
</development_workflow>
|
||||
|
||||
<implementation_roadmap>
|
||||
<phase_1_foundation>
|
||||
</phase_1_foundation>
|
||||
<phase_2_core_logic>
|
||||
</phase_2_core_logic>
|
||||
<phase_3_kanban_and_interaction>
|
||||
</phase_3_kanban_and_interaction>
|
||||
<phase_4_polish>
|
||||
</phase_4_polish>
|
||||
</implementation_roadmap>
|
||||
</project_specification>`;
|
||||
|
||||
/**
|
||||
* Spec Regeneration Service - Regenerates app spec based on project description and tech stack
|
||||
*/
|
||||
class SpecRegenerationService {
|
||||
constructor() {
|
||||
this.runningRegeneration = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial app spec for a new project
|
||||
* @param {string} projectPath - Path to the project
|
||||
* @param {string} projectOverview - User's project description
|
||||
* @param {Function} sendToRenderer - Function to send events to renderer
|
||||
* @param {Object} execution - Execution context with abort controller
|
||||
* @param {boolean} generateFeatures - Whether to generate feature entries in features folder
|
||||
*/
|
||||
async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) {
|
||||
console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`);
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
execution.abortController = abortController;
|
||||
|
||||
const options = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures),
|
||||
maxTurns: 50,
|
||||
cwd: projectPath,
|
||||
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||
permissionMode: "acceptEdits",
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
autoAllowBashIfSandboxed: true,
|
||||
},
|
||||
abortController: abortController,
|
||||
};
|
||||
|
||||
const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures);
|
||||
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting project analysis and spec creation...\n",
|
||||
});
|
||||
|
||||
const currentQuery = query({ prompt, options });
|
||||
execution.query = currentQuery;
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const msg of currentQuery) {
|
||||
if (!execution.isActive()) break;
|
||||
|
||||
if (msg.type === "assistant" && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
fullResponse += block.text;
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execution.query = null;
|
||||
execution.abortController = null;
|
||||
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "Initial spec creation complete!",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Initial spec creation complete",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
||||
console.log("[SpecRegeneration] Creation aborted");
|
||||
if (execution) {
|
||||
execution.abortController = null;
|
||||
execution.query = null;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: "Creation aborted",
|
||||
};
|
||||
}
|
||||
|
||||
console.error("[SpecRegeneration] Error creating initial spec:", error);
|
||||
if (execution) {
|
||||
execution.abortController = null;
|
||||
execution.query = null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for initial spec creation
|
||||
* @param {boolean} generateFeatures - Whether features should be generated
|
||||
*/
|
||||
getInitialCreationSystemPrompt(generateFeatures = true) {
|
||||
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview.
|
||||
|
||||
You should:
|
||||
1. First, thoroughly analyze the project structure to understand the existing tech stack
|
||||
2. Read key configuration files (package.json, tsconfig.json, Cargo.toml, requirements.txt, etc.) to understand dependencies and frameworks
|
||||
3. Understand the current architecture and patterns used
|
||||
4. Based on the user's project overview, create a comprehensive app specification
|
||||
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
|
||||
6. Use the XML template format provided
|
||||
7. Write the specification to .automaker/app_spec.txt
|
||||
|
||||
When analyzing, look at:
|
||||
- package.json, cargo.toml, requirements.txt or similar config files for tech stack
|
||||
- Source code structure and organization
|
||||
- Framework-specific patterns (Next.js, React, Django, etc.)
|
||||
- Database configurations and schemas
|
||||
- API structures and patterns
|
||||
|
||||
**Feature Storage:**
|
||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
||||
Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features.
|
||||
|
||||
You CAN and SHOULD modify:
|
||||
- .automaker/app_spec.txt (this is your primary target)
|
||||
|
||||
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt for initial spec creation
|
||||
* @param {string} projectOverview - User's project description
|
||||
* @param {boolean} generateFeatures - Whether to generate feature entries in features folder
|
||||
*/
|
||||
buildInitialCreationPrompt(projectOverview, generateFeatures = true) {
|
||||
return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one.
|
||||
|
||||
**My Project Overview:**
|
||||
${projectOverview}
|
||||
|
||||
**Your Task:**
|
||||
|
||||
1. First, explore the project to understand the existing tech stack:
|
||||
- Read package.json, Cargo.toml, requirements.txt, or similar config files
|
||||
- Identify all frameworks and libraries being used
|
||||
- Understand the current project structure and architecture
|
||||
- Note any database, authentication, or other infrastructure in use
|
||||
|
||||
2. Based on my project overview and the existing tech stack, create a comprehensive app specification using this XML template:
|
||||
|
||||
\`\`\`xml
|
||||
${APP_SPEC_XML_TEMPLATE}
|
||||
\`\`\`
|
||||
|
||||
3. Fill out the template with:
|
||||
- **project_name**: Extract from the project or derive from overview
|
||||
- **overview**: A clear description based on my project overview
|
||||
- **technology_stack**: All technologies you discover in the project (fill out the relevant sections, remove irrelevant ones)
|
||||
- **core_capabilities**: List all the major capabilities the app should have based on my overview
|
||||
- **ui_layout**: Describe the UI structure if relevant
|
||||
- **development_workflow**: Note any testing or development patterns
|
||||
- **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built
|
||||
|
||||
4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
|
||||
|
||||
**Guidelines:**
|
||||
- Be comprehensive! Include ALL features needed for a complete application
|
||||
- Only include technology_stack sections that are relevant (e.g., skip desktop_shell if it's a web-only app)
|
||||
- Add new sections to core_capabilities as needed for the specific project
|
||||
- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually
|
||||
- Consider user flows, error states, and edge cases when defining features
|
||||
- Each phase should have multiple specific, actionable features
|
||||
|
||||
Begin by exploring the project structure.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the app spec based on user's project definition
|
||||
*/
|
||||
async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) {
|
||||
console.log(`[SpecRegeneration] Regenerating spec for: ${projectPath}`);
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
execution.abortController = abortController;
|
||||
|
||||
const options = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
systemPrompt: this.getSystemPrompt(),
|
||||
maxTurns: 50,
|
||||
cwd: projectPath,
|
||||
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||
permissionMode: "acceptEdits",
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
autoAllowBashIfSandboxed: true,
|
||||
},
|
||||
abortController: abortController,
|
||||
};
|
||||
|
||||
const prompt = this.buildRegenerationPrompt(projectDefinition);
|
||||
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting spec regeneration...\n",
|
||||
});
|
||||
|
||||
const currentQuery = query({ prompt, options });
|
||||
execution.query = currentQuery;
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const msg of currentQuery) {
|
||||
if (!execution.isActive()) break;
|
||||
|
||||
if (msg.type === "assistant" && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
fullResponse += block.text;
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execution.query = null;
|
||||
execution.abortController = null;
|
||||
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "Spec regeneration complete!",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Spec regeneration complete",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
||||
console.log("[SpecRegeneration] Regeneration aborted");
|
||||
if (execution) {
|
||||
execution.abortController = null;
|
||||
execution.query = null;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: "Regeneration aborted",
|
||||
};
|
||||
}
|
||||
|
||||
console.error("[SpecRegeneration] Error regenerating spec:", error);
|
||||
if (execution) {
|
||||
execution.abortController = null;
|
||||
execution.query = null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for spec regeneration
|
||||
*/
|
||||
getSystemPrompt() {
|
||||
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project definition.
|
||||
|
||||
You should:
|
||||
1. First, thoroughly analyze the project structure to understand the existing tech stack
|
||||
2. Read key configuration files (package.json, tsconfig.json, etc.) to understand dependencies and frameworks
|
||||
3. Understand the current architecture and patterns used
|
||||
4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision
|
||||
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
|
||||
6. Write the specification to .automaker/app_spec.txt
|
||||
|
||||
When analyzing, look at:
|
||||
- package.json, cargo.toml, or similar config files for tech stack
|
||||
- Source code structure and organization
|
||||
- Framework-specific patterns (Next.js, React, etc.)
|
||||
- Database configurations and schemas
|
||||
- API structures and patterns
|
||||
|
||||
**Feature Storage:**
|
||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
||||
Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features.
|
||||
|
||||
You CAN and SHOULD modify:
|
||||
- .automaker/app_spec.txt (this is your primary target)
|
||||
|
||||
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt for regenerating the spec
|
||||
*/
|
||||
buildRegenerationPrompt(projectDefinition) {
|
||||
return `I need you to regenerate my application specification based on the following project definition. Be very comprehensive and liberal when defining features - I want a complete, polished application.
|
||||
|
||||
**My Project Definition:**
|
||||
${projectDefinition}
|
||||
|
||||
**Your Task:**
|
||||
|
||||
1. First, explore the project to understand the existing tech stack:
|
||||
- Read package.json or similar config files
|
||||
- Identify all frameworks and libraries being used
|
||||
- Understand the current project structure and architecture
|
||||
- Note any database, authentication, or other infrastructure in use
|
||||
|
||||
2. Based on my project definition and the existing tech stack, create a comprehensive app specification that includes:
|
||||
- Product Overview: A clear description of what the app does
|
||||
- Tech Stack: All technologies currently in use
|
||||
- Features: A COMPREHENSIVE list of all features needed to realize my vision
|
||||
- Be liberal! Include all features that would make this a complete, production-ready application
|
||||
- Include core features, supporting features, and nice-to-have features
|
||||
- Think about user experience, error handling, edge cases, etc.
|
||||
- Architecture Notes: Any important architectural decisions or patterns
|
||||
|
||||
3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
|
||||
|
||||
**Format Guidelines for the Spec:**
|
||||
|
||||
Use this general structure:
|
||||
|
||||
\`\`\`
|
||||
# [App Name] - Application Specification
|
||||
|
||||
## Product Overview
|
||||
[Description of what the app does and its purpose]
|
||||
|
||||
## Tech Stack
|
||||
- Frontend: [frameworks, libraries]
|
||||
- Backend: [frameworks, APIs]
|
||||
- Database: [if applicable]
|
||||
- Other: [other relevant tech]
|
||||
|
||||
## Features
|
||||
|
||||
### [Category 1]
|
||||
- **[Feature Name]**: [Detailed description of the feature]
|
||||
- **[Feature Name]**: [Detailed description]
|
||||
...
|
||||
|
||||
### [Category 2]
|
||||
- **[Feature Name]**: [Detailed description]
|
||||
...
|
||||
|
||||
## Architecture Notes
|
||||
[Any important architectural notes, patterns, or conventions]
|
||||
\`\`\`
|
||||
|
||||
**Remember:**
|
||||
- Be comprehensive! Include ALL features needed for a complete application
|
||||
- Consider user flows, error states, loading states, etc.
|
||||
- Include authentication, authorization if relevant
|
||||
- Think about what would make this a polished, production-ready app
|
||||
- The more detailed and complete the spec, the better
|
||||
|
||||
Begin by exploring the project structure.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current regeneration
|
||||
*/
|
||||
stop() {
|
||||
if (this.runningRegeneration && this.runningRegeneration.abortController) {
|
||||
this.runningRegeneration.abortController.abort();
|
||||
}
|
||||
this.runningRegeneration = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SpecRegenerationService();
|
||||
13896
app/package-lock.json
generated
@@ -1,96 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react";
|
||||
import type { KanbanDetailLevel } from "../shared/types";
|
||||
|
||||
interface KanbanDisplaySectionProps {
|
||||
detailLevel: KanbanDetailLevel;
|
||||
onChange: (level: KanbanDetailLevel) => void;
|
||||
}
|
||||
|
||||
export function KanbanDisplaySection({
|
||||
detailLevel,
|
||||
onChange,
|
||||
}: KanbanDisplaySectionProps) {
|
||||
return (
|
||||
<div
|
||||
id="kanban"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LayoutGrid className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Kanban Card Display
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Control how much information is displayed on Kanban cards.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground">Detail Level</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Button
|
||||
variant={detailLevel === "minimal" ? "secondary" : "outline"}
|
||||
onClick={() => onChange("minimal")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
detailLevel === "minimal"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-minimal"
|
||||
>
|
||||
<Minimize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Minimal</span>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Title & category only
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={detailLevel === "standard" ? "secondary" : "outline"}
|
||||
onClick={() => onChange("standard")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
detailLevel === "standard"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-standard"
|
||||
>
|
||||
<Square className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Standard</span>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Steps & progress
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={detailLevel === "detailed" ? "secondary" : "outline"}
|
||||
onClick={() => onChange("detailed")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
detailLevel === "detailed"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-detailed"
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Detailed</span>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Model, tools & tasks
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Minimal:</strong> Shows only title and category
|
||||
<br />
|
||||
<strong>Standard:</strong> Adds steps preview and progress bar
|
||||
<br />
|
||||
<strong>Detailed:</strong> Shows all info including model, tool
|
||||
calls, task list, and summaries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,437 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
|
||||
export function SpecView() {
|
||||
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [specExists, setSpecExists] = useState(true);
|
||||
|
||||
// Regeneration state
|
||||
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
||||
const [projectDefinition, setProjectDefinition] = useState("");
|
||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||
|
||||
// Create spec state
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [projectOverview, setProjectOverview] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
|
||||
// Load spec from file
|
||||
const loadSpec = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`
|
||||
);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setAppSpec(result.content);
|
||||
setSpecExists(true);
|
||||
setHasChanges(false);
|
||||
} else {
|
||||
// File doesn't exist
|
||||
setAppSpec("");
|
||||
setSpecExists(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load spec:", error);
|
||||
setSpecExists(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentProject, setAppSpec]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSpec();
|
||||
}, [loadSpec]);
|
||||
|
||||
// Subscribe to spec regeneration events
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
|
||||
console.log("[SpecView] Regeneration event:", event.type);
|
||||
|
||||
if (event.type === "spec_regeneration_complete") {
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
setShowRegenerateDialog(false);
|
||||
setShowCreateDialog(false);
|
||||
setProjectDefinition("");
|
||||
setProjectOverview("");
|
||||
// Reload the spec to show the new content
|
||||
loadSpec();
|
||||
} else if (event.type === "spec_regeneration_error") {
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
console.error("[SpecView] Regeneration error:", event.error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [loadSpec]);
|
||||
|
||||
// Save spec to file
|
||||
const saveSpec = async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
await api.writeFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`,
|
||||
appSpec
|
||||
);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save spec:", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setAppSpec(value);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
if (!currentProject || !projectDefinition.trim()) return;
|
||||
|
||||
setIsRegenerating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
console.error("[SpecView] Spec regeneration not available");
|
||||
setIsRegenerating(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.generate(
|
||||
currentProject.path,
|
||||
projectDefinition.trim()
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("[SpecView] Failed to start regeneration:", result.error);
|
||||
setIsRegenerating(false);
|
||||
}
|
||||
// If successful, we'll wait for the events to update the state
|
||||
} catch (error) {
|
||||
console.error("[SpecView] Failed to regenerate spec:", error);
|
||||
setIsRegenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSpec = async () => {
|
||||
if (!currentProject || !projectOverview.trim()) return;
|
||||
|
||||
setIsCreating(true);
|
||||
setShowCreateDialog(false);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
console.error("[SpecView] Spec regeneration not available");
|
||||
setIsCreating(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.create(
|
||||
currentProject.path,
|
||||
projectOverview.trim(),
|
||||
generateFeatures
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("[SpecView] Failed to start spec creation:", result.error);
|
||||
setIsCreating(false);
|
||||
}
|
||||
// If successful, we'll wait for the events to update the state
|
||||
} catch (error) {
|
||||
console.error("[SpecView] Failed to create spec:", error);
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="spec-view-no-project"
|
||||
>
|
||||
<p className="text-muted-foreground">No project selected</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="spec-view-loading"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar)
|
||||
if (!specExists) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="spec-view-empty"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">App Specification</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.path}/.automaker/app_spec.txt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="p-4 rounded-full bg-primary/10">
|
||||
<FilePlus2 className="w-12 h-12 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mb-3">No App Specification Found</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Create an app specification to help our system understand your project.
|
||||
We'll analyze your codebase and generate a comprehensive spec based on your description.
|
||||
</p>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
<FilePlus2 className="w-5 h-5 mr-2" />
|
||||
Create app_spec
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create App Specification</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt
|
||||
to help describe your project for our system. We'll analyze your project's
|
||||
tech stack and create a comprehensive specification.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Project Overview
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Describe what your project does and what features you want to build.
|
||||
Be as detailed as you want - this will help us create a better specification.
|
||||
</p>
|
||||
<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"
|
||||
value={projectOverview}
|
||||
onChange={(e) => setProjectOverview(e.target.value)}
|
||||
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 pt-2">
|
||||
<Checkbox
|
||||
id="generate-features"
|
||||
checked={generateFeatures}
|
||||
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="generate-features"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Generate feature list
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Automatically create features in the features folder from the
|
||||
implementation roadmap after the spec is generated.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setShowCreateDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleCreateSpec}
|
||||
disabled={!projectOverview.trim()}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={showCreateDialog}
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Generate Spec
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="spec-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">App Specification</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.path}/.automaker/app_spec.txt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowRegenerateDialog(true)}
|
||||
disabled={isRegenerating}
|
||||
data-testid="regenerate-spec"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isRegenerating ? "Regenerating..." : "Regenerate"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveSpec}
|
||||
disabled={!hasChanges || isSaving}
|
||||
data-testid="save-spec"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<Card className="h-full overflow-hidden">
|
||||
<XmlSyntaxEditor
|
||||
value={appSpec}
|
||||
onChange={handleChange}
|
||||
placeholder="Write your app specification here..."
|
||||
data-testid="spec-editor"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Regenerate Dialog */}
|
||||
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Regenerate App Specification</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
We will regenerate your app spec based on a short project definition and the
|
||||
current tech stack found in your project. The agent will analyze your codebase
|
||||
to understand your existing technologies and create a comprehensive specification.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Describe your project
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Provide a clear description of what your app should do. Be as detailed as you
|
||||
want - the more context you provide, the more comprehensive the spec will be.
|
||||
</p>
|
||||
<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"
|
||||
value={projectDefinition}
|
||||
onChange={(e) => setProjectDefinition(e.target.value)}
|
||||
placeholder="e.g., A task management app where users can create projects, add tasks with due dates, assign tasks to team members, track progress with a kanban board, and receive notifications for upcoming deadlines..."
|
||||
disabled={isRegenerating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setShowRegenerateDialog(false)}
|
||||
disabled={isRegenerating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleRegenerate}
|
||||
disabled={!projectDefinition.trim() || isRegenerating}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={showRegenerateDialog}
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Regenerating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Regenerate Spec
|
||||
</>
|
||||
)}
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/.gitignore → apps/app/.gitignore
vendored
@@ -33,6 +33,13 @@ cd automaker
|
||||
npm install
|
||||
```
|
||||
|
||||
### Windows notes (in-app Claude auth)
|
||||
|
||||
- Node.js 22.x
|
||||
- Prebuilt PTY is bundled; Visual Studio build tools are not required for Claude auth.
|
||||
- If you prefer the external terminal flow, set `CLAUDE_AUTH_DISABLE_PTY=1`.
|
||||
- If you later add native modules beyond the prebuilt PTY, you may still need VS Build Tools + Python to rebuild those.
|
||||
|
||||
**Step 3:** Run the Claude Code setup token command:
|
||||
|
||||
```bash
|
||||
@@ -55,7 +62,15 @@ npm run dev:electron
|
||||
|
||||
This will start both the Next.js development server and the Electron application.
|
||||
|
||||
**Step 6:** MOST IMPORANT: Run the Following after all is setup
|
||||
### Auth smoke test (Windows)
|
||||
|
||||
1. Ensure dependencies are installed (prebuilt pty is included).
|
||||
2. Run `npm run dev:electron` and open the Setup modal.
|
||||
3. Click Start on Claude auth; watch the embedded terminal stream logs.
|
||||
4. Successful runs show “Token captured automatically.”; otherwise copy/paste the token from the log.
|
||||
5. Optional: `node --test tests/claude-cli-detector.test.js` to verify token parsing.
|
||||
|
||||
**Step 6:** MOST IMPORTANT: Run the Following after all is setup
|
||||
|
||||
```bash
|
||||
echo "W"
|
||||
@@ -391,8 +391,7 @@ class AutoModeService {
|
||||
featureId,
|
||||
"waiting_approval",
|
||||
projectPath,
|
||||
null, // no summary
|
||||
error.message // pass error message
|
||||
{ error: error.message }
|
||||
);
|
||||
} catch (statusError) {
|
||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||
@@ -495,8 +494,7 @@ class AutoModeService {
|
||||
featureId,
|
||||
"waiting_approval",
|
||||
projectPath,
|
||||
null, // no summary
|
||||
error.message // pass error message
|
||||
{ error: error.message }
|
||||
);
|
||||
} catch (statusError) {
|
||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||
@@ -662,8 +660,7 @@ class AutoModeService {
|
||||
featureId,
|
||||
"waiting_approval",
|
||||
projectPath,
|
||||
null, // no summary
|
||||
error.message // pass error message
|
||||
{ error: error.message }
|
||||
);
|
||||
} catch (statusError) {
|
||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||
@@ -859,8 +856,7 @@ class AutoModeService {
|
||||
featureId,
|
||||
"waiting_approval",
|
||||
projectPath,
|
||||
null, // no summary
|
||||
error.message // pass error message
|
||||
{ error: error.message }
|
||||
);
|
||||
} catch (statusError) {
|
||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||
@@ -1102,8 +1098,7 @@ class AutoModeService {
|
||||
featureId,
|
||||
"waiting_approval",
|
||||
projectPath,
|
||||
null, // no summary
|
||||
error.message // pass error message
|
||||
{ error: error.message }
|
||||
);
|
||||
} catch (statusError) {
|
||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||
@@ -42,7 +42,10 @@ function createWindow() {
|
||||
const isDev = !app.isPackaged;
|
||||
if (isDev) {
|
||||
mainWindow.loadURL("http://localhost:3007");
|
||||
// mainWindow.webContents.openDevTools();
|
||||
// Open DevTools if OPEN_DEVTOOLS environment variable is set
|
||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
||||
}
|
||||
@@ -894,12 +897,9 @@ ipcMain.handle(
|
||||
async (_, { featureId, status, projectPath, summary }) => {
|
||||
try {
|
||||
const featureLoader = require("./services/feature-loader");
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
status,
|
||||
projectPath,
|
||||
summary
|
||||
);
|
||||
await featureLoader.updateFeatureStatus(featureId, status, projectPath, {
|
||||
summary,
|
||||
});
|
||||
|
||||
// Notify renderer if window is available
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
@@ -931,51 +931,59 @@ let suggestionsExecution = null;
|
||||
* @param {string} projectPath - The path to the project
|
||||
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
||||
*/
|
||||
ipcMain.handle("suggestions:generate", async (_, { projectPath, suggestionType = "features" }) => {
|
||||
try {
|
||||
// Check if already running
|
||||
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Suggestions generation is already running",
|
||||
};
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
suggestionsExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => suggestionsExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("suggestions:event", data);
|
||||
ipcMain.handle(
|
||||
"suggestions:generate",
|
||||
async (_, { projectPath, suggestionType = "features" }) => {
|
||||
try {
|
||||
// Check if already running
|
||||
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Suggestions generation is already running",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating suggestions (runs in background)
|
||||
featureSuggestionsService
|
||||
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution, suggestionType)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] suggestions:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
// Create execution context
|
||||
suggestionsExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => suggestionsExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("suggestions:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating suggestions (runs in background)
|
||||
featureSuggestionsService
|
||||
.generateSuggestions(
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
suggestionsExecution,
|
||||
suggestionType
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] suggestions:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
suggestionsExecution = null;
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
suggestionsExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:generate error:", error);
|
||||
suggestionsExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:generate error:", error);
|
||||
suggestionsExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Stop the current suggestions generation
|
||||
@@ -1170,6 +1178,7 @@ ipcMain.handle("spec-regeneration:status", () => {
|
||||
isRunning:
|
||||
specRegenerationExecution !== null &&
|
||||
specRegenerationExecution.isActive(),
|
||||
currentPhase: specRegenerationService.getCurrentPhase(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1234,6 +1243,69 @@ ipcMain.handle(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Generate features from existing app_spec.txt
|
||||
* This allows users to generate features retroactively without regenerating the spec
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"spec-regeneration:generate-features",
|
||||
async (_, { projectPath }) => {
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
// Check if already running
|
||||
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Spec regeneration is already running",
|
||||
};
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
specRegenerationExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => specRegenerationExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("spec-regeneration:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating features (runs in background)
|
||||
specRegenerationService
|
||||
.generateFeaturesOnly(
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
specRegenerationExecution
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[IPC] spec-regeneration:generate-features background error:",
|
||||
error
|
||||
);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
specRegenerationExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] spec-regeneration:generate-features error:", error);
|
||||
specRegenerationExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Merge feature worktree changes back to main branch
|
||||
*/
|
||||
@@ -1731,7 +1803,10 @@ ipcMain.handle(
|
||||
}
|
||||
|
||||
const featureLoader = require("./services/feature-loader");
|
||||
const content = await featureLoader.getAgentOutput(projectPath, featureId);
|
||||
const content = await featureLoader.getAgentOutput(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
return { success: true, content };
|
||||
} catch (error) {
|
||||
console.error("[IPC] features:getAgentOutput error:", error);
|
||||
@@ -285,6 +285,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
projectDefinition,
|
||||
}),
|
||||
|
||||
// Generate features from existing app_spec.txt
|
||||
generateFeatures: (projectPath) =>
|
||||
ipcRenderer.invoke("spec-regeneration:generate-features", {
|
||||
projectPath,
|
||||
}),
|
||||
|
||||
// Stop regenerating spec
|
||||
stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
|
||||
|
||||
@@ -3,6 +3,22 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
let runPtyCommand = null;
|
||||
try {
|
||||
({ runPtyCommand } = require("./pty-runner"));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:",
|
||||
error?.message || error
|
||||
);
|
||||
}
|
||||
|
||||
const ANSI_REGEX =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u001b\][^\u0007]*\u0007/g;
|
||||
|
||||
const stripAnsi = (text = "") => text.replace(ANSI_REGEX, "");
|
||||
|
||||
/**
|
||||
* Claude CLI Detector
|
||||
*
|
||||
@@ -459,6 +475,247 @@ class ClaudeCliDetector {
|
||||
note: "This token is from your Claude subscription and allows you to use Claude without API charges.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract OAuth token from command output
|
||||
* Tries multiple patterns to find the token
|
||||
* @param {string} output The command output
|
||||
* @returns {string|null} Extracted token or null
|
||||
*/
|
||||
static extractTokenFromOutput(output) {
|
||||
// Pattern 1: CLAUDE_CODE_OAUTH_TOKEN=<token> or CLAUDE_CODE_OAUTH_TOKEN: <token>
|
||||
const envMatch = output.match(
|
||||
/CLAUDE_CODE_OAUTH_TOKEN[=:]\s*["']?([a-zA-Z0-9_\-\.]+)["']?/i
|
||||
);
|
||||
if (envMatch) return envMatch[1];
|
||||
|
||||
// Pattern 2: "Token: <token>" or "token: <token>"
|
||||
const tokenLabelMatch = output.match(
|
||||
/\btoken[:\s]+["']?([a-zA-Z0-9_\-\.]{40,})["']?/i
|
||||
);
|
||||
if (tokenLabelMatch) return tokenLabelMatch[1];
|
||||
|
||||
// Pattern 3: Look for token after success/authenticated message
|
||||
const successMatch = output.match(
|
||||
/(?:success|authenticated|generated|token is)[^\n]*\n\s*([a-zA-Z0-9_\-\.]{40,})/i
|
||||
);
|
||||
if (successMatch) return successMatch[1];
|
||||
|
||||
// Pattern 4: Standalone long alphanumeric string on its own line (last resort)
|
||||
// This catches tokens that are printed on their own line
|
||||
const lines = output.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots
|
||||
if (/^[a-zA-Z0-9_\-\.]{40,}$/.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run claude setup-token command to generate OAuth token
|
||||
* Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI
|
||||
* @param {Function} onProgress Callback for progress updates
|
||||
* @returns {Promise<Object>} Result indicating terminal was opened
|
||||
*/
|
||||
static async runSetupToken(onProgress) {
|
||||
const detection = this.detectClaudeInstallation();
|
||||
|
||||
if (!detection.installed) {
|
||||
throw {
|
||||
success: false,
|
||||
error: "Claude CLI is not installed. Please install it first.",
|
||||
requiresManualAuth: false,
|
||||
};
|
||||
}
|
||||
|
||||
const claudePath = detection.path;
|
||||
const platform = process.platform;
|
||||
const preferPty =
|
||||
(platform === "win32" ||
|
||||
platform === "darwin" ||
|
||||
process.env.CLAUDE_AUTH_FORCE_PTY === "1") &&
|
||||
process.env.CLAUDE_AUTH_DISABLE_PTY !== "1";
|
||||
|
||||
const send = (data) => {
|
||||
if (onProgress && data) {
|
||||
onProgress({ type: "stdout", data });
|
||||
}
|
||||
};
|
||||
|
||||
if (preferPty && runPtyCommand) {
|
||||
try {
|
||||
send("Starting in-app terminal session for Claude auth...\n");
|
||||
send("If your browser opens, complete sign-in and return here.\n\n");
|
||||
|
||||
const ptyResult = await runPtyCommand(claudePath, ["setup-token"], {
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
onData: (chunk) => send(chunk),
|
||||
env: {
|
||||
FORCE_COLOR: "1",
|
||||
},
|
||||
});
|
||||
|
||||
const cleanedOutput = stripAnsi(ptyResult.output || "");
|
||||
const token = this.extractTokenFromOutput(cleanedOutput);
|
||||
|
||||
if (ptyResult.success && token) {
|
||||
send("\nCaptured token automatically.\n");
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
requiresManualAuth: false,
|
||||
terminalOpened: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (ptyResult.success && !token) {
|
||||
send(
|
||||
"\nCLI completed but token was not detected automatically. You can copy it above or retry.\n"
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
terminalOpened: false,
|
||||
error: "Could not capture token automatically",
|
||||
output: cleanedOutput,
|
||||
};
|
||||
}
|
||||
|
||||
send(
|
||||
`\nClaude CLI exited with code ${ptyResult.exitCode}. Falling back to manual copy.\n`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: `Claude CLI exited with code ${ptyResult.exitCode}`,
|
||||
requiresManualAuth: true,
|
||||
output: cleanedOutput,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[ClaudeCliDetector] PTY auth failed, falling back:", error);
|
||||
send(
|
||||
`In-app terminal failed (${error?.message || "unknown error"}). Falling back to external terminal...\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: external terminal window
|
||||
if (preferPty && !runPtyCommand) {
|
||||
send("In-app terminal unavailable (node-pty not loaded).");
|
||||
} else if (!preferPty) {
|
||||
send("Using system terminal for authentication on this platform.");
|
||||
}
|
||||
send("Opening system terminal for authentication...\n");
|
||||
|
||||
// Helper function to check if a command exists asynchronously
|
||||
const commandExists = (cmd) => {
|
||||
return new Promise((resolve) => {
|
||||
require("child_process").exec(
|
||||
`which ${cmd}`,
|
||||
{ timeout: 1000 },
|
||||
(error) => {
|
||||
resolve(!error);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// For Linux, find available terminal first (async)
|
||||
let linuxTerminal = null;
|
||||
if (platform !== "win32" && platform !== "darwin") {
|
||||
const terminals = [
|
||||
["gnome-terminal", ["--", claudePath, "setup-token"]],
|
||||
["konsole", ["-e", claudePath, "setup-token"]],
|
||||
["xterm", ["-e", claudePath, "setup-token"]],
|
||||
["x-terminal-emulator", ["-e", `${claudePath} setup-token`]],
|
||||
];
|
||||
|
||||
for (const [term, termArgs] of terminals) {
|
||||
const exists = await commandExists(term);
|
||||
if (exists) {
|
||||
linuxTerminal = { command: term, args: termArgs };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Open command in external terminal since Claude CLI requires TTY
|
||||
let command, args;
|
||||
|
||||
if (platform === "win32") {
|
||||
// Windows: Open new cmd window that stays open
|
||||
command = "cmd";
|
||||
args = ["/c", "start", "cmd", "/k", `"${claudePath}" setup-token`];
|
||||
} else if (platform === "darwin") {
|
||||
// macOS: Open Terminal.app
|
||||
command = "osascript";
|
||||
args = [
|
||||
"-e",
|
||||
`tell application "Terminal" to do script "${claudePath} setup-token"`,
|
||||
"-e",
|
||||
'tell application "Terminal" to activate',
|
||||
];
|
||||
} else {
|
||||
// Linux: Use the terminal we found earlier
|
||||
if (!linuxTerminal) {
|
||||
reject({
|
||||
success: false,
|
||||
error:
|
||||
"Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal.",
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
command = linuxTerminal.command;
|
||||
args = linuxTerminal.args;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[ClaudeCliDetector] Spawning terminal:",
|
||||
command,
|
||||
args.join(" ")
|
||||
);
|
||||
|
||||
const proc = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
shell: platform === "win32",
|
||||
});
|
||||
|
||||
proc.unref();
|
||||
|
||||
proc.on("error", (error) => {
|
||||
console.error("[ClaudeCliDetector] Failed to open terminal:", error);
|
||||
reject({
|
||||
success: false,
|
||||
error: `Failed to open terminal: ${error.message}`,
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Give the terminal a moment to open
|
||||
setTimeout(() => {
|
||||
send("Terminal window opened!\n\n");
|
||||
send("1. Complete the sign-in in your browser\n");
|
||||
send("2. Copy the token from the terminal\n");
|
||||
send("3. Paste it below\n");
|
||||
|
||||
// Resolve with manual auth required since we can't capture from external terminal
|
||||
resolve({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
terminalOpened: true,
|
||||
message:
|
||||
"Terminal opened. Complete authentication and paste the token below.",
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeCliDetector;
|
||||
@@ -170,14 +170,38 @@ class FeatureLoader {
|
||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||
|
||||
try {
|
||||
// Read feature.json directly - handle ENOENT in catch block
|
||||
// This avoids TOCTOU race condition from checking with fs.access first
|
||||
const content = await fs.readFile(featureJsonPath, "utf-8");
|
||||
const feature = JSON.parse(content);
|
||||
|
||||
// Validate that the feature has required fields
|
||||
if (!feature.id) {
|
||||
console.warn(
|
||||
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
features.push(feature);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[FeatureLoader] Failed to load feature ${featureId}:`,
|
||||
error
|
||||
);
|
||||
// Handle different error types appropriately
|
||||
if (error.code === "ENOENT") {
|
||||
// File doesn't exist - this is expected for incomplete feature directories
|
||||
// Skip silently (feature.json not yet created or was removed)
|
||||
continue;
|
||||
} else if (error instanceof SyntaxError) {
|
||||
// JSON parse error - log as warning since file exists but is malformed
|
||||
console.warn(
|
||||
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
|
||||
);
|
||||
} else {
|
||||
// Other errors - log as error
|
||||
console.error(
|
||||
`[FeatureLoader] Failed to load feature ${featureId}:`,
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
// Continue loading other features
|
||||
}
|
||||
}
|
||||
@@ -339,30 +363,93 @@ class FeatureLoader {
|
||||
/**
|
||||
* Update feature status (legacy API)
|
||||
* Features are stored in .automaker/features/{id}/feature.json
|
||||
* Creates the feature if it doesn't exist.
|
||||
* @param {string} featureId - The ID of the feature to update
|
||||
* @param {string} status - The new status
|
||||
* @param {string} projectPath - Path to the project
|
||||
* @param {string} [summary] - Optional summary of what was done
|
||||
* @param {string} [error] - Optional error message if feature errored
|
||||
* @param {Object} options - Options object for optional parameters
|
||||
* @param {string} [options.summary] - Optional summary of what was done
|
||||
* @param {string} [options.error] - Optional error message if feature errored
|
||||
* @param {string} [options.description] - Optional detailed description
|
||||
* @param {string} [options.category] - Optional category/phase
|
||||
* @param {string[]} [options.steps] - Optional array of implementation steps
|
||||
*/
|
||||
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
|
||||
async updateFeatureStatus(featureId, status, projectPath, options = {}) {
|
||||
const { summary, error, description, category, steps } = options;
|
||||
// Check if feature exists
|
||||
const existingFeature = await this.get(projectPath, featureId);
|
||||
|
||||
if (!existingFeature) {
|
||||
// Feature doesn't exist - create it with all required fields
|
||||
console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`);
|
||||
const newFeature = {
|
||||
id: featureId,
|
||||
title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
|
||||
description: description || summary || '', // Use provided description, fall back to summary
|
||||
category: category || "Uncategorized",
|
||||
steps: steps || [],
|
||||
status: status,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: false, // Auto-generated features should run tests by default
|
||||
model: "sonnet",
|
||||
thinkingLevel: "none",
|
||||
summary: summary || description || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
if (error !== undefined) {
|
||||
newFeature.error = error;
|
||||
}
|
||||
await this.create(projectPath, newFeature);
|
||||
console.log(
|
||||
`[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${
|
||||
summary ? `, summary="${summary}"` : ""
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Feature exists - update it
|
||||
const updates = { status };
|
||||
if (summary !== undefined) {
|
||||
updates.summary = summary;
|
||||
// Also update description if it's empty or not set
|
||||
if (!existingFeature.description) {
|
||||
updates.description = summary;
|
||||
}
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updates.description = description;
|
||||
}
|
||||
if (category !== undefined) {
|
||||
updates.category = category;
|
||||
}
|
||||
if (steps !== undefined && Array.isArray(steps)) {
|
||||
updates.steps = steps;
|
||||
}
|
||||
if (error !== undefined) {
|
||||
updates.error = error;
|
||||
} else {
|
||||
// Clear error if not provided
|
||||
const feature = await this.get(projectPath, featureId);
|
||||
if (feature && feature.error) {
|
||||
if (existingFeature.error) {
|
||||
updates.error = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure required fields exist (for features created before this fix)
|
||||
if (!existingFeature.category && !updates.category) updates.category = "Uncategorized";
|
||||
if (!existingFeature.steps && !updates.steps) updates.steps = [];
|
||||
if (!existingFeature.images) updates.images = [];
|
||||
if (!existingFeature.imagePaths) updates.imagePaths = [];
|
||||
if (existingFeature.skipTests === undefined) updates.skipTests = false;
|
||||
if (!existingFeature.model) updates.model = "sonnet";
|
||||
if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none";
|
||||
|
||||
await this.update(projectPath, featureId, updates);
|
||||
console.log(
|
||||
`[FeatureLoader] Updated feature ${featureId}: status=${status}${
|
||||
category ? `, category="${category}"` : ""
|
||||
}${steps ? `, steps=${steps.length}` : ""}${
|
||||
summary ? `, summary="${summary}"` : ""
|
||||
}`
|
||||
);
|
||||
98
apps/app/electron/services/mcp-server-factory.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk");
|
||||
const { z } = require("zod");
|
||||
const featureLoader = require("./feature-loader");
|
||||
|
||||
/**
|
||||
* MCP Server Factory - Creates custom MCP servers with tools
|
||||
*/
|
||||
class McpServerFactory {
|
||||
/**
|
||||
* Create a custom MCP server with the UpdateFeatureStatus tool
|
||||
* This tool allows Claude Code to safely update feature status without
|
||||
* directly modifying feature files, preventing race conditions
|
||||
* and accidental state corruption.
|
||||
*/
|
||||
createFeatureToolsServer(updateFeatureStatusCallback, projectPath) {
|
||||
return createSdkMcpServer({
|
||||
name: "automaker-tools",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"UpdateFeatureStatus",
|
||||
"Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.",
|
||||
{
|
||||
featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"),
|
||||
status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. Use 'backlog' or 'todo' for new features."),
|
||||
summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."),
|
||||
description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."),
|
||||
category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"),
|
||||
steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']")
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`);
|
||||
console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`);
|
||||
|
||||
// Load the feature to check skipTests flag
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
const feature = features.find((f) => f.id === args.featureId);
|
||||
|
||||
if (!feature) {
|
||||
console.log(`[Feature Creation] Feature ${args.featureId} not found - this might be a new feature being created`);
|
||||
// This might be a new feature - try to proceed anyway
|
||||
}
|
||||
|
||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
||||
let finalStatus = args.status;
|
||||
// Convert 'todo' to 'backlog' for consistency, but only for new features
|
||||
if (!feature && finalStatus === "todo") {
|
||||
finalStatus = "backlog";
|
||||
}
|
||||
if (feature && args.status === "verified" && feature.skipTests === true) {
|
||||
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
|
||||
finalStatus = "waiting_approval";
|
||||
}
|
||||
|
||||
// Call the provided callback to update feature status
|
||||
await updateFeatureStatusCallback(
|
||||
args.featureId,
|
||||
finalStatus,
|
||||
projectPath,
|
||||
{
|
||||
summary: args.summary,
|
||||
description: args.description,
|
||||
category: args.category,
|
||||
steps: args.steps,
|
||||
}
|
||||
);
|
||||
|
||||
const statusMessage = finalStatus !== args.status
|
||||
? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}`
|
||||
: `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`;
|
||||
|
||||
console.log(`[Feature Creation] ✓ ${statusMessage}`);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: statusMessage
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error);
|
||||
console.error(`[Feature Creation] ✗ Failed to create/update feature ${args.featureId}: ${error.message}`);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to update feature status: ${error.message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new McpServerFactory();
|
||||
@@ -215,7 +215,7 @@ async function handleToolsCall(params, id) {
|
||||
// Call the update callback via IPC or direct call
|
||||
// Since we're in a separate process, we need to use IPC to communicate back
|
||||
// For now, we'll call the feature loader directly since it has the update method
|
||||
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, summary);
|
||||
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary });
|
||||
|
||||
const statusMessage = finalStatus !== status
|
||||
? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}`
|
||||
@@ -52,11 +52,11 @@ ${memoryContent}
|
||||
**Current Feature to Implement:**
|
||||
|
||||
ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
Category: ${feature.category || "Uncategorized"}
|
||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
||||
**Steps to Complete:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
||||
|
||||
**Your Task:**
|
||||
|
||||
@@ -195,12 +195,12 @@ ${memoryContent}
|
||||
**Feature to Implement/Verify:**
|
||||
|
||||
ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
Category: ${feature.category || "Uncategorized"}
|
||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
||||
Current Status: ${feature.status}
|
||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
||||
**Steps that should be implemented:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
||||
|
||||
**Your Task:**
|
||||
|
||||
@@ -335,11 +335,11 @@ ${memoryContent}
|
||||
**Current Feature:**
|
||||
|
||||
ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
Category: ${feature.category || "Uncategorized"}
|
||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
||||
**Steps to Complete:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
||||
|
||||
**Previous Work Context:**
|
||||
|
||||
84
apps/app/electron/services/pty-runner.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const os = require("os");
|
||||
|
||||
// Prefer prebuilt to avoid native build issues.
|
||||
const pty = require("@homebridge/node-pty-prebuilt-multiarch");
|
||||
|
||||
/**
|
||||
* Minimal PTY helper to run CLI commands with a pseudo-terminal.
|
||||
* Useful for CLIs (like Claude) that need raw mode on Windows.
|
||||
*
|
||||
* @param {string} command Executable path
|
||||
* @param {string[]} args Arguments for the executable
|
||||
* @param {Object} options Additional spawn options
|
||||
* @param {(chunk: string) => void} [options.onData] Data callback
|
||||
* @param {string} [options.cwd] Working directory
|
||||
* @param {Object} [options.env] Extra env vars
|
||||
* @param {number} [options.cols] Terminal columns
|
||||
* @param {number} [options.rows] Terminal rows
|
||||
* @returns {Promise<{ success: boolean, exitCode: number, signal?: number, output: string, errorOutput: string }>}
|
||||
*/
|
||||
function runPtyCommand(command, args = [], options = {}) {
|
||||
const {
|
||||
onData,
|
||||
cwd = process.cwd(),
|
||||
env = {},
|
||||
cols = 120,
|
||||
rows = 30,
|
||||
} = options;
|
||||
|
||||
const mergedEnv = {
|
||||
...process.env,
|
||||
TERM: process.env.TERM || "xterm-256color",
|
||||
...env,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let ptyProcess;
|
||||
|
||||
try {
|
||||
ptyProcess = pty.spawn(command, args, {
|
||||
name: os.platform() === "win32" ? "Windows.Terminal" : "xterm-color",
|
||||
cols,
|
||||
rows,
|
||||
cwd,
|
||||
env: mergedEnv,
|
||||
useConpty: true,
|
||||
});
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
let output = "";
|
||||
let errorOutput = "";
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
output += data;
|
||||
if (typeof onData === "function") {
|
||||
onData(data);
|
||||
}
|
||||
});
|
||||
|
||||
// node-pty does not emit 'error' in practice, but guard anyway
|
||||
if (ptyProcess.on) {
|
||||
ptyProcess.on("error", (err) => {
|
||||
errorOutput += err?.message || "";
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
||||
resolve({
|
||||
success: exitCode === 0,
|
||||
exitCode,
|
||||
signal,
|
||||
output,
|
||||
errorOutput,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runPtyCommand,
|
||||
};
|
||||
|
||||
1038
apps/app/electron/services/spec-regeneration-service.js
Normal file
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "automaker",
|
||||
"name": "@automaker/app",
|
||||
"version": "0.1.0",
|
||||
"description": "An autonomous AI development studio that helps you build software faster using AI-powered agents",
|
||||
"homepage": "https://github.com/AutoMaker-Org/automaker",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AutoMaker-Org/automaker.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Cody Seibert",
|
||||
"email": "webdevcody@gmail.com"
|
||||
@@ -14,6 +18,7 @@
|
||||
"dev": "next dev -p 3007",
|
||||
"dev:web": "next dev -p 3007",
|
||||
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
|
||||
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
|
||||
"build": "next build",
|
||||
"build:electron": "next build && electron-builder",
|
||||
"start": "next start",
|
||||
@@ -28,6 +33,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -42,6 +48,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next": "16.0.7",
|
||||
"react": "19.2.0",
|
||||
@@ -52,6 +59,7 @@
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^4.0.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
@@ -70,6 +78,7 @@
|
||||
"build": {
|
||||
"appId": "com.automaker.app",
|
||||
"productName": "Automaker",
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
@@ -137,7 +146,8 @@
|
||||
],
|
||||
"category": "Development",
|
||||
"icon": "public/logo_larger.png",
|
||||
"maintainer": "webdevcody@gmail.com"
|
||||
"maintainer": "webdevcody@gmail.com",
|
||||
"executableName": "automaker"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,18 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { Toaster } from "sonner";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Automaker - Autonomous AI Development Studio",
|
||||
description: "Build software autonomously with intelligent orchestration",
|
||||
@@ -26,7 +16,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Toaster richColors position="bottom-right" />
|
||||
@@ -742,12 +742,15 @@ export function Sidebar() {
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={cn(
|
||||
"h-20 pt-8 flex items-center justify-between border-b border-sidebar-border shrink-0 titlebar-drag-region",
|
||||
sidebarOpen ? "px-3 lg:px-6" : "px-3"
|
||||
"h-20 border-b border-sidebar-border shrink-0 titlebar-drag-region",
|
||||
sidebarOpen ? "pt-8 px-3 lg:px-6 flex items-center justify-between" : "pt-2 pb-2 px-3 flex flex-col items-center justify-center gap-2"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center titlebar-no-drag cursor-pointer"
|
||||
className={cn(
|
||||
"flex items-center titlebar-no-drag cursor-pointer",
|
||||
!sidebarOpen && "flex-col gap-1"
|
||||
)}
|
||||
onClick={() => setCurrentView("welcome")}
|
||||
data-testid="logo-button"
|
||||
>
|
||||
@@ -208,7 +208,19 @@ export function LogViewer({ output, className }: LogViewerProps) {
|
||||
};
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No log entries yet. Logs will appear here as the process runs.</p>
|
||||
{output && output.trim() && (
|
||||
<div className="mt-4 p-3 bg-zinc-900/50 rounded text-xs font-mono text-left max-h-40 overflow-auto">
|
||||
<pre className="whitespace-pre-wrap">{output}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Count entries by type
|
||||
@@ -2322,11 +2322,11 @@ export function BoardView() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="skip-tests"
|
||||
checked={newFeature.skipTests}
|
||||
checked={!newFeature.skipTests}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
skipTests: checked === true,
|
||||
skipTests: checked !== true,
|
||||
})
|
||||
}
|
||||
data-testid="skip-tests-checkbox"
|
||||
@@ -2336,14 +2336,14 @@ export function BoardView() {
|
||||
htmlFor="skip-tests"
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
Skip automated testing
|
||||
Enable automated testing
|
||||
</Label>
|
||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, this feature will require manual verification
|
||||
instead of automated TDD.
|
||||
When enabled, this feature will use automated TDD. When
|
||||
disabled, it will require manual verification.
|
||||
</p>
|
||||
|
||||
{/* Verification Steps - Only shown when skipTests is enabled */}
|
||||
@@ -2742,11 +2742,11 @@ export function BoardView() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="edit-skip-tests"
|
||||
checked={editingFeature.skipTests ?? false}
|
||||
checked={!(editingFeature.skipTests ?? false)}
|
||||
onCheckedChange={(checked) =>
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
skipTests: checked === true,
|
||||
skipTests: checked !== true,
|
||||
})
|
||||
}
|
||||
data-testid="edit-skip-tests-checkbox"
|
||||
@@ -2756,14 +2756,14 @@ export function BoardView() {
|
||||
htmlFor="edit-skip-tests"
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
Skip automated testing
|
||||
Enable automated testing
|
||||
</Label>
|
||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, this feature will require manual verification
|
||||
instead of automated TDD.
|
||||
When enabled, this feature will use automated TDD. When
|
||||
disabled, it will require manual verification.
|
||||
</p>
|
||||
|
||||
{/* Verification Steps - Only shown when skipTests is enabled */}
|
||||
@@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
!isDescriptionExpanded && "line-clamp-3"
|
||||
)}
|
||||
>
|
||||
{feature.description}
|
||||
{feature.description || feature.summary || feature.id}
|
||||
</CardTitle>
|
||||
{/* Show More/Less toggle - only show when description is likely truncated */}
|
||||
{feature.description.length > 100 && (
|
||||
{(feature.description || feature.summary || "").length > 100 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -427,7 +427,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</CardHeader>
|
||||
<CardContent className="p-3 pt-0">
|
||||
{/* Steps Preview - Show in Standard and Detailed modes */}
|
||||
{showSteps && feature.steps.length > 0 && (
|
||||
{showSteps && feature.steps && feature.steps.length > 0 && (
|
||||
<div className="mb-3 space-y-1">
|
||||
{feature.steps.slice(0, 3).map((step, index) => (
|
||||
<div
|
||||
@@ -844,10 +844,13 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Sparkles className="w-5 h-5 text-green-400" />
|
||||
Implementation Summary
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm" title={feature.description}>
|
||||
{feature.description.length > 100
|
||||
? `${feature.description.slice(0, 100)}...`
|
||||
: feature.description}
|
||||
<DialogDescription className="text-sm" title={feature.description || feature.summary || ""}>
|
||||
{(() => {
|
||||
const displayText = feature.description || feature.summary || "No description";
|
||||
return displayText.length > 100
|
||||
? `${displayText.slice(0, 100)}...`
|
||||
: displayText;
|
||||
})()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border">
|
||||