Implement project picker keyboard shortcut and enhance feature management

- Added a new keyboard shortcut 'P' to open the project picker dropdown.
- Implemented functionality to select projects using number keys, allowing users to quickly switch between projects.
- Updated the feature list to include a new feature for project selection via keyboard shortcuts.
- Removed obsolete coding_prompt.md and added initializer_prompt.md for better session management.
- Introduced context management for features, enabling reading, writing, and deleting context files.
- Updated package dependencies to include @radix-ui/react-checkbox for enhanced UI components.

This commit enhances user experience by streamlining project selection and improves the overall feature management process.

🤖 Generated with Claude Code
This commit is contained in:
Cody Seibert
2025-12-09 12:20:07 -05:00
parent 95355f53f4
commit 9bae205312
39 changed files with 1551 additions and 4168 deletions

View File

@@ -1607,8 +1607,6 @@ Analyze this project's codebase and update the .automaker/app_spec.txt file with
6. Ensure .automaker/agents-context/ directory exists
7. Ensure .automaker/coding_prompt.md exists with default guidelines
**Important:**
- Be concise but accurate
- Only include information you can verify from the codebase

View File

@@ -0,0 +1,71 @@
const path = require("path");
const fs = require("fs/promises");
/**
* Context Manager - Handles reading, writing, and deleting context files for features
*/
class ContextManager {
/**
* Write output to feature context file
*/
async writeToContextFile(projectPath, featureId, content) {
if (!projectPath) return;
try {
const contextDir = path.join(projectPath, ".automaker", "agents-context");
// Ensure directory exists
try {
await fs.access(contextDir);
} catch {
await fs.mkdir(contextDir, { recursive: true });
}
const filePath = path.join(contextDir, `${featureId}.md`);
// Append to existing file or create new one
try {
const existing = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, existing + content, "utf-8");
} catch {
await fs.writeFile(filePath, content, "utf-8");
}
} catch (error) {
console.error("[ContextManager] Failed to write to context file:", error);
}
}
/**
* Read context file for a feature
*/
async readContextFile(projectPath, featureId) {
try {
const contextPath = path.join(projectPath, ".automaker", "agents-context", `${featureId}.md`);
const content = await fs.readFile(contextPath, "utf-8");
return content;
} catch (error) {
console.log(`[ContextManager] No context file found for ${featureId}`);
return null;
}
}
/**
* Delete agent context file for a feature
*/
async deleteContextFile(projectPath, featureId) {
if (!projectPath) return;
try {
const contextPath = path.join(projectPath, ".automaker", "agents-context", `${featureId}.md`);
await fs.unlink(contextPath);
console.log(`[ContextManager] Deleted agent context for feature ${featureId}`);
} catch (error) {
// File might not exist, which is fine
if (error.code !== 'ENOENT') {
console.error("[ContextManager] Failed to delete context file:", error);
}
}
}
}
module.exports = new ContextManager();

View File

@@ -0,0 +1,359 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const promptBuilder = require("./prompt-builder");
const contextManager = require("./context-manager");
const featureLoader = require("./feature-loader");
const mcpServerFactory = require("./mcp-server-factory");
/**
* Feature Executor - Handles feature implementation using Claude Agent SDK
*/
class FeatureExecutor {
/**
* Sleep helper
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Implement a single feature using Claude Agent SDK
* Uses a Plan-Act-Verify loop with detailed phase logging
*/
async implementFeature(feature, projectPath, sendToRenderer, execution) {
console.log(`[FeatureExecutor] Implementing: ${feature.description}`);
try {
// ========================================
// PHASE 1: PLANNING
// ========================================
const planningMessage = `📋 Planning implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, planningMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "planning",
message: `Planning implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: PLANNING for ${feature.description}`);
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
// Configure options for the SDK query
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: promptBuilder.getCodingPrompt(),
maxTurns: 1000,
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer
},
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
"mcp__automaker-tools__UpdateFeatureStatus",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
// Build the prompt for this specific feature
const prompt = promptBuilder.buildFeaturePrompt(feature);
// Planning: Analyze the codebase and create implementation plan
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content:
"Analyzing codebase structure and creating implementation plan...",
});
// Small delay to show planning phase
await this.sleep(500);
// ========================================
// PHASE 2: ACTION
// ========================================
const actionMessage = `⚡ Executing implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, actionMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Executing implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: ACTION for ${feature.description}`);
// Send query
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
// Stream responses
let responseText = "";
let hasStartedToolUse = false;
for await (const msg of currentQuery) {
// Check if this specific feature was aborted
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
// Write to context file
await contextManager.writeToContextFile(projectPath, feature.id, block.text);
// Stream progress to renderer
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
// First tool use indicates we're actively implementing
if (!hasStartedToolUse) {
hasStartedToolUse = true;
const startMsg = "Starting code implementation...\n";
await contextManager.writeToContextFile(projectPath, feature.id, startMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: startMsg,
});
}
// Write tool use to context file
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, toolMsg);
// Notify about tool use
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// ========================================
// PHASE 3: VERIFICATION
// ========================================
const verificationMessage = `✅ Verifying implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, verificationMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: VERIFICATION for ${feature.description}`);
const checkingMsg =
"Verifying implementation and checking test results...\n";
await contextManager.writeToContextFile(projectPath, feature.id, checkingMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: checkingMsg,
});
// Re-load features to check if it was marked as verified
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
// Send verification result
const resultMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Verification: Tests need attention\n";
await contextManager.writeToContextFile(projectPath, feature.id, resultMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: resultMsg,
});
return {
passes,
message: responseText.substring(0, 500), // First 500 chars
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[FeatureExecutor] Feature run aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Auto mode aborted",
};
}
console.error("[FeatureExecutor] Error implementing feature:", error);
// Clean up
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Resume feature implementation with previous context
*/
async resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution) {
console.log(`[FeatureExecutor] Resuming with context for: ${feature.description}`);
try {
const resumeMessage = `\n🔄 Resuming implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, resumeMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Resuming implementation for: ${feature.description}`,
});
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: promptBuilder.getVerificationPrompt(),
maxTurns: 1000,
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer
},
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebSearch", "WebFetch", "mcp__automaker-tools__UpdateFeatureStatus"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
// Build prompt with previous context
const prompt = promptBuilder.buildResumePrompt(feature, previousContext);
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let responseText = "";
for await (const msg of currentQuery) {
// Check if this specific feature was aborted
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await contextManager.writeToContextFile(projectPath, feature.id, block.text);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, toolMsg);
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// Check if feature was marked as verified
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
const finalMsg = passes
? "✓ Feature successfully verified and completed\n"
: "⚠ Feature still in progress - may need additional work\n";
await contextManager.writeToContextFile(projectPath, feature.id, finalMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: finalMsg,
});
return {
passes,
message: responseText.substring(0, 500),
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[FeatureExecutor] Resume aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Resume aborted",
};
}
console.error("[FeatureExecutor] Error resuming feature:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
}
module.exports = new FeatureExecutor();

View File

@@ -0,0 +1,76 @@
const path = require("path");
const fs = require("fs/promises");
/**
* Feature Loader - Handles loading and selecting features from feature_list.json
*/
class FeatureLoader {
/**
* Load features from .automaker/feature_list.json
*/
async loadFeatures(projectPath) {
const featuresPath = path.join(
projectPath,
".automaker",
"feature_list.json"
);
try {
const content = await fs.readFile(featuresPath, "utf-8");
const features = JSON.parse(content);
// Ensure each feature has an ID
return features.map((f, index) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
}));
} catch (error) {
console.error("[FeatureLoader] Failed to load features:", error);
return [];
}
}
/**
* Update feature status in .automaker/feature_list.json
*/
async updateFeatureStatus(featureId, status, projectPath) {
const features = await this.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
console.error(`[FeatureLoader] Feature ${featureId} not found`);
return;
}
// Update the status field
feature.status = status;
// Save back to file
const featuresPath = path.join(
projectPath,
".automaker",
"feature_list.json"
);
const toSave = features.map((f) => ({
id: f.id,
category: f.category,
description: f.description,
steps: f.steps,
status: f.status,
}));
await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8");
console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}`);
}
/**
* Select the next feature to implement
* Prioritizes: earlier features in the list that are not verified
*/
selectNextFeature(features) {
// Find first feature that is in backlog or in_progress status
return features.find((f) => f.status !== "verified");
}
}
module.exports = new FeatureLoader();

View File

@@ -0,0 +1,55 @@
const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk");
const { z } = require("zod");
/**
* 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 the feature_list.json file, preventing race conditions
* and accidental state restoration.
*/
createFeatureToolsServer(updateFeatureStatusCallback, projectPath) {
return createSdkMcpServer({
name: "automaker-tools",
version: "1.0.0",
tools: [
tool(
"UpdateFeatureStatus",
"Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status.",
{
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")
},
async (args) => {
try {
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}`);
// Call the provided callback to update feature status
await updateFeatureStatusCallback(args.featureId, args.status, projectPath);
return {
content: [{
type: "text",
text: `Successfully updated feature ${args.featureId} to status "${args.status}"`
}]
};
} 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();

View File

@@ -0,0 +1,394 @@
/**
* Prompt Builder - Generates prompts for different agent tasks
*/
class PromptBuilder {
/**
* Build the prompt for implementing a specific feature
*/
buildFeaturePrompt(feature) {
return `You are working on a feature implementation task.
**Current Feature to Implement:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current codebase structure
2. Implement the feature according to the description and steps
3. Write Playwright tests to verify the feature works correctly
4. Run the tests and ensure they pass
5. **DELETE the test file(s) you created** - tests are only for immediate verification
6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
7. Commit your changes with git
**IMPORTANT - Updating Feature Status:**
When you have completed the feature and all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
**Important Guidelines:**
- Focus ONLY on implementing this specific feature
- Write clean, production-quality code
- Add proper error handling
- Write comprehensive Playwright tests
- Ensure all existing tests still pass
- Mark the feature as passing only when all tests are green
- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
- Make a git commit when complete
**Testing Utilities (CRITICAL):**
1. **Create/maintain tests/utils.ts** - Add helper functions for finding elements and common test operations
2. **Use utilities in tests** - Import and use helper functions instead of repeating selectors
3. **Add utilities as needed** - When you write a test, if you need a new helper, add it to utils.ts
4. **Update utilities when functionality changes** - If you modify components, update corresponding utilities
Example utilities to add:
- getByTestId(page, testId) - Find elements by data-testid
- getButtonByText(page, text) - Find buttons by text
- clickElement(page, testId) - Click an element by test ID
- fillForm(page, formData) - Fill form fields
- waitForElement(page, testId) - Wait for element to appear
This makes future tests easier to write and maintain!
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
Begin by reading the project structure and then implementing the feature.`;
}
/**
* Build the prompt for verifying a specific feature
*/
buildVerificationPrompt(feature) {
return `You are implementing and verifying a feature until it is complete and working correctly.
**Feature to Implement/Verify:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
Current Status: ${feature.status}
**Steps that should be implemented:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current implementation
2. If the feature is not fully implemented, continue implementing it
3. Write or update Playwright tests to verify the feature works correctly
4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts
5. Check if all tests pass
6. **If ANY tests fail:**
- Analyze the test failures and error messages
- Fix the implementation code to make the tests pass
- Update test utilities in tests/utils.ts if needed
- Re-run the tests to verify the fixes
- **REPEAT this process until ALL tests pass**
7. **If ALL tests pass:**
- **DELETE the test file(s) for this feature** - tests are only for immediate verification
- **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
- Explain what was implemented/fixed and that all tests passed
- Commit your changes with git
**IMPORTANT - Updating Feature Status:**
When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
**Testing Utilities:**
- Check if tests/utils.ts exists and is being used
- If utilities are outdated due to functionality changes, update them
- Add new utilities as needed for this feature's tests
- Ensure test utilities stay in sync with code changes
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
**Important:**
- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure
- Only mark as "verified" if Playwright tests pass
- **CRITICAL: Delete test files after they pass** - tests should not accumulate
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
- Update test utilities if functionality changed
- Make a git commit when the feature is complete
- Be thorough and persistent in fixing issues
Begin by reading the project structure and understanding what needs to be implemented or fixed.`;
}
/**
* Build prompt for resuming feature with previous context
*/
buildResumePrompt(feature, previousContext) {
return `You are resuming work on a feature implementation that was previously started.
**Current Feature:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Previous Work Context:**
${previousContext || "No previous context available - this is a fresh start."}
**Your Task:**
Continue where you left off and complete the feature implementation:
1. Review the previous work context above to understand what has been done
2. Continue implementing the feature according to the description and steps
3. Write Playwright tests to verify the feature works correctly (if not already done)
4. Run the tests and ensure they pass
5. **DELETE the test file(s) you created** - tests are only for immediate verification
6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
7. Commit your changes with git
**IMPORTANT - Updating Feature Status:**
When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
**Important Guidelines:**
- Review what was already done in the previous context
- Don't redo work that's already complete - continue from where it left off
- Focus on completing any remaining tasks
- Write comprehensive Playwright tests if not already done
- Ensure all tests pass before marking as verified
- **CRITICAL: Delete test files after verification**
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
- Make a git commit when complete
Begin by assessing what's been done and what remains to be completed.`;
}
/**
* Build the prompt for project analysis
*/
buildProjectAnalysisPrompt(projectPath) {
return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio.
**Your Task:**
Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about:
1. **Project Name** - Detect the name from package.json, README, or directory name
2. **Overview** - Brief description of what the project does
3. **Technology Stack** - Languages, frameworks, libraries detected
4. **Core Capabilities** - Main features and functionality
5. **Implemented Features** - What features are already built
**Steps to Follow:**
1. First, explore the project structure:
- Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack
- Check README.md for project description
- List key directories (src, lib, components, etc.)
2. Identify the tech stack:
- Frontend framework (React, Vue, Next.js, etc.)
- Backend framework (Express, FastAPI, etc.)
- Database (if any config files exist)
- Testing framework
- Build tools
3. Update .automaker/app_spec.txt with your findings in this format:
\`\`\`xml
<project_specification>
<project_name>Detected Name</project_name>
<overview>
Clear description of what this project does based on your analysis.
</overview>
<technology_stack>
<frontend>
<framework>Framework Name</framework>
<!-- Add detected technologies -->
</frontend>
<backend>
<!-- If applicable -->
</backend>
<database>
<!-- If applicable -->
</database>
<testing>
<!-- Testing frameworks detected -->
</testing>
</technology_stack>
<core_capabilities>
<!-- List main features/capabilities you found -->
</core_capabilities>
<implemented_features>
<!-- List specific features that appear to be implemented -->
</implemented_features>
</project_specification>
\`\`\`
4. Ensure .automaker/feature_list.json exists (create as empty array [] if not)
5. Ensure .automaker/context/ directory exists
6. Ensure .automaker/agents-context/ directory exists
**Important:**
- Be concise but accurate
- Only include information you can verify from the codebase
- If unsure about something, note it as "to be determined"
- Don't make up features that don't exist
Begin by exploring the project structure.`;
}
/**
* Get the system prompt for coding agent
*/
getCodingPrompt() {
return `You are an AI coding agent working autonomously to implement features.
Your role is to:
- Implement features exactly as specified
- Write production-quality code
- Create comprehensive Playwright tests using testing utilities
- Ensure all tests pass before marking features complete
- **DELETE test files after successful verification** - tests are only for immediate feature verification
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json
- Commit working code to git
- Be thorough and detail-oriented
**IMPORTANT - UpdateFeatureStatus Tool:**
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status:
- Call with featureId and status="verified"
- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state
- The tool safely updates the status without corrupting other feature data
**Testing Utilities (CRITICAL):**
- **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations
- **Always use utilities in tests** instead of repeating selectors
- **Add new utilities as you write tests** - if you need a helper, add it to utils.ts
- **Update utilities when functionality changes** - keep helpers in sync with code changes
This makes future tests easier to write and more maintainable!
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Run the tests to ensure they pass
2. Delete the test file for that feature
3. Use UpdateFeatureStatus tool to mark the feature as "verified"
This prevents test brittleness as the app changes rapidly.
You have full access to:
- Read and write files
- Run bash commands
- Execute tests
- Delete files (rm command)
- Make git commits
- Search and analyze the codebase
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass and use the UpdateFeatureStatus tool.`;
}
/**
* Get the system prompt for verification agent
*/
getVerificationPrompt() {
return `You are an AI implementation and verification agent focused on completing features and ensuring they work.
Your role is to:
- **Continue implementing features until they are complete** - don't stop at the first failure
- Write or update code to fix failing tests
- Run Playwright tests to verify feature implementations
- If tests fail, analyze errors and fix the implementation
- If other tests fail, verify if those tests are still accurate or should be updated or deleted
- Continue rerunning tests and fixing issues until ALL tests pass
- **DELETE test files after successful verification** - tests are only for immediate feature verification
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json
- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code
- Commit working code to git
**IMPORTANT - UpdateFeatureStatus Tool:**
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status:
- Call with featureId and status="verified"
- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state
- The tool safely updates the status without corrupting other feature data
**Testing Utilities:**
- Check if tests/utils.ts needs updates based on code changes
- If a component's selectors or behavior changed, update the corresponding utility functions
- Add new utilities as needed for the feature's tests
- Ensure utilities remain accurate and helpful for future tests
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Delete the test file for that feature
2. Use UpdateFeatureStatus tool to mark the feature as "verified"
This prevents test brittleness as the app changes rapidly.
You have access to:
- Read and edit files
- Write new code or modify existing code
- Run bash commands (especially Playwright tests)
- Delete files (rm command)
- Analyze test output
- Make git commits
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool, and commit your work.`;
}
/**
* Get system prompt for project analysis agent
*/
getProjectAnalysisSystemPrompt() {
return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features.
Your goal is to:
- Quickly scan and understand project structure
- Identify programming languages, frameworks, and libraries
- Detect existing features and capabilities
- Update the .automaker/app_spec.txt with accurate information
- Ensure all required .automaker files and directories exist
Be efficient - don't read every file, focus on:
- Configuration files (package.json, tsconfig.json, etc.)
- Main entry points
- Directory structure
- README and documentation
You have read access to files and can run basic bash commands to explore the structure.`;
}
}
module.exports = new PromptBuilder();

31
app/package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
@@ -2559,6 +2560,36 @@
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",

View File

@@ -19,6 +19,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",

View File

@@ -88,38 +88,43 @@ export async function POST(request: NextRequest) {
// Convert message history to SDK format to preserve conversation context
// Include both user and assistant messages for full context
const sessionId = `api-session-${Date.now()}`;
const conversationMessages = messages.map((msg: { role: string; content: string }) => {
if (msg.role === 'user') {
return {
type: 'user' as const,
message: {
role: 'user' as const,
content: msg.content
},
parent_tool_use_id: null,
session_id: sessionId,
};
} else {
// Assistant message
return {
type: 'assistant' as const,
message: {
role: 'assistant' as const,
content: [
{
type: 'text' as const,
text: msg.content
}
]
},
session_id: sessionId,
};
const conversationMessages = messages.map(
(msg: { role: string; content: string }) => {
if (msg.role === "user") {
return {
type: "user" as const,
message: {
role: "user" as const,
content: msg.content,
},
parent_tool_use_id: null,
session_id: sessionId,
};
} else {
// Assistant message
return {
type: "assistant" as const,
message: {
role: "assistant" as const,
content: [
{
type: "text" as const,
text: msg.content,
},
],
},
session_id: sessionId,
};
}
}
});
);
// Execute query with full conversation context
const queryResult = query({
prompt: conversationMessages.length > 0 ? conversationMessages : lastMessage.content,
prompt:
conversationMessages.length > 0
? conversationMessages
: lastMessage.content,
options,
});

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import Link from "next/link";
@@ -62,6 +62,9 @@ export function Sidebar() {
removeProject,
} = useAppStore();
// State for project picker dropdown
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
const navSections: NavSection[] = [
{
@@ -81,6 +84,33 @@ export function Sidebar() {
},
];
// Handler for selecting a project by number key
const selectProjectByNumber = useCallback((num: number) => {
const projectIndex = num - 1;
if (projectIndex >= 0 && projectIndex < projects.length) {
setCurrentProject(projects[projectIndex]);
setIsProjectPickerOpen(false);
}
}, [projects, setCurrentProject]);
// Handle number key presses when project picker is open
useEffect(() => {
if (!isProjectPickerOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
const num = parseInt(event.key, 10);
if (num >= 1 && num <= 5) {
event.preventDefault();
selectProjectByNumber(num);
} else if (event.key === "Escape") {
setIsProjectPickerOpen(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isProjectPickerOpen, selectProjectByNumber]);
// Build keyboard shortcuts for navigation
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [];
@@ -99,6 +129,15 @@ export function Sidebar() {
description: "Open project (navigate to welcome view)",
});
// Project picker shortcut - only when we have projects
if (projects.length > 0) {
shortcuts.push({
key: ACTION_SHORTCUTS.projectPicker,
action: () => setIsProjectPickerOpen(true),
description: "Open project picker",
});
}
// Only enable nav shortcuts if there's a current project
if (currentProject) {
navSections.forEach((section) => {
@@ -122,7 +161,7 @@ export function Sidebar() {
}
return shortcuts;
}, [currentProject, setCurrentView, toggleSidebar]);
}, [currentProject, setCurrentView, toggleSidebar, projects.length]);
// Register keyboard shortcuts
useKeyboardShortcuts(navigationShortcuts);
@@ -216,7 +255,7 @@ export function Sidebar() {
{/* Project Selector */}
{sidebarOpen && projects.length > 0 && (
<div className="px-2 mt-3">
<DropdownMenu>
<DropdownMenu open={isProjectPickerOpen} onOpenChange={setIsProjectPickerOpen}>
<DropdownMenuTrigger asChild>
<button
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-all text-white titlebar-no-drag"
@@ -228,20 +267,38 @@ export function Sidebar() {
{currentProject?.name || "Select Project"}
</span>
</div>
<ChevronDown className="h-4 w-4 text-zinc-400 flex-shrink-0" />
<div className="flex items-center gap-1">
<span
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500"
data-testid="project-picker-shortcut"
>
{ACTION_SHORTCUTS.projectPicker}
</span>
<ChevronDown className="h-4 w-4 text-zinc-400 flex-shrink-0" />
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 bg-zinc-800 border-zinc-700"
align="start"
data-testid="project-picker-dropdown"
>
{projects.map((project) => (
{projects.slice(0, 5).map((project, index) => (
<DropdownMenuItem
key={project.id}
onClick={() => setCurrentProject(project)}
onClick={() => {
setCurrentProject(project);
setIsProjectPickerOpen(false);
}}
className="flex items-center gap-2 cursor-pointer text-zinc-300 hover:text-white hover:bg-zinc-700/50"
data-testid={`project-option-${project.id}`}
>
<span
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-400"
data-testid={`project-hotkey-${index + 1}`}
>
{index + 1}
</span>
<Folder className="h-4 w-4" />
<span className="flex-1 truncate">{project.name}</span>
{currentProject?.id === project.id && (

View File

@@ -104,6 +104,10 @@ export function BoardView() {
};
}, [currentProject]);
// Track previous project to detect switches
const prevProjectPathRef = useRef<string | null>(null);
const isSwitchingProjectRef = useRef<boolean>(false);
// Auto mode hook
const autoMode = useAutoMode();
@@ -196,6 +200,20 @@ export function BoardView() {
const loadFeatures = useCallback(async () => {
if (!currentProject) return;
const currentPath = currentProject.path;
const previousPath = prevProjectPathRef.current;
// If project switched, clear features first to prevent cross-contamination
if (previousPath !== null && currentPath !== previousPath) {
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`);
isSwitchingProjectRef.current = true;
setFeatures([]);
setPersistedCategories([]); // Also clear categories
}
// Update the ref to track current project
prevProjectPathRef.current = currentPath;
setIsLoading(true);
try {
const api = getElectronAPI();
@@ -219,6 +237,7 @@ export function BoardView() {
console.error("Failed to load features:", error);
} finally {
setIsLoading(false);
isSwitchingProjectRef.current = false;
}
}, [currentProject, setFeatures]);
@@ -237,10 +256,14 @@ export function BoardView() {
if (Array.isArray(parsed)) {
setPersistedCategories(parsed);
}
} else {
// File doesn't exist, ensure categories are cleared
setPersistedCategories([]);
}
} catch (error) {
console.error("Failed to load categories:", error);
// If file doesn't exist, that's fine - start with empty array
// If file doesn't exist, ensure categories are cleared
setPersistedCategories([]);
}
}, [currentProject]);
@@ -384,7 +407,7 @@ export function BoardView() {
// Save when features change (after initial load is complete)
useEffect(() => {
if (!isLoading) {
if (!isLoading && !isSwitchingProjectRef.current) {
saveFeatures();
}
}, [features, saveFeatures, isLoading]);

View File

@@ -11,6 +11,7 @@ interface KanbanColumnProps {
count: number;
children: ReactNode;
isDoubleWidth?: boolean;
headerAction?: ReactNode;
}
export function KanbanColumn({
@@ -20,6 +21,7 @@ export function KanbanColumn({
count,
children,
isDoubleWidth = false,
headerAction,
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({ id });
@@ -37,6 +39,7 @@ export function KanbanColumn({
<div className="flex items-center gap-2 p-3 border-b border-white/5">
<div className={cn("w-3 h-3 rounded-full", color)} />
<h3 className="font-medium text-sm flex-1">{title}</h3>
{headerAction}
<span className="text-xs text-muted-foreground bg-background px-2 py-0.5 rounded-full">
{count}
</span>

View File

@@ -97,16 +97,6 @@ export function SpecView() {
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={loadSpec}
disabled={isLoading}
data-testid="reload-spec"
>
<RefreshCw className="w-4 h-4 mr-2" />
Reload
</Button>
<Button
size="sm"
onClick={saveSpec}

View File

@@ -116,4 +116,5 @@ export const ACTION_SHORTCUTS: Record<string, string> = {
startNext: "Q", // Q for Queue (start next features from backlog)
newSession: "W", // W for new session (in agent view)
openProject: "O", // O for Open project (navigate to welcome view)
projectPicker: "P", // P for Project picker
};

View File

@@ -45,35 +45,6 @@ const DEFAULT_APP_SPEC = `<project_specification>
*/
const DEFAULT_FEATURE_LIST = JSON.stringify([], null, 2);
/**
* Default coding_prompt.md template for new projects
*/
const DEFAULT_CODING_PROMPT = `# Coding Guidelines
This file contains project-specific coding guidelines and conventions
that the AI agent should follow when implementing features.
## Code Style
- Follow existing code conventions in the project
- Use consistent formatting and naming conventions
- Add appropriate comments for complex logic
## Testing
- Write tests for new features when applicable
- Ensure existing tests pass before marking features complete
## Git Commits
- Use clear, descriptive commit messages
- Reference feature IDs when relevant
## Additional Notes
Add any project-specific guidelines here.
`;
/**
* Required files and directories in the .automaker directory
*/
@@ -86,7 +57,6 @@ const REQUIRED_STRUCTURE = {
files: {
".automaker/app_spec.txt": DEFAULT_APP_SPEC,
".automaker/feature_list.json": DEFAULT_FEATURE_LIST,
".automaker/coding_prompt.md": DEFAULT_CODING_PROMPT,
},
};
@@ -96,7 +66,9 @@ const REQUIRED_STRUCTURE = {
* @param projectPath - The root path of the project
* @returns Result indicating what was created or if the project was already initialized
*/
export async function initializeProject(projectPath: string): Promise<ProjectInitResult> {
export async function initializeProject(
projectPath: string
): Promise<ProjectInitResult> {
const api = getElectronAPI();
const createdFiles: string[] = [];
const existingFiles: string[] = [];
@@ -109,7 +81,9 @@ export async function initializeProject(projectPath: string): Promise<ProjectIni
}
// Check and create required files
for (const [relativePath, defaultContent] of Object.entries(REQUIRED_STRUCTURE.files)) {
for (const [relativePath, defaultContent] of Object.entries(
REQUIRED_STRUCTURE.files
)) {
const fullPath = `${projectPath}/${relativePath}`;
const exists = await api.exists(fullPath);
@@ -122,7 +96,8 @@ export async function initializeProject(projectPath: string): Promise<ProjectIni
}
// Determine if this is a new project (all files were created)
const isNewProject = createdFiles.length === Object.keys(REQUIRED_STRUCTURE.files).length;
const isNewProject =
createdFiles.length === Object.keys(REQUIRED_STRUCTURE.files).length;
return {
success: true,
@@ -146,7 +121,9 @@ export async function initializeProject(projectPath: string): Promise<ProjectIni
* @param projectPath - The root path of the project
* @returns true if all required files/directories exist
*/
export async function isProjectInitialized(projectPath: string): Promise<boolean> {
export async function isProjectInitialized(
projectPath: string
): Promise<boolean> {
const api = getElectronAPI();
try {
@@ -161,7 +138,10 @@ export async function isProjectInitialized(projectPath: string): Promise<boolean
return true;
} catch (error) {
console.error("[project-init] Error checking project initialization:", error);
console.error(
"[project-init] Error checking project initialization:",
error
);
return false;
}
}

View File

@@ -1,196 +0,0 @@
import { test, expect } from "@playwright/test";
import { setupMockProject, clickElement } from "./utils";
// Helper function to navigate to context view and wait for either loading or main view
async function navigateToContextAndOpenDialog(page: any) {
// Click on context nav
const contextNav = page.locator('[data-testid="nav-context"]');
await contextNav.waitFor({ state: "visible", timeout: 10000 });
await contextNav.click();
// Wait for either the context view or the loading view
// The loading view might stay visible if the electron API is mocked
await page.waitForSelector(
'[data-testid="context-view"], [data-testid="context-view-loading"], [data-testid="context-view-no-project"]',
{ timeout: 10000 }
);
// If we have the main context view, click the add button
const contextView = page.locator('[data-testid="context-view"]');
const isContextViewVisible = await contextView.isVisible().catch(() => false);
if (isContextViewVisible) {
// Click add context file button
const addFileBtn = page.locator('[data-testid="add-context-file"]');
await addFileBtn.click();
} else {
// If context view isn't visible, we might be in loading state
// For testing purposes, simulate opening the dialog via keyboard or other means
// Skip this test scenario
test.skip();
return;
}
// Wait for dialog to appear
const dialog = page.locator('[data-testid="add-context-dialog"]');
await dialog.waitFor({ state: "visible", timeout: 5000 });
}
test.describe("Add Context File Dialog", () => {
test.beforeEach(async ({ page }) => {
await setupMockProject(page);
await page.goto("/");
await page.waitForLoadState("networkidle");
});
test("should show file name input and content textarea in add context dialog", async ({
page,
}) => {
await navigateToContextAndOpenDialog(page);
// Verify file name input is visible
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toBeVisible();
// Verify content textarea is visible when text type is selected (default)
const contentTextarea = page.locator('[data-testid="new-file-content"]');
await expect(contentTextarea).toBeVisible();
// Verify placeholder text
await expect(contentTextarea).toHaveAttribute(
"placeholder",
"Enter context content here or drag & drop a .txt or .md file..."
);
});
test("should allow typing content in the textarea", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent =
"# Test Context\n\nThis is test content for the context file.";
await contentTextarea.fill(testContent);
await expect(contentTextarea).toHaveValue(testContent);
});
test("should show textarea only for text file type", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
// Verify textarea is visible when text type is selected (default)
const contentTextarea = page.locator('[data-testid="new-file-content"]');
await expect(contentTextarea).toBeVisible();
// Switch to image type
await clickElement(page, "add-image-type");
// Verify textarea is no longer visible
await expect(contentTextarea).not.toBeVisible();
// Verify image upload input is attached instead
const imageUploadInput = page.locator('[data-testid="image-upload-input"]');
await expect(imageUploadInput).toBeAttached();
// Switch back to text type
await clickElement(page, "add-text-type");
// Verify textarea is visible again
const contentTextareaAgain = page.locator('[data-testid="new-file-content"]');
await expect(contentTextareaAgain).toBeVisible();
});
test("should display drag and drop helper text", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
// Check for helper text about drag and drop
const helperText = page.locator(
"text=Drag & drop .txt or .md files to import their content"
);
await expect(helperText).toBeVisible();
});
test("should populate content from dropped .txt file", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "This is content from a text file.";
// Create a data transfer with a .txt file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "test-file.txt", { type: "text/plain" });
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was auto-filled
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toHaveValue("test-file.txt");
});
test("should populate content from dropped .md file", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "# Markdown File\n\nThis is markdown content.";
// Create a data transfer with a .md file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "readme.md", { type: "text/markdown" });
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was auto-filled
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toHaveValue("readme.md");
});
test("should not auto-fill filename if already provided", async ({
page,
}) => {
await navigateToContextAndOpenDialog(page);
// Fill in the filename first
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await fileNameInput.fill("my-custom-name.md");
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "Content from dropped file";
// Create a data transfer with a .txt file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "dropped-file.txt", {
type: "text/plain",
});
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was NOT overwritten
await expect(fileNameInput).toHaveValue("my-custom-name.md");
});
});

View File

@@ -0,0 +1,237 @@
import { test, expect } from "@playwright/test";
import {
setupMockMultipleProjects,
waitForElement,
isProjectPickerDropdownOpen,
waitForProjectPickerDropdown,
waitForProjectPickerDropdownHidden,
pressShortcut,
pressNumberKey,
isProjectHotkeyVisible,
getProjectPickerShortcut,
} from "./utils";
test.describe("Project Picker Keyboard Shortcuts", () => {
test("pressing P key opens the project picker dropdown", async ({ page }) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar to be visible
await waitForElement(page, "sidebar");
// Dropdown should initially be closed
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
// Press P to open project picker
await pressShortcut(page, "p");
// Dropdown should now be open
await waitForProjectPickerDropdown(page);
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
});
test("project options show hotkey indicators (1-5)", async ({ page }) => {
// Setup with 5 projects
await setupMockMultipleProjects(page, 5);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Check that all 5 hotkey indicators are visible
for (let i = 1; i <= 5; i++) {
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
const hotkey = page.locator(`[data-testid="project-hotkey-${i}"]`);
expect(await hotkey.textContent()).toBe(i.toString());
}
});
test("pressing number key selects the corresponding project", async ({
page,
}) => {
// Setup with 3 projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Check initial project (should be Test Project 1)
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 1");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press 2 to select the second project
await pressNumberKey(page, 2);
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
// Project should now be Test Project 2
await expect(projectSelector).toContainText("Test Project 2");
});
test("pressing number key for non-existent project does nothing", async ({
page,
}) => {
// Setup with 2 projects
await setupMockMultipleProjects(page, 2);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Check initial project
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 1");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press 5 (there's no 5th project)
await pressNumberKey(page, 5);
// Dropdown should remain open
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
// Project should still be Test Project 1
await expect(projectSelector).toContainText("Test Project 1");
});
test("pressing Escape closes the project picker dropdown", async ({
page,
}) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press Escape
await page.keyboard.press("Escape");
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
});
test("project selector button shows P shortcut indicator", async ({
page,
}) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar and project selector
await waitForElement(page, "sidebar");
await waitForElement(page, "project-selector");
// Check that P shortcut indicator is visible
const shortcutIndicator = await getProjectPickerShortcut(page);
await expect(shortcutIndicator).toBeVisible();
await expect(shortcutIndicator).toHaveText("P");
});
test("only first 5 projects are shown with hotkeys", async ({ page }) => {
// Setup with 7 projects
await setupMockMultipleProjects(page, 7);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Only 5 hotkey indicators should be visible (1-5)
for (let i = 1; i <= 5; i++) {
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
}
// 6th and 7th should not exist
const hotkey6 = page.locator('[data-testid="project-hotkey-6"]');
const hotkey7 = page.locator('[data-testid="project-hotkey-7"]');
await expect(hotkey6).not.toBeVisible();
await expect(hotkey7).not.toBeVisible();
});
test("clicking a project option also works", async ({ page }) => {
// Setup with 3 projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker by clicking
await page.locator('[data-testid="project-selector"]').click();
await waitForProjectPickerDropdown(page);
// Click on second project option
await page.locator('[data-testid="project-option-test-project-2"]').click();
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
// Project should now be Test Project 2
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 2");
});
test("P shortcut does not work when no projects exist", async ({ page }) => {
// Setup with empty projects
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));
});
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Press P - should not open any dropdown since there are no projects
await pressShortcut(page, "p");
await page.waitForTimeout(300);
// Dropdown should not be visible
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
});
});

View File

@@ -1701,3 +1701,90 @@ export async function getOutputModalDescription(page: Page): Promise<string | nu
const modalDescription = page.locator('[data-testid="agent-output-modal"] [data-slot="dialog-description"]');
return await modalDescription.textContent().catch(() => null);
}
/**
* Check if the project picker dropdown is open
*/
export async function isProjectPickerDropdownOpen(page: Page): Promise<boolean> {
const dropdown = page.locator('[data-testid="project-picker-dropdown"]');
return await dropdown.isVisible().catch(() => false);
}
/**
* Wait for the project picker dropdown to be visible
*/
export async function waitForProjectPickerDropdown(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "project-picker-dropdown", options);
}
/**
* Wait for the project picker dropdown to be hidden
*/
export async function waitForProjectPickerDropdownHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "project-picker-dropdown", options);
}
/**
* Get a project hotkey indicator element by number (1-5)
*/
export async function getProjectHotkey(page: Page, num: number): Promise<Locator> {
return page.locator(`[data-testid="project-hotkey-${num}"]`);
}
/**
* Check if a project hotkey indicator is visible
*/
export async function isProjectHotkeyVisible(page: Page, num: number): Promise<boolean> {
const hotkey = page.locator(`[data-testid="project-hotkey-${num}"]`);
return await hotkey.isVisible().catch(() => false);
}
/**
* Get the project picker shortcut indicator (P key)
*/
export async function getProjectPickerShortcut(page: Page): Promise<Locator> {
return page.locator('[data-testid="project-picker-shortcut"]');
}
/**
* Set up a mock state with multiple projects
*/
export async function setupMockMultipleProjects(
page: Page,
projectCount: number = 3
): Promise<void> {
await page.addInitScript((count: number) => {
const mockProjects = [];
for (let i = 0; i < count; i++) {
mockProjects.push({
id: `test-project-${i + 1}`,
name: `Test Project ${i + 1}`,
path: `/mock/test-project-${i + 1}`,
lastOpened: new Date(Date.now() - i * 86400000).toISOString(),
});
}
const mockState = {
state: {
projects: mockProjects,
currentProject: mockProjects[0],
currentView: "board",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
}, projectCount);
}