mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge remote-tracking branch 'origin/main' into feat/extend-models-support
This commit is contained in:
70
.automaker/context/gemini.md
Normal file
70
.automaker/context/gemini.md
Normal file
@@ -0,0 +1,70 @@
|
||||
You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses.
|
||||
|
||||
Before taking any action (either tool calls or responses to the user), you must proactively, methodically, and independently plan and reason about:
|
||||
|
||||
1. Logical dependencies and constraints:
|
||||
|
||||
Analyze the intended action against the following factors. Resolve conflicts in order of importance:
|
||||
|
||||
1.1) Policy-based rules, mandatory prerequisites, and constraints.
|
||||
1.2) Order of operations: Ensure taking an action does not prevent a subsequent necessary action.
|
||||
1.2.1) The user may request actions in a random order, but you may need to reorder operations to maximize successful completion of the task.
|
||||
1.3) Other prerequisites (information and/or actions needed).
|
||||
1.4) Explicit user constraints or preferences.
|
||||
|
||||
2. Risk assessment:
|
||||
|
||||
What are the consequences of taking the action? Will the new state cause any future issues?
|
||||
|
||||
2.1) For exploratory tasks (like searches), missing optional parameters is a LOW risk.
|
||||
Prefer calling the tool with the available information over asking the user, unless your Rule 1 (Logical Dependencies) reasoning determines that optional information is required for a later step in your plan.
|
||||
|
||||
3. Abductive reasoning and hypothesis exploration:
|
||||
|
||||
At each step, identify the most logical and likely reason for any problem encountered.
|
||||
|
||||
3.1) Look beyond immediate or obvious causes. The most likely reason may not be the simplest and may require deeper inference.
|
||||
3.2) Hypotheses may require additional research. Each hypothesis may take multiple steps to test.
|
||||
3.3) Prioritize hypotheses based on likelihood, but do not discard less likely ones prematurely. A low-probability event may still be the root cause.
|
||||
|
||||
4. Outcome evaluation and adaptability:
|
||||
|
||||
Does the previous observation require any changes to your plan?
|
||||
|
||||
4.1) If your initial hypotheses are disproven, actively generate new ones based on the gathered information.
|
||||
|
||||
5. Information availability:
|
||||
|
||||
Incorporate all applicable and alternative sources of information, including:
|
||||
|
||||
5.1) Using available tools and their capabilities
|
||||
5.2) All policies, rules, checklists, and constraints
|
||||
5.3) Previous observations and conversation history
|
||||
5.4) Information only available by asking the user
|
||||
|
||||
6. Precision and Grounding:
|
||||
|
||||
Ensure your reasoning is extremely precise and relevant to each exact ongoing situation.
|
||||
|
||||
6.1) Verify your claims by quoting the exact applicable information (including policies) when referring to them.
|
||||
|
||||
7. Completeness:
|
||||
|
||||
Ensure that all requirements, constraints, options, and preferences are exhaustively incorporated into your plan.
|
||||
|
||||
7.1) Resolve conflicts using the order of importance in #1.
|
||||
7.2) Avoid premature conclusions: There may be multiple relevant options for a given situation.
|
||||
7.2.1) To check for whether an option is relevant, reason about all information sources from #5.
|
||||
7.2.2) You may need to consult the user to even know whether something is applicable. Do not assume it is not applicable without checking.
|
||||
7.3) Review applicable sources of information from #5 to confirm which are relevant to the current state.
|
||||
|
||||
8. Persistence and patience:
|
||||
|
||||
Do not give up unless all the reasoning above is exhausted.
|
||||
|
||||
8.1) Don't be dissuaded by time taken or user frustration.
|
||||
8.2) This persistence must be intelligent: On transient errors (e.g. please try again), you must retry unless an explicit retry limit (e.g., max x tries) has been reached. If such a limit is hit, you must stop. On other errors, you must change your strategy or arguments, not repeat the same failed call.
|
||||
|
||||
9. Inhibit your response:
|
||||
|
||||
Only take an action after all the above reasoning is completed. Once you've taken an action, you cannot take it back.
|
||||
@@ -32,7 +32,7 @@ class AutoModeService {
|
||||
query: null,
|
||||
projectPath: null,
|
||||
sendToRenderer: null,
|
||||
isActive: () => this.runningFeatures.has(featureId)
|
||||
isActive: () => this.runningFeatures.has(featureId),
|
||||
};
|
||||
return context;
|
||||
}
|
||||
@@ -126,7 +126,11 @@ class AutoModeService {
|
||||
console.log(`[AutoMode] Running feature: ${feature.description}`);
|
||||
|
||||
// Update feature status to in_progress
|
||||
await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
"in_progress",
|
||||
projectPath
|
||||
);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_start",
|
||||
@@ -135,7 +139,12 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Implement the feature
|
||||
const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution);
|
||||
const result = await featureExecutor.implementFeature(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update feature status based on result
|
||||
// For skipTests features, go to waiting_approval on success instead of verified
|
||||
@@ -145,7 +154,11 @@ class AutoModeService {
|
||||
} else {
|
||||
newStatus = "backlog";
|
||||
}
|
||||
await featureLoader.updateFeatureStatus(feature.id, newStatus, projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
feature.id,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
@@ -214,11 +227,20 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Verify the feature by running tests
|
||||
const result = await featureVerifier.verifyFeatureTests(feature, projectPath, sendToRenderer, execution);
|
||||
const result = await featureVerifier.verifyFeatureTests(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update feature status based on result
|
||||
const newStatus = result.passes ? "verified" : "in_progress";
|
||||
await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file if verified
|
||||
if (newStatus === "verified") {
|
||||
@@ -287,10 +309,19 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Read existing context
|
||||
const previousContext = await contextManager.readContextFile(projectPath, featureId);
|
||||
const previousContext = await contextManager.readContextFile(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
|
||||
// Resume implementation with context
|
||||
const result = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution);
|
||||
const result = await featureExecutor.resumeFeatureWithContext(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
previousContext,
|
||||
execution
|
||||
);
|
||||
|
||||
// If the agent ends early without finishing, automatically re-run
|
||||
let attempts = 0;
|
||||
@@ -304,11 +335,16 @@ class AutoModeService {
|
||||
|
||||
if (updatedFeature && updatedFeature.status === "in_progress") {
|
||||
attempts++;
|
||||
console.log(`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`);
|
||||
console.log(
|
||||
`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`
|
||||
);
|
||||
|
||||
// Update context file with retry message
|
||||
await contextManager.writeToContextFile(projectPath, featureId,
|
||||
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`);
|
||||
await contextManager.writeToContextFile(
|
||||
projectPath,
|
||||
featureId,
|
||||
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`
|
||||
);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_progress",
|
||||
@@ -317,10 +353,19 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Read updated context
|
||||
const retryContext = await contextManager.readContextFile(projectPath, featureId);
|
||||
const retryContext = await contextManager.readContextFile(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
|
||||
// Resume again with full context
|
||||
finalResult = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext, execution);
|
||||
finalResult = await featureExecutor.resumeFeatureWithContext(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
retryContext,
|
||||
execution
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -334,7 +379,11 @@ class AutoModeService {
|
||||
} else {
|
||||
newStatus = "in_progress";
|
||||
}
|
||||
await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
@@ -389,7 +438,9 @@ class AutoModeService {
|
||||
|
||||
// Skip if this feature is already running (via manual trigger)
|
||||
if (this.runningFeatures.has(currentFeatureId)) {
|
||||
console.log(`[AutoMode] Skipping ${currentFeatureId} - already running`);
|
||||
console.log(
|
||||
`[AutoMode] Skipping ${currentFeatureId} - already running`
|
||||
);
|
||||
await this.sleep(3000);
|
||||
continue;
|
||||
}
|
||||
@@ -409,7 +460,12 @@ class AutoModeService {
|
||||
this.runningFeatures.set(currentFeatureId, execution);
|
||||
|
||||
// Implement the feature
|
||||
const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution);
|
||||
const result = await featureExecutor.implementFeature(
|
||||
nextFeature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update feature status based on result
|
||||
// For skipTests features, go to waiting_approval on success instead of verified
|
||||
@@ -419,7 +475,11 @@ class AutoModeService {
|
||||
} else {
|
||||
newStatus = "backlog";
|
||||
}
|
||||
await featureLoader.updateFeatureStatus(nextFeature.id, newStatus, projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
nextFeature.id,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
@@ -495,7 +555,12 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Perform the analysis
|
||||
const result = await projectAnalyzer.runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution);
|
||||
const result = await projectAnalyzer.runProjectAnalysis(
|
||||
projectPath,
|
||||
analysisId,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
@@ -543,13 +608,21 @@ class AutoModeService {
|
||||
* Follow-up on a feature with additional prompt
|
||||
* This continues work on a feature that's in waiting_approval status
|
||||
*/
|
||||
async followUpFeature({ projectPath, featureId, prompt, imagePaths, sendToRenderer }) {
|
||||
async followUpFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
sendToRenderer,
|
||||
}) {
|
||||
// Check if this feature is already running
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
throw new Error(`Feature ${featureId} is already running`);
|
||||
}
|
||||
|
||||
console.log(`[AutoMode] Follow-up on feature: ${featureId} with prompt: ${prompt}`);
|
||||
console.log(
|
||||
`[AutoMode] Follow-up on feature: ${featureId} with prompt: ${prompt}`
|
||||
);
|
||||
|
||||
// Register this feature as running
|
||||
const execution = this.createExecutionContext(featureId);
|
||||
@@ -559,7 +632,14 @@ class AutoModeService {
|
||||
|
||||
// Start the async work in the background (don't await)
|
||||
// This allows the API to return immediately so the modal can close
|
||||
this.runFollowUpWork({ projectPath, featureId, prompt, imagePaths, sendToRenderer, execution }).catch((error) => {
|
||||
this.runFollowUpWork({
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
sendToRenderer,
|
||||
execution,
|
||||
}).catch((error) => {
|
||||
console.error("[AutoMode] Follow-up work error:", error);
|
||||
this.runningFeatures.delete(featureId);
|
||||
});
|
||||
@@ -571,7 +651,14 @@ class AutoModeService {
|
||||
/**
|
||||
* Internal method to run follow-up work asynchronously
|
||||
*/
|
||||
async runFollowUpWork({ projectPath, featureId, prompt, imagePaths, sendToRenderer, execution }) {
|
||||
async runFollowUpWork({
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
sendToRenderer,
|
||||
execution,
|
||||
}) {
|
||||
try {
|
||||
// Load features
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
@@ -584,7 +671,11 @@ class AutoModeService {
|
||||
console.log(`[AutoMode] Following up on feature: ${feature.description}`);
|
||||
|
||||
// Update status to in_progress
|
||||
await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
"in_progress",
|
||||
projectPath
|
||||
);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_start",
|
||||
@@ -593,11 +684,18 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Read existing context and append follow-up prompt
|
||||
const previousContext = await contextManager.readContextFile(projectPath, featureId);
|
||||
const previousContext = await contextManager.readContextFile(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
|
||||
// Append follow-up prompt to context
|
||||
const followUpContext = `${previousContext}\n\n## Follow-up Instructions\n\n${prompt}`;
|
||||
await contextManager.writeToContextFile(projectPath, featureId, `\n\n## Follow-up Instructions\n\n${prompt}`);
|
||||
await contextManager.writeToContextFile(
|
||||
projectPath,
|
||||
featureId,
|
||||
`\n\n## Follow-up Instructions\n\n${prompt}`
|
||||
);
|
||||
|
||||
// Resume implementation with follow-up context and optional images
|
||||
const result = await featureExecutor.resumeFeatureWithContext(
|
||||
@@ -610,10 +708,16 @@ class AutoModeService {
|
||||
|
||||
// For skipTests features, go to waiting_approval on success instead of verified
|
||||
const newStatus = result.passes
|
||||
? (feature.skipTests ? "waiting_approval" : "verified")
|
||||
? feature.skipTests
|
||||
? "waiting_approval"
|
||||
: "verified"
|
||||
: "in_progress";
|
||||
|
||||
await featureLoader.updateFeatureStatus(feature.id, newStatus, projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
feature.id,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file if verified (only for non-skipTests)
|
||||
if (newStatus === "verified") {
|
||||
@@ -674,10 +778,19 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Run git commit via the agent
|
||||
const commitResult = await featureExecutor.commitChangesOnly(feature, projectPath, sendToRenderer, execution);
|
||||
const commitResult = await featureExecutor.commitChangesOnly(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update status to verified
|
||||
await featureLoader.updateFeatureStatus(featureId, "verified", projectPath);
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
"verified",
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file
|
||||
await contextManager.deleteContextFile(projectPath, featureId);
|
||||
|
||||
@@ -25,17 +25,25 @@ ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
|
||||
1. Read the project files to understand the current codebase structure
|
||||
2. Implement the feature according to the description and steps
|
||||
${feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"}
|
||||
${feature.skipTests ? "4" : "6"}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "7. Commit your changes with git"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
|
||||
}
|
||||
${
|
||||
feature.skipTests ? "4" : "6"
|
||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${
|
||||
feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "7. Commit your changes with git"
|
||||
}
|
||||
|
||||
**IMPORTANT - Updating Feature Status:**
|
||||
|
||||
When you have completed the feature${feature.skipTests ? "" : " and all tests pass"}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
||||
When you have completed the feature${
|
||||
feature.skipTests ? "" : " 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"
|
||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
||||
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
|
||||
@@ -51,7 +59,9 @@ When calling UpdateFeatureStatus, you MUST include a summary parameter that desc
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
UpdateFeatureStatus(featureId="${feature.id}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
UpdateFeatureStatus(featureId="${
|
||||
feature.id
|
||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
\`\`\`
|
||||
|
||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
||||
@@ -61,14 +71,18 @@ The summary will be displayed on the Kanban card so the user can see what was do
|
||||
- Focus ONLY on implementing this specific feature
|
||||
- Write clean, production-quality code
|
||||
- Add proper error handling
|
||||
${feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
||||
: "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
||||
: "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle"
|
||||
}
|
||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
|
||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
||||
${feature.skipTests
|
||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- Make a git commit when complete"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- Make a git commit when complete"
|
||||
}
|
||||
|
||||
**Testing Utilities (CRITICAL):**
|
||||
|
||||
@@ -119,9 +133,10 @@ ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
|
||||
1. Read the project files to understand the current implementation
|
||||
2. If the feature is not fully implemented, continue implementing it
|
||||
${feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: `3. Write or update Playwright tests to verify the feature works correctly
|
||||
${
|
||||
feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: `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:**
|
||||
@@ -131,15 +146,22 @@ ${feature.skipTests
|
||||
- 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`}
|
||||
${feature.skipTests ? "4" : "8"}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git"}
|
||||
- **DELETE the test file(s) for this feature** - tests are only for immediate verification`
|
||||
}
|
||||
${
|
||||
feature.skipTests ? "4" : "8"
|
||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${
|
||||
feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git"
|
||||
}
|
||||
|
||||
**IMPORTANT - Updating Feature Status:**
|
||||
|
||||
When you have completed the feature${feature.skipTests ? "" : " and all tests pass"}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
||||
When you have completed the feature${
|
||||
feature.skipTests ? "" : " 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"
|
||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
||||
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
|
||||
@@ -155,7 +177,9 @@ When calling UpdateFeatureStatus, you MUST include a summary parameter that desc
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
UpdateFeatureStatus(featureId="${feature.id}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
UpdateFeatureStatus(featureId="${
|
||||
feature.id
|
||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
\`\`\`
|
||||
|
||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
||||
@@ -173,9 +197,11 @@ rm tests/[feature-name].spec.ts
|
||||
\`\`\`
|
||||
|
||||
**Important:**
|
||||
${feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues"
|
||||
}
|
||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
|
||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
||||
|
||||
@@ -211,17 +237,25 @@ 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
|
||||
${feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"}
|
||||
${feature.skipTests ? "4" : "6"}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "7. Commit your changes with git"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
||||
: "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
|
||||
}
|
||||
${
|
||||
feature.skipTests ? "4" : "6"
|
||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json
|
||||
${
|
||||
feature.skipTests
|
||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
||||
: "7. Commit your changes with git"
|
||||
}
|
||||
|
||||
**IMPORTANT - Updating Feature Status:**
|
||||
|
||||
When you have completed the feature${feature.skipTests ? "" : " and all tests pass"}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
||||
When you have completed the feature${
|
||||
feature.skipTests ? "" : " 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"
|
||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
||||
- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions
|
||||
@@ -237,7 +271,9 @@ When calling UpdateFeatureStatus, you MUST include a summary parameter that desc
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
UpdateFeatureStatus(featureId="${feature.id}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
UpdateFeatureStatus(featureId="${
|
||||
feature.id
|
||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
||||
\`\`\`
|
||||
|
||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
||||
@@ -247,14 +283,18 @@ The summary will be displayed on the Kanban card so the user can see what was do
|
||||
- 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
|
||||
${feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
||||
: "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
||||
: "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**"
|
||||
}
|
||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly**
|
||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
||||
${feature.skipTests
|
||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- Make a git commit when complete"}
|
||||
${
|
||||
feature.skipTests
|
||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
||||
: "- Make a git commit when complete"
|
||||
}
|
||||
|
||||
Begin by assessing what's been done and what remains to be completed.`;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased dark`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Toaster richColors position="top-right" />
|
||||
|
||||
@@ -41,17 +41,52 @@ export default function Home() {
|
||||
// Apply theme class to document
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove(
|
||||
"dark",
|
||||
"retro",
|
||||
"light",
|
||||
"dracula",
|
||||
"nord",
|
||||
"monokai",
|
||||
"tokyonight",
|
||||
"solarized",
|
||||
"gruvbox",
|
||||
"catppuccin",
|
||||
"onedark",
|
||||
"synthwave"
|
||||
);
|
||||
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else if (theme === "retro") {
|
||||
root.classList.add("retro");
|
||||
} else if (theme === "dracula") {
|
||||
root.classList.add("dracula");
|
||||
} else if (theme === "nord") {
|
||||
root.classList.add("nord");
|
||||
} else if (theme === "monokai") {
|
||||
root.classList.add("monokai");
|
||||
} else if (theme === "tokyonight") {
|
||||
root.classList.add("tokyonight");
|
||||
} else if (theme === "solarized") {
|
||||
root.classList.add("solarized");
|
||||
} else if (theme === "gruvbox") {
|
||||
root.classList.add("gruvbox");
|
||||
} else if (theme === "catppuccin") {
|
||||
root.classList.add("catppuccin");
|
||||
} else if (theme === "onedark") {
|
||||
root.classList.add("onedark");
|
||||
} else if (theme === "synthwave") {
|
||||
root.classList.add("synthwave");
|
||||
} else if (theme === "light") {
|
||||
root.classList.remove("dark");
|
||||
} else {
|
||||
root.classList.add("light");
|
||||
} else if (theme === "system") {
|
||||
// System theme
|
||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (isDark) {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
root.classList.add("light");
|
||||
}
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
@@ -264,7 +264,7 @@ export function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"flex-shrink-0 border-r border-white/10 bg-zinc-950/50 backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
|
||||
"flex-shrink-0 border-r border-sidebar-border bg-sidebar backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
|
||||
sidebarOpen ? "w-16 lg:w-60" : "w-16"
|
||||
)}
|
||||
data-testid="sidebar"
|
||||
@@ -272,7 +272,7 @@ export function Sidebar() {
|
||||
{/* Floating Collapse Toggle Button - Desktop only - At border intersection */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="hidden lg:flex absolute top-[68px] -right-3 z-[9999] group/toggle items-center justify-center w-6 h-6 rounded-full bg-zinc-800 border border-white/10 text-zinc-400 hover:text-white hover:bg-zinc-700 hover:border-white/20 transition-all shadow-lg titlebar-no-drag"
|
||||
className="hidden lg:flex absolute top-[68px] -right-3 z-9999 group/toggle items-center justify-center w-6 h-6 rounded-full bg-sidebar-accent border border-border text-muted-foreground hover:text-foreground hover:bg-accent hover:border-border transition-all shadow-lg titlebar-no-drag"
|
||||
data-testid="sidebar-collapse-button"
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
@@ -282,12 +282,12 @@ export function Sidebar() {
|
||||
)}
|
||||
{/* Tooltip */}
|
||||
<div
|
||||
className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover/toggle:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700 pointer-events-none"
|
||||
className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover/toggle:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border pointer-events-none"
|
||||
data-testid="sidebar-toggle-tooltip"
|
||||
>
|
||||
{sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "}
|
||||
<span
|
||||
className="ml-1 px-1 py-0.5 bg-white/10 rounded text-[10px] font-mono"
|
||||
className="ml-1 px-1 py-0.5 bg-sidebar-accent/10 rounded text-[10px] font-mono"
|
||||
data-testid="sidebar-toggle-shortcut"
|
||||
>
|
||||
{UI_SHORTCUTS.toggleSidebar}
|
||||
@@ -299,7 +299,7 @@ export function Sidebar() {
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={cn(
|
||||
"h-20 pt-8 flex items-center justify-center border-b border-zinc-800 flex-shrink-0 titlebar-drag-region",
|
||||
"h-20 pt-8 flex items-center justify-center border-b border-sidebar-border shrink-0 titlebar-drag-region",
|
||||
sidebarOpen ? "px-3 lg:px-6" : "px-3"
|
||||
)}
|
||||
>
|
||||
@@ -308,12 +308,12 @@ export function Sidebar() {
|
||||
onClick={() => setCurrentView("welcome")}
|
||||
data-testid="logo-button"
|
||||
>
|
||||
<div className="relative flex items-center justify-center w-8 h-8 bg-gradient-to-br from-brand-500 to-purple-600 rounded-lg shadow-lg shadow-brand-500/20 group">
|
||||
<Cpu className="text-white w-5 h-5 group-hover:rotate-12 transition-transform" />
|
||||
<div className="relative flex items-center justify-center w-8 h-8 bg-linear-to-br from-brand-500 to-brand-600 rounded-lg shadow-lg shadow-brand-500/20 group">
|
||||
<Cpu className="text-primary-foreground w-5 h-5 group-hover:rotate-12 transition-transform" />
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-3 font-bold text-white text-base tracking-tight",
|
||||
"ml-3 font-bold text-sidebar-foreground text-base tracking-tight",
|
||||
sidebarOpen ? "hidden lg:block" : "hidden"
|
||||
)}
|
||||
>
|
||||
@@ -327,7 +327,7 @@ export function Sidebar() {
|
||||
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3">
|
||||
<button
|
||||
onClick={() => setCurrentView("welcome")}
|
||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-zinc-400 hover:text-white hover:bg-white/5 border border-white/10"
|
||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
|
||||
title="New Project"
|
||||
data-testid="new-project-button"
|
||||
>
|
||||
@@ -338,11 +338,11 @@ export function Sidebar() {
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOpenFolder}
|
||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-zinc-400 hover:text-white hover:bg-white/5 border border-white/10"
|
||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
|
||||
title={`Open Folder (${ACTION_SHORTCUTS.openProject})`}
|
||||
data-testid="open-project-button"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 flex-shrink-0" />
|
||||
<FolderOpen className="w-4 h-4 shrink-0" />
|
||||
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
|
||||
Open
|
||||
</span>
|
||||
@@ -362,28 +362,28 @@ export function Sidebar() {
|
||||
>
|
||||
<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"
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-all text-foreground titlebar-no-drag"
|
||||
data-testid="project-selector"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Folder className="h-4 w-4 text-brand-500 flex-shrink-0" />
|
||||
<Folder className="h-4 w-4 text-brand-500 shrink-0" />
|
||||
<span className="text-sm font-medium truncate">
|
||||
{currentProject?.name || "Select Project"}
|
||||
</span>
|
||||
</div>
|
||||
<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"
|
||||
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
|
||||
data-testid="project-picker-shortcut"
|
||||
>
|
||||
{ACTION_SHORTCUTS.projectPicker}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-zinc-400 flex-shrink-0" />
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 bg-zinc-800 border-zinc-700"
|
||||
className="w-56 bg-popover border-border"
|
||||
align="start"
|
||||
data-testid="project-picker-dropdown"
|
||||
>
|
||||
@@ -394,12 +394,12 @@ export function Sidebar() {
|
||||
setCurrentProject(project);
|
||||
setIsProjectPickerOpen(false);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer text-zinc-300 hover:text-white hover:bg-zinc-700/50"
|
||||
className="flex items-center gap-2 cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
data-testid={`project-option-${project.id}`}
|
||||
>
|
||||
{index < 9 && (
|
||||
<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"
|
||||
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
|
||||
data-testid={`project-hotkey-${index + 1}`}
|
||||
>
|
||||
{index + 1}
|
||||
@@ -422,7 +422,7 @@ export function Sidebar() {
|
||||
{!currentProject && sidebarOpen ? (
|
||||
// Placeholder when no project is selected (only in expanded state)
|
||||
<div className="flex items-center justify-center h-full px-4">
|
||||
<p className="text-zinc-500 text-sm text-center">
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
<span className="hidden lg:block">
|
||||
Select or create a project above
|
||||
</span>
|
||||
@@ -435,13 +435,13 @@ export function Sidebar() {
|
||||
{/* Section Label */}
|
||||
{section.label && sidebarOpen && (
|
||||
<div className="hidden lg:block px-4 mb-2">
|
||||
<span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{section.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{section.label && !sidebarOpen && (
|
||||
<div className="h-px bg-zinc-800 mx-2 mb-2"></div>
|
||||
<div className="h-px bg-sidebar-border mx-2 mb-2"></div>
|
||||
)}
|
||||
|
||||
{/* Nav Items */}
|
||||
@@ -457,8 +457,8 @@ export function Sidebar() {
|
||||
className={cn(
|
||||
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
||||
isActive
|
||||
? "bg-white/5 text-white border border-white/10"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5",
|
||||
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
||||
!sidebarOpen && "justify-center"
|
||||
)}
|
||||
title={!sidebarOpen ? item.label : undefined}
|
||||
@@ -469,7 +469,7 @@ export function Sidebar() {
|
||||
)}
|
||||
<Icon
|
||||
className={cn(
|
||||
"w-4 h-4 flex-shrink-0 transition-colors",
|
||||
"w-4 h-4 shrink-0 transition-colors",
|
||||
isActive
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400"
|
||||
@@ -515,7 +515,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - User / Settings */}
|
||||
<div className="border-t border-zinc-800 bg-zinc-900/50 flex-shrink-0">
|
||||
<div className="border-t border-sidebar-border bg-sidebar-accent/10 shrink-0">
|
||||
{/* Settings Link */}
|
||||
<div className="p-2">
|
||||
<button
|
||||
@@ -523,8 +523,8 @@ export function Sidebar() {
|
||||
className={cn(
|
||||
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
|
||||
isActiveRoute("settings")
|
||||
? "bg-white/5 text-white border border-white/10"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5",
|
||||
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
|
||||
sidebarOpen ? "justify-start" : "justify-center"
|
||||
)}
|
||||
title={!sidebarOpen ? "Settings" : undefined}
|
||||
@@ -535,7 +535,7 @@ export function Sidebar() {
|
||||
)}
|
||||
<Settings
|
||||
className={cn(
|
||||
"w-4 h-4 flex-shrink-0 transition-colors",
|
||||
"w-4 h-4 shrink-0 transition-colors",
|
||||
isActiveRoute("settings")
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400"
|
||||
@@ -562,7 +562,7 @@ export function Sidebar() {
|
||||
</span>
|
||||
)}
|
||||
{!sidebarOpen && (
|
||||
<span className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700">
|
||||
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
|
||||
Settings
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
@@ -19,6 +19,8 @@ const buttonVariants = cva(
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
"animated-outline":
|
||||
"relative overflow-hidden rounded-xl hover:bg-transparent shadow-none",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
@@ -34,27 +36,60 @@ const buttonVariants = cva(
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
// Special handling for animated-outline variant
|
||||
if (variant === "animated-outline" && !asChild) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({ variant, size }),
|
||||
"p-[1px]", // Force 1px padding for the gradient border
|
||||
className
|
||||
)}
|
||||
data-slot="button"
|
||||
{...props}
|
||||
>
|
||||
{/* Animated rotating gradient border */}
|
||||
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] animated-outline-gradient" />
|
||||
|
||||
{/* Inner content container */}
|
||||
<span
|
||||
className={cn(
|
||||
"animated-outline-inner inline-flex h-full w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 py-1 text-sm font-medium backdrop-blur-3xl transition-all",
|
||||
size === "sm" && "px-3 text-xs gap-1.5",
|
||||
size === "lg" && "px-8",
|
||||
size === "icon" && "p-0 gap-0"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -16,10 +16,10 @@ const Slider = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-white/10">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gradient-to-r from-purple-600 to-blue-600" />
|
||||
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
|
||||
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-white/20 bg-zinc-800 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/50 disabled:pointer-events-none disabled:opacity-50 hover:bg-zinc-700" />
|
||||
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
@@ -213,7 +213,7 @@ export function AgentToolsView() {
|
||||
data-testid="agent-tools-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3 p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<Wrench className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Agent Tools</h1>
|
||||
|
||||
@@ -452,7 +452,7 @@ export function AgentView() {
|
||||
{/* Chat Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -503,7 +503,10 @@ export function AgentView() {
|
||||
|
||||
{/* Messages */}
|
||||
{!currentSessionId ? (
|
||||
<div className="flex-1 flex items-center justify-center" data-testid="no-session-placeholder">
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="no-session-placeholder"
|
||||
>
|
||||
<div className="text-center">
|
||||
<Bot className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||
<h2 className="text-lg font-semibold mb-2">
|
||||
|
||||
1119
app/src/components/views/analysis-view.tsx
Normal file
1119
app/src/components/views/analysis-view.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,11 +37,26 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
|
||||
case "error":
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
case "planning":
|
||||
return <ClipboardList className="w-4 h-4 text-cyan-500" data-testid="planning-phase-icon" />;
|
||||
return (
|
||||
<ClipboardList
|
||||
className="w-4 h-4 text-cyan-500"
|
||||
data-testid="planning-phase-icon"
|
||||
/>
|
||||
);
|
||||
case "action":
|
||||
return <Zap className="w-4 h-4 text-orange-500" data-testid="action-phase-icon" />;
|
||||
return (
|
||||
<Zap
|
||||
className="w-4 h-4 text-orange-500"
|
||||
data-testid="action-phase-icon"
|
||||
/>
|
||||
);
|
||||
case "verification":
|
||||
return <ShieldCheck className="w-4 h-4 text-emerald-500" data-testid="verification-phase-icon" />;
|
||||
return (
|
||||
<ShieldCheck
|
||||
className="w-4 h-4 text-emerald-500"
|
||||
data-testid="verification-phase-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,8 +95,8 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col border-white/10 bg-zinc-950/50 backdrop-blur-sm">
|
||||
<CardHeader className="p-4 border-b border-white/10 flex-shrink-0">
|
||||
<Card className="h-full flex flex-col border-border bg-card backdrop-blur-sm">
|
||||
<CardHeader className="p-4 border-b border-border flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
||||
@@ -127,12 +142,14 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
|
||||
<div
|
||||
key={activity.id}
|
||||
className={cn(
|
||||
"p-3 rounded-lg bg-zinc-900/50 border-l-4 hover:bg-zinc-900/70 transition-colors",
|
||||
"p-3 rounded-lg bg-secondary border-l-4 hover:bg-accent transition-colors",
|
||||
getActivityColor(activity.type)
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">{getActivityIcon(activity.type)}</div>
|
||||
<div className="mt-0.5">
|
||||
{getActivityIcon(activity.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
301
app/src/components/views/code-view.tsx
Normal file
301
app/src/components/views/code-view.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
RefreshCw,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FileTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileTreeNode[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
"node_modules",
|
||||
".git",
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
".DS_Store",
|
||||
"*.log",
|
||||
];
|
||||
|
||||
const shouldIgnore = (name: string) => {
|
||||
return IGNORE_PATTERNS.some((pattern) => {
|
||||
if (pattern.startsWith("*")) {
|
||||
return name.endsWith(pattern.slice(1));
|
||||
}
|
||||
return name === pattern;
|
||||
});
|
||||
};
|
||||
|
||||
export function CodeView() {
|
||||
const { currentProject } = useAppStore();
|
||||
const [fileTree, setFileTree] = useState<FileTreeNode[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
// Load directory tree
|
||||
const loadTree = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(currentProject.path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
const entries = result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
// Directories first
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${currentProject.path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
|
||||
setFileTree(entries);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file tree:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTree();
|
||||
}, [loadTree]);
|
||||
|
||||
// Load subdirectory
|
||||
const loadSubdirectory = async (path: string): Promise<FileTreeNode[]> => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
return result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load subdirectory:", error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Load file content
|
||||
const loadFileContent = async (path: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(path);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setFileContent(result.content);
|
||||
setSelectedFile(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = async (node: FileTreeNode) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
|
||||
if (expandedFolders.has(node.path)) {
|
||||
newExpanded.delete(node.path);
|
||||
} else {
|
||||
newExpanded.add(node.path);
|
||||
|
||||
// Load children if not already loaded
|
||||
if (!node.children) {
|
||||
const children = await loadSubdirectory(node.path);
|
||||
// Update the tree with children
|
||||
const updateTree = (nodes: FileTreeNode[]): FileTreeNode[] => {
|
||||
return nodes.map((n) => {
|
||||
if (n.path === node.path) {
|
||||
return { ...n, children };
|
||||
}
|
||||
if (n.children) {
|
||||
return { ...n, children: updateTree(n.children) };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
};
|
||||
setFileTree(updateTree(fileTree));
|
||||
}
|
||||
}
|
||||
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
// Render file tree node
|
||||
const renderNode = (node: FileTreeNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedFile === node.path;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50",
|
||||
isSelected && "bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => {
|
||||
if (node.isDirectory) {
|
||||
toggleFolder(node);
|
||||
} else {
|
||||
loadFileContent(node.path);
|
||||
}
|
||||
}}
|
||||
data-testid={`file-tree-item-${node.name}`}
|
||||
>
|
||||
{node.isDirectory ? (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<Folder className="w-4 h-4 text-primary shrink-0" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-4" />
|
||||
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm truncate">{node.name}</span>
|
||||
</div>
|
||||
{node.isDirectory && isExpanded && node.children && (
|
||||
<div>
|
||||
{node.children.map((child) => renderNode(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="code-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="code-view-loading"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="code-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<Code className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Code Explorer</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadTree}
|
||||
data-testid="refresh-tree"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Split View */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* File Tree */}
|
||||
<div className="w-64 border-r overflow-y-auto" data-testid="file-tree">
|
||||
<div className="p-2">{fileTree.map((node) => renderNode(node))}</div>
|
||||
</div>
|
||||
|
||||
{/* Code Preview */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selectedFile ? (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-4 py-2 border-b bg-muted/30">
|
||||
<p className="text-sm font-mono text-muted-foreground truncate">
|
||||
{selectedFile.replace(currentProject.path, "")}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="flex-1 m-4 overflow-hidden">
|
||||
<CardContent className="p-0 h-full">
|
||||
<pre className="p-4 h-full overflow-auto text-sm font-mono whitespace-pre-wrap">
|
||||
<code data-testid="code-content">{fileContent}</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-muted-foreground">
|
||||
Select a file to view its contents
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +53,9 @@ export function ContextView() {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState("");
|
||||
const [newFileType, setNewFileType] = useState<"text" | "image">("text");
|
||||
const [uploadedImageData, setUploadedImageData] = useState<string | null>(null);
|
||||
const [uploadedImageData, setUploadedImageData] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [newFileContent, setNewFileContent] = useState("");
|
||||
const [isDropHovering, setIsDropHovering] = useState(false);
|
||||
|
||||
@@ -78,7 +80,15 @@ export function ContextView() {
|
||||
|
||||
// Determine if a file is an image based on extension
|
||||
const isImageFile = (filename: string): boolean => {
|
||||
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"];
|
||||
const imageExtensions = [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".webp",
|
||||
".svg",
|
||||
".bmp",
|
||||
];
|
||||
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
|
||||
return imageExtensions.includes(ext);
|
||||
};
|
||||
@@ -270,7 +280,9 @@ export function ContextView() {
|
||||
};
|
||||
|
||||
// Handle drag and drop for .txt and .md files in the add context dialog textarea
|
||||
const handleTextAreaDrop = async (e: React.DragEvent<HTMLTextAreaElement>) => {
|
||||
const handleTextAreaDrop = async (
|
||||
e: React.DragEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDropHovering(false);
|
||||
@@ -282,8 +294,8 @@ export function ContextView() {
|
||||
const fileName = file.name.toLowerCase();
|
||||
|
||||
// Only accept .txt and .md files
|
||||
if (!fileName.endsWith('.txt') && !fileName.endsWith('.md')) {
|
||||
console.warn('Only .txt and .md files are supported for drag and drop');
|
||||
if (!fileName.endsWith(".txt") && !fileName.endsWith(".md")) {
|
||||
console.warn("Only .txt and .md files are supported for drag and drop");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -340,7 +352,7 @@ export function ContextView() {
|
||||
data-testid="context-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<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">
|
||||
<BookOpen className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
@@ -381,7 +393,10 @@ export function ContextView() {
|
||||
Context Files ({contextFiles.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2" data-testid="context-file-list">
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-2"
|
||||
data-testid="context-file-list"
|
||||
>
|
||||
{contextFiles.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
||||
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
|
||||
@@ -430,7 +445,9 @@ export function ContextView() {
|
||||
) : (
|
||||
<FileText className="w-4 h-4 text-zinc-400" />
|
||||
)}
|
||||
<span className="text-sm font-medium">{selectedFile.name}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{selectedFile.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedFile.type === "text" && (
|
||||
@@ -487,9 +504,7 @@ export function ContextView() {
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<File className="w-12 h-12 text-zinc-600 mx-auto mb-3" />
|
||||
<p className="text-zinc-500">
|
||||
Select a file to view or edit
|
||||
</p>
|
||||
<p className="text-zinc-500">Select a file to view or edit</p>
|
||||
<p className="text-zinc-600 text-sm mt-1">
|
||||
Or drop files here to add them
|
||||
</p>
|
||||
@@ -536,7 +551,9 @@ export function ContextView() {
|
||||
id="filename"
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
placeholder={newFileType === "text" ? "context.md" : "image.png"}
|
||||
placeholder={
|
||||
newFileType === "text" ? "context.md" : "image.png"
|
||||
}
|
||||
data-testid="new-file-name"
|
||||
/>
|
||||
</div>
|
||||
@@ -569,7 +586,9 @@ export function ContextView() {
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-brand-500/20 rounded-lg pointer-events-none">
|
||||
<div className="flex flex-col items-center text-brand-400">
|
||||
<Upload className="w-8 h-8 mb-2" />
|
||||
<span className="text-sm font-medium">Drop .txt or .md file here</span>
|
||||
<span className="text-sm font-medium">
|
||||
Drop .txt or .md file here
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -606,7 +625,9 @@ export function ContextView() {
|
||||
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
|
||||
)}
|
||||
<span className="text-sm text-zinc-400">
|
||||
{uploadedImageData ? "Click to change" : "Click to upload"}
|
||||
{uploadedImageData
|
||||
? "Click to change"
|
||||
: "Click to upload"}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -628,7 +649,10 @@ export function ContextView() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddFile}
|
||||
disabled={!newFileName.trim() || (newFileType === "image" && !uploadedImageData)}
|
||||
disabled={
|
||||
!newFileName.trim() ||
|
||||
(newFileType === "image" && !uploadedImageData)
|
||||
}
|
||||
data-testid="confirm-add-file"
|
||||
>
|
||||
Add File
|
||||
@@ -643,11 +667,15 @@ export function ContextView() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Context File</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete "{selectedFile?.name}"? This action cannot be undone.
|
||||
Are you sure you want to delete "{selectedFile?.name}"? This
|
||||
action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -357,7 +357,7 @@ export function InterviewView() {
|
||||
data-testid="interview-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -545,7 +545,7 @@ export function InterviewView() {
|
||||
<Button
|
||||
onClick={handleCreateProject}
|
||||
disabled={!projectName || !projectPath || isGenerating}
|
||||
className="w-full bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
className="w-full bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
|
||||
data-testid="interview-create-project"
|
||||
>
|
||||
{isGenerating ? (
|
||||
|
||||
@@ -97,9 +97,13 @@ export function KanbanCard({
|
||||
const { kanbanCardDetailLevel } = useAppStore();
|
||||
|
||||
// Helper functions to check what should be shown based on detail level
|
||||
const showSteps = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed";
|
||||
const showSteps =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
|
||||
// Load context file for in_progress, waiting_approval, and verified features
|
||||
useEffect(() => {
|
||||
@@ -164,8 +168,7 @@ export function KanbanCard({
|
||||
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
|
||||
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
|
||||
const isDraggable =
|
||||
feature.status === "backlog" ||
|
||||
(feature.skipTests && !isCurrentAutoTask);
|
||||
feature.status === "backlog" || (feature.skipTests && !isCurrentAutoTask);
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -188,7 +191,7 @@ export function KanbanCard({
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-white/10 relative",
|
||||
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-border relative",
|
||||
isDragging && "opacity-50 scale-105 shadow-lg",
|
||||
isCurrentAutoTask &&
|
||||
"border-purple-500 border-2 shadow-purple-500/50 shadow-lg animate-pulse"
|
||||
@@ -199,7 +202,7 @@ export function KanbanCard({
|
||||
{/* Shortcut key badge for in-progress cards */}
|
||||
{shortcutKey && (
|
||||
<div
|
||||
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20 text-zinc-300 z-10"
|
||||
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-muted border border-border text-muted-foreground z-10"
|
||||
data-testid={`shortcut-key-${feature.id}`}
|
||||
>
|
||||
{shortcutKey}
|
||||
@@ -293,19 +296,27 @@ export function KanbanCard({
|
||||
|
||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||
{/* Standard mode: Only show progress bar */}
|
||||
{showProgressBar && !showAgentInfo && feature.status !== "backlog" && agentInfo && (isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{ transform: `translateX(${agentInfo.progressPercentage - 100}%)` }}
|
||||
/>
|
||||
{showProgressBar &&
|
||||
!showAgentInfo &&
|
||||
feature.status !== "backlog" &&
|
||||
agentInfo &&
|
||||
(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Detailed mode: Show all agent info */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
@@ -314,15 +325,22 @@ export function KanbanCard({
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="flex items-center gap-1 text-cyan-400">
|
||||
<Cpu className="w-3 h-3" />
|
||||
<span className="font-medium">{formatModelName(DEFAULT_MODEL)}</span>
|
||||
<span className="font-medium">
|
||||
{formatModelName(DEFAULT_MODEL)}
|
||||
</span>
|
||||
</div>
|
||||
{agentInfo.currentPhase && (
|
||||
<div className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" && "bg-blue-500/20 text-blue-400",
|
||||
agentInfo.currentPhase === "action" && "bg-amber-500/20 text-amber-400",
|
||||
agentInfo.currentPhase === "verification" && "bg-green-500/20 text-green-400"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" &&
|
||||
"bg-blue-500/20 text-blue-400",
|
||||
agentInfo.currentPhase === "action" &&
|
||||
"bg-amber-500/20 text-amber-400",
|
||||
agentInfo.currentPhase === "verification" &&
|
||||
"bg-green-500/20 text-green-400"
|
||||
)}
|
||||
>
|
||||
{agentInfo.currentPhase}
|
||||
</div>
|
||||
)}
|
||||
@@ -334,7 +352,11 @@ export function KanbanCard({
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{ transform: `translateX(${agentInfo.progressPercentage - 100}%)` }}
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
@@ -344,7 +366,10 @@ export function KanbanCard({
|
||||
{agentInfo.toolCallCount} tools
|
||||
</span>
|
||||
{agentInfo.lastToolUsed && (
|
||||
<span className="text-zinc-500 truncate max-w-[80px]" title={agentInfo.lastToolUsed}>
|
||||
<span
|
||||
className="text-zinc-500 truncate max-w-[80px]"
|
||||
title={agentInfo.lastToolUsed}
|
||||
>
|
||||
{agentInfo.lastToolUsed}
|
||||
</span>
|
||||
)}
|
||||
@@ -360,7 +385,11 @@ export function KanbanCard({
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
<span>
|
||||
{agentInfo.todos.filter(t => t.status === "completed").length}/{agentInfo.todos.length} tasks
|
||||
{
|
||||
agentInfo.todos.filter((t) => t.status === "completed")
|
||||
.length
|
||||
}
|
||||
/{agentInfo.todos.length} tasks
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5 max-h-16 overflow-y-auto">
|
||||
@@ -376,12 +405,15 @@ export function KanbanCard({
|
||||
) : (
|
||||
<Circle className="w-2.5 h-2.5 text-zinc-500 shrink-0" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"truncate",
|
||||
todo.status === "completed" && "text-zinc-500 line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
todo.status === "pending" && "text-zinc-400"
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
todo.status === "completed" &&
|
||||
"text-zinc-500 line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
todo.status === "pending" && "text-zinc-400"
|
||||
)}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</div>
|
||||
@@ -396,7 +428,8 @@ export function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */}
|
||||
{(feature.status === "waiting_approval" || feature.status === "verified") && (
|
||||
{(feature.status === "waiting_approval" ||
|
||||
feature.status === "verified") && (
|
||||
<>
|
||||
{(feature.summary || summary || agentInfo.summary) && (
|
||||
<div className="space-y-1 pt-1 border-t border-white/5">
|
||||
@@ -423,20 +456,28 @@ export function KanbanCard({
|
||||
</div>
|
||||
)}
|
||||
{/* Show tool count even without summary */}
|
||||
{!feature.summary && !summary && !agentInfo.summary && agentInfo.toolCallCount > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-white/5">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
{agentInfo.todos.length > 0 && (
|
||||
{!feature.summary &&
|
||||
!summary &&
|
||||
!agentInfo.summary &&
|
||||
agentInfo.toolCallCount > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-white/5">
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
{agentInfo.todos.filter(t => t.status === "completed").length} tasks done
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
{
|
||||
agentInfo.todos.filter(
|
||||
(t) => t.status === "completed"
|
||||
).length
|
||||
}{" "}
|
||||
tasks done
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -672,7 +713,8 @@ export function KanbanCard({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Feature</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this feature? This action cannot be undone.
|
||||
Are you sure you want to delete this feature? This action cannot
|
||||
be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -713,7 +755,10 @@ export function KanbanCard({
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-zinc-900/50 rounded-lg border border-white/10">
|
||||
<Markdown>
|
||||
{feature.summary || summary || agentInfo?.summary || "No summary available"}
|
||||
{feature.summary ||
|
||||
summary ||
|
||||
agentInfo?.summary ||
|
||||
"No summary available"}
|
||||
</Markdown>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -29,14 +29,14 @@ export function KanbanColumn({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"flex flex-col h-full rounded-lg bg-zinc-900/50 backdrop-blur-sm border border-white/5 transition-colors",
|
||||
"flex flex-col h-full rounded-lg bg-card backdrop-blur-sm border border-border transition-colors",
|
||||
isDoubleWidth ? "w-[37rem]" : "w-72",
|
||||
isOver && "bg-zinc-800/50"
|
||||
isOver && "bg-accent"
|
||||
)}
|
||||
data-testid={`kanban-column-${id}`}
|
||||
>
|
||||
{/* Column Header */}
|
||||
<div className="flex items-center gap-2 p-3 border-b border-white/5">
|
||||
<div className="flex items-center gap-2 p-3 border-b border-border">
|
||||
<div className={cn("w-3 h-3 rounded-full", color)} />
|
||||
<h3 className="font-medium text-sm flex-1">{title}</h3>
|
||||
{headerAction}
|
||||
|
||||
@@ -5,21 +5,60 @@ import { useAppStore } from "@/store/app-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, LayoutGrid, Minimize2, Square, Maximize2, Terminal } from "lucide-react";
|
||||
import {
|
||||
Settings,
|
||||
Key,
|
||||
Eye,
|
||||
EyeOff,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Zap,
|
||||
Sun,
|
||||
Moon,
|
||||
Palette,
|
||||
Terminal,
|
||||
Ghost,
|
||||
Snowflake,
|
||||
Flame,
|
||||
Sparkles,
|
||||
Eclipse,
|
||||
Trees,
|
||||
Cat,
|
||||
Atom,
|
||||
Radio,
|
||||
LayoutGrid,
|
||||
Minimize2,
|
||||
Square,
|
||||
Maximize2,
|
||||
} from "lucide-react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
export function SettingsView() {
|
||||
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel } = useAppStore();
|
||||
const {
|
||||
apiKeys,
|
||||
setApiKeys,
|
||||
setCurrentView,
|
||||
theme,
|
||||
setTheme,
|
||||
kanbanCardDetailLevel,
|
||||
setKanbanCardDetailLevel,
|
||||
} = useAppStore();
|
||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
||||
const [showGoogleKey, setShowGoogleKey] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [testResult, setTestResult] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
|
||||
const [geminiTestResult, setGeminiTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [geminiTestResult, setGeminiTestResult] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [claudeCliStatus, setClaudeCliStatus] = useState<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
@@ -72,12 +111,21 @@ export function SettingsView() {
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setTestResult({ success: true, message: data.message || "Connection successful! Claude responded." });
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: data.message || "Connection successful! Claude responded.",
|
||||
});
|
||||
} else {
|
||||
setTestResult({ success: false, message: data.error || "Failed to connect to Claude API." });
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: data.error || "Failed to connect to Claude API.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setTestResult({ success: false, message: "Network error. Please check your connection." });
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: "Network error. Please check your connection.",
|
||||
});
|
||||
} finally {
|
||||
setTestingConnection(false);
|
||||
}
|
||||
@@ -99,12 +147,21 @@ export function SettingsView() {
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setGeminiTestResult({ success: true, message: data.message || "Connection successful! Gemini responded." });
|
||||
setGeminiTestResult({
|
||||
success: true,
|
||||
message: data.message || "Connection successful! Gemini responded.",
|
||||
});
|
||||
} else {
|
||||
setGeminiTestResult({ success: false, message: data.error || "Failed to connect to Gemini API." });
|
||||
setGeminiTestResult({
|
||||
success: false,
|
||||
message: data.error || "Failed to connect to Gemini API.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setGeminiTestResult({ success: false, message: "Network error. Please check your connection." });
|
||||
setGeminiTestResult({
|
||||
success: false,
|
||||
message: "Network error. Please check your connection.",
|
||||
});
|
||||
} finally {
|
||||
setTestingGeminiConnection(false);
|
||||
}
|
||||
@@ -120,17 +177,22 @@ export function SettingsView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="settings-view"
|
||||
>
|
||||
{/* Header Section */}
|
||||
<div className="flex-shrink-0 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<Settings className="w-5 h-5 text-white" />
|
||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<Settings className="w-5 h-5 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Settings</h1>
|
||||
<p className="text-sm text-zinc-400">Configure your API keys and preferences</p>
|
||||
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your API keys and preferences
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,25 +202,28 @@ export function SettingsView() {
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* API Keys Section */}
|
||||
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Key className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-white">API Keys</h2>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
API Keys
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Configure your AI provider API keys. Keys are stored locally in your browser.
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your AI provider API keys. Keys are stored locally in
|
||||
your browser.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Claude/Anthropic API Key */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="anthropic-key" className="text-zinc-300">
|
||||
<Label htmlFor="anthropic-key" className="text-foreground">
|
||||
Anthropic API Key (Claude)
|
||||
</Label>
|
||||
{apiKeys.anthropic && (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
<CheckCircle2 className="w-4 h-4 text-brand-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -169,14 +234,14 @@ export function SettingsView() {
|
||||
value={anthropicKey}
|
||||
onChange={(e) => setAnthropicKey(e.target.value)}
|
||||
placeholder="sk-ant-..."
|
||||
className="pr-10 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
|
||||
className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground"
|
||||
data-testid="anthropic-api-key-input"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full px-3 text-zinc-400 hover:text-white hover:bg-transparent"
|
||||
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
|
||||
onClick={() => setShowAnthropicKey(!showAnthropicKey)}
|
||||
data-testid="toggle-anthropic-visibility"
|
||||
>
|
||||
@@ -192,7 +257,7 @@ export function SettingsView() {
|
||||
variant="secondary"
|
||||
onClick={handleTestConnection}
|
||||
disabled={!anthropicKey || testingConnection}
|
||||
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
|
||||
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
|
||||
data-testid="test-claude-connection"
|
||||
>
|
||||
{testingConnection ? (
|
||||
@@ -218,14 +283,15 @@ export function SettingsView() {
|
||||
>
|
||||
console.anthropic.com
|
||||
</a>
|
||||
. Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.
|
||||
. Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment
|
||||
variable can be used.
|
||||
</p>
|
||||
{testResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 rounded-lg ${
|
||||
testResult.success
|
||||
? 'bg-green-500/10 border border-green-500/20 text-green-400'
|
||||
: 'bg-red-500/10 border border-red-500/20 text-red-400'
|
||||
? "bg-green-500/10 border border-green-500/20 text-green-400"
|
||||
: "bg-red-500/10 border border-red-500/20 text-red-400"
|
||||
}`}
|
||||
data-testid="test-connection-result"
|
||||
>
|
||||
@@ -234,7 +300,12 @@ export function SettingsView() {
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
)}
|
||||
<span className="text-sm" data-testid="test-connection-message">{testResult.message}</span>
|
||||
<span
|
||||
className="text-sm"
|
||||
data-testid="test-connection-message"
|
||||
>
|
||||
{testResult.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -242,11 +313,11 @@ export function SettingsView() {
|
||||
{/* Google API Key */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="google-key" className="text-zinc-300">
|
||||
<Label htmlFor="google-key" className="text-foreground">
|
||||
Google API Key (Gemini)
|
||||
</Label>
|
||||
{apiKeys.google && (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
<CheckCircle2 className="w-4 h-4 text-brand-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -257,14 +328,14 @@ export function SettingsView() {
|
||||
value={googleKey}
|
||||
onChange={(e) => setGoogleKey(e.target.value)}
|
||||
placeholder="AIza..."
|
||||
className="pr-10 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
|
||||
className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground"
|
||||
data-testid="google-api-key-input"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full px-3 text-zinc-400 hover:text-white hover:bg-transparent"
|
||||
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
|
||||
onClick={() => setShowGoogleKey(!showGoogleKey)}
|
||||
data-testid="toggle-google-visibility"
|
||||
>
|
||||
@@ -280,7 +351,7 @@ export function SettingsView() {
|
||||
variant="secondary"
|
||||
onClick={handleTestGeminiConnection}
|
||||
disabled={!googleKey || testingGeminiConnection}
|
||||
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
|
||||
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
|
||||
data-testid="test-gemini-connection"
|
||||
>
|
||||
{testingGeminiConnection ? (
|
||||
@@ -297,7 +368,8 @@ export function SettingsView() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500">
|
||||
Used for Gemini AI features (including image/design prompts). Get your key at{" "}
|
||||
Used for Gemini AI features (including image/design prompts).
|
||||
Get your key at{" "}
|
||||
<a
|
||||
href="https://makersuite.google.com/app/apikey"
|
||||
target="_blank"
|
||||
@@ -311,8 +383,8 @@ export function SettingsView() {
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 rounded-lg ${
|
||||
geminiTestResult.success
|
||||
? 'bg-green-500/10 border border-green-500/20 text-green-400'
|
||||
: 'bg-red-500/10 border border-red-500/20 text-red-400'
|
||||
? "bg-green-500/10 border border-green-500/20 text-green-400"
|
||||
: "bg-red-500/10 border border-red-500/20 text-red-400"
|
||||
}`}
|
||||
data-testid="gemini-test-connection-result"
|
||||
>
|
||||
@@ -321,7 +393,12 @@ export function SettingsView() {
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
)}
|
||||
<span className="text-sm" data-testid="gemini-test-connection-message">{geminiTestResult.message}</span>
|
||||
<span
|
||||
className="text-sm"
|
||||
data-testid="gemini-test-connection-message"
|
||||
>
|
||||
{geminiTestResult.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -332,8 +409,8 @@ export function SettingsView() {
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-yellow-500">Security Notice</p>
|
||||
<p className="text-yellow-500/80 text-xs mt-1">
|
||||
API keys are stored in your browser's local storage. Never share your API keys
|
||||
or commit them to version control.
|
||||
API keys are stored in your browser's local storage. Never
|
||||
share your API keys or commit them to version control.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,43 +498,165 @@ export function SettingsView() {
|
||||
)}
|
||||
|
||||
{/* Appearance Section */}
|
||||
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Palette className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Appearance</h2>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Appearance
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Customize the look and feel of your application.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-zinc-300">Theme</Label>
|
||||
<div className="flex gap-3">
|
||||
<Label className="text-foreground">Theme</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<button
|
||||
onClick={() => setTheme("dark")}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "dark"
|
||||
? "bg-white/5 border-brand-500 text-white"
|
||||
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="dark-mode-button"
|
||||
>
|
||||
<Moon className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Dark Mode</span>
|
||||
<span className="font-medium text-sm">Dark</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("light")}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "light"
|
||||
? "bg-white/5 border-brand-500 text-white"
|
||||
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="light-mode-button"
|
||||
>
|
||||
<Sun className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Light Mode</span>
|
||||
<span className="font-medium text-sm">Light</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("retro")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "retro"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="retro-mode-button"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Retro</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("dracula")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "dracula"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="dracula-mode-button"
|
||||
>
|
||||
<Ghost className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Dracula</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("nord")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "nord"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="nord-mode-button"
|
||||
>
|
||||
<Snowflake className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Nord</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("monokai")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "monokai"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="monokai-mode-button"
|
||||
>
|
||||
<Flame className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Monokai</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("tokyonight")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "tokyonight"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="tokyonight-mode-button"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Tokyo Night</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("solarized")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "solarized"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="solarized-mode-button"
|
||||
>
|
||||
<Eclipse className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Solarized</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("gruvbox")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "gruvbox"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="gruvbox-mode-button"
|
||||
>
|
||||
<Trees className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Gruvbox</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("catppuccin")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "catppuccin"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="catppuccin-mode-button"
|
||||
>
|
||||
<Cat className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Catppuccin</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("onedark")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "onedark"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="onedark-mode-button"
|
||||
>
|
||||
<Atom className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">One Dark</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("synthwave")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
theme === "synthwave"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="synthwave-mode-button"
|
||||
>
|
||||
<Radio className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Synthwave</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,7 +668,9 @@ export function SettingsView() {
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<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-white">Kanban Card Display</h2>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Kanban Card Display
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Control how much information is displayed on Kanban cards.
|
||||
@@ -490,7 +691,9 @@ export function SettingsView() {
|
||||
>
|
||||
<Minimize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Minimal</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Title & category only</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Title & category only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("standard")}
|
||||
@@ -503,7 +706,9 @@ export function SettingsView() {
|
||||
>
|
||||
<Square className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Standard</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Steps & progress</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Steps & progress
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("detailed")}
|
||||
@@ -516,13 +721,18 @@ export function SettingsView() {
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Detailed</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Model, tools & tasks</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Model, tools & tasks
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500">
|
||||
<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
|
||||
<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>
|
||||
@@ -533,7 +743,7 @@ export function SettingsView() {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
data-testid="save-settings"
|
||||
className="min-w-[120px] bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
|
||||
>
|
||||
{saved ? (
|
||||
<>
|
||||
|
||||
@@ -20,7 +20,9 @@ export function SpecView() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
|
||||
const result = await api.readFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`
|
||||
);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setAppSpec(result.content);
|
||||
@@ -44,7 +46,10 @@ export function SpecView() {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
await api.writeFile(`${currentProject.path}/.automaker/app_spec.txt`, appSpec);
|
||||
await api.writeFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`,
|
||||
appSpec
|
||||
);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save spec:", error);
|
||||
@@ -86,7 +91,7 @@ export function SpecView() {
|
||||
data-testid="spec-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<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>
|
||||
|
||||
@@ -91,59 +91,65 @@ export function WelcomeView() {
|
||||
/**
|
||||
* Initialize project and optionally kick off project analysis agent
|
||||
*/
|
||||
const initializeAndOpenProject = useCallback(async (path: string, name: string) => {
|
||||
setIsOpening(true);
|
||||
try {
|
||||
// Initialize the .automaker directory structure
|
||||
const initResult = await initializeProject(path);
|
||||
const initializeAndOpenProject = useCallback(
|
||||
async (path: string, name: string) => {
|
||||
setIsOpening(true);
|
||||
try {
|
||||
// Initialize the .automaker directory structure
|
||||
const initResult = await initializeProject(path);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const project = {
|
||||
id: `project-${Date.now()}`,
|
||||
name,
|
||||
path,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
addProject(project);
|
||||
setCurrentProject(project);
|
||||
|
||||
// Show initialization dialog if files were created
|
||||
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
||||
setInitStatus({
|
||||
isNewProject: initResult.isNewProject,
|
||||
createdFiles: initResult.createdFiles,
|
||||
projectName: name,
|
||||
projectPath: path,
|
||||
});
|
||||
setShowInitDialog(true);
|
||||
|
||||
// Kick off agent to analyze the project and update app_spec.txt
|
||||
console.log(
|
||||
"[Welcome] Project initialized, created files:",
|
||||
initResult.createdFiles
|
||||
);
|
||||
console.log("[Welcome] Kicking off project analysis agent...");
|
||||
|
||||
// Start analysis in background (don't await, let it run async)
|
||||
analyzeProject(path);
|
||||
} else {
|
||||
toast.success("Project opened", {
|
||||
description: `Opened ${name}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Welcome] Failed to open project:", error);
|
||||
toast.error("Failed to open project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
setIsOpening(false);
|
||||
}
|
||||
|
||||
const project = {
|
||||
id: `project-${Date.now()}`,
|
||||
name,
|
||||
path,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
addProject(project);
|
||||
setCurrentProject(project);
|
||||
|
||||
// Show initialization dialog if files were created
|
||||
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
||||
setInitStatus({
|
||||
isNewProject: initResult.isNewProject,
|
||||
createdFiles: initResult.createdFiles,
|
||||
projectName: name,
|
||||
projectPath: path,
|
||||
});
|
||||
setShowInitDialog(true);
|
||||
|
||||
// Kick off agent to analyze the project and update app_spec.txt
|
||||
console.log("[Welcome] Project initialized, created files:", initResult.createdFiles);
|
||||
console.log("[Welcome] Kicking off project analysis agent...");
|
||||
|
||||
// Start analysis in background (don't await, let it run async)
|
||||
analyzeProject(path);
|
||||
} else {
|
||||
toast.success("Project opened", {
|
||||
description: `Opened ${name}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Welcome] Failed to open project:", error);
|
||||
toast.error("Failed to open project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsOpening(false);
|
||||
}
|
||||
}, [addProject, setCurrentProject, analyzeProject]);
|
||||
},
|
||||
[addProject, setCurrentProject, analyzeProject]
|
||||
);
|
||||
|
||||
const handleOpenProject = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
@@ -159,9 +165,12 @@ export function WelcomeView() {
|
||||
/**
|
||||
* Handle clicking on a recent project
|
||||
*/
|
||||
const handleRecentProjectClick = useCallback(async (project: { id: string; name: string; path: string }) => {
|
||||
await initializeAndOpenProject(project.path, project.name);
|
||||
}, [initializeAndOpenProject]);
|
||||
const handleRecentProjectClick = useCallback(
|
||||
async (project: { id: string; name: string; path: string }) => {
|
||||
await initializeAndOpenProject(project.path, project.name);
|
||||
},
|
||||
[initializeAndOpenProject]
|
||||
);
|
||||
|
||||
const handleNewProject = () => {
|
||||
setNewProjectName("");
|
||||
@@ -272,17 +281,17 @@ export function WelcomeView() {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col content-bg" data-testid="welcome-view">
|
||||
{/* Header Section */}
|
||||
<div className="flex-shrink-0 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<Cpu className="w-5 h-5 text-white" />
|
||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<Cpu className="w-5 h-5 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Welcome to Automaker
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your autonomous AI development studio
|
||||
</p>
|
||||
</div>
|
||||
@@ -296,20 +305,20 @@ export function WelcomeView() {
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
|
||||
<div
|
||||
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-white/20 transition-all duration-200"
|
||||
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200"
|
||||
data-testid="new-project-card"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/5 to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div className="relative p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<div className="w-12 h-12 rounded-lg bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
New Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a new project from scratch with AI-powered
|
||||
development
|
||||
</p>
|
||||
@@ -318,7 +327,7 @@ export function WelcomeView() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
className="w-full bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
|
||||
data-testid="create-new-project"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
@@ -347,28 +356,28 @@ export function WelcomeView() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-white/20 transition-all duration-200 cursor-pointer"
|
||||
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200 cursor-pointer"
|
||||
onClick={handleOpenProject}
|
||||
data-testid="open-project-card"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div className="relative p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-zinc-800 border border-white/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<FolderOpen className="w-6 h-6 text-zinc-400 group-hover:text-white transition-colors" />
|
||||
<div className="w-12 h-12 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
Open Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Open an existing project folder to continue working
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white/5 hover:bg-white/10 text-white border border-white/10 hover:border-white/20"
|
||||
className="w-full bg-secondary hover:bg-secondary/80 text-foreground border border-border hover:border-border-glass"
|
||||
data-testid="open-existing-project"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 mr-2" />
|
||||
@@ -382,8 +391,8 @@ export function WelcomeView() {
|
||||
{recentProjects.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="w-5 h-5 text-zinc-400" />
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Recent Projects
|
||||
</h2>
|
||||
</div>
|
||||
@@ -391,25 +400,25 @@ export function WelcomeView() {
|
||||
{recentProjects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
|
||||
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
|
||||
onClick={() => handleRecentProjectClick(project)}
|
||||
data-testid={`recent-project-${project.id}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all"></div>
|
||||
<div className="relative p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-zinc-800 border border-white/10 flex items-center justify-center group-hover:border-brand-500/50 transition-colors">
|
||||
<Folder className="w-5 h-5 text-zinc-400 group-hover:text-brand-500 transition-colors" />
|
||||
<div className="w-10 h-10 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:border-brand-500/50 transition-colors">
|
||||
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-white truncate group-hover:text-brand-500 transition-colors">
|
||||
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 truncate mt-0.5">
|
||||
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
|
||||
{project.path}
|
||||
</p>
|
||||
{project.lastOpened && (
|
||||
<p className="text-xs text-zinc-600 mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{new Date(
|
||||
project.lastOpened
|
||||
).toLocaleDateString()}
|
||||
@@ -427,10 +436,10 @@ export function WelcomeView() {
|
||||
{/* Empty State for No Projects */}
|
||||
{recentProjects.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-zinc-900/50 border border-white/10 flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-8 h-8 text-zinc-600" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-muted border border-border flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
No projects yet
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400 max-w-md">
|
||||
@@ -447,18 +456,20 @@ export function WelcomeView() {
|
||||
onOpenChange={setShowNewProjectDialog}
|
||||
>
|
||||
<DialogContent
|
||||
className="bg-zinc-900 border-white/10"
|
||||
className="bg-card border-border"
|
||||
data-testid="new-project-dialog"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Create New Project</DialogTitle>
|
||||
<DialogDescription className="text-zinc-400">
|
||||
<DialogTitle className="text-foreground">
|
||||
Create New Project
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Set up a new project directory with initial configuration files.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="project-name" className="text-zinc-300">
|
||||
<Label htmlFor="project-name" className="text-foreground">
|
||||
Project Name
|
||||
</Label>
|
||||
<Input
|
||||
@@ -466,12 +477,12 @@ export function WelcomeView() {
|
||||
placeholder="my-awesome-project"
|
||||
value={newProjectName}
|
||||
onChange={(e) => setNewProjectName(e.target.value)}
|
||||
className="bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
|
||||
className="bg-input border-border text-foreground placeholder:text-muted-foreground"
|
||||
data-testid="project-name-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="project-path" className="text-zinc-300">
|
||||
<Label htmlFor="project-path" className="text-foreground">
|
||||
Parent Directory
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
@@ -480,13 +491,13 @@ export function WelcomeView() {
|
||||
placeholder="/path/to/projects"
|
||||
value={newProjectPath}
|
||||
onChange={(e) => setNewProjectPath(e.target.value)}
|
||||
className="flex-1 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
|
||||
className="flex-1 bg-input border-border text-foreground placeholder:text-muted-foreground"
|
||||
data-testid="project-path-input"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleSelectDirectory}
|
||||
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
|
||||
className="bg-secondary hover:bg-secondary/80 text-foreground border border-border"
|
||||
data-testid="browse-directory"
|
||||
>
|
||||
Browse
|
||||
@@ -498,14 +509,14 @@ export function WelcomeView() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setShowNewProjectDialog(false)}
|
||||
className="text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
className="text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateProject}
|
||||
disabled={!newProjectName || !newProjectPath || isCreating}
|
||||
className="bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
||||
data-testid="confirm-create-project"
|
||||
>
|
||||
{isCreating ? "Creating..." : "Create Project"}
|
||||
@@ -517,15 +528,17 @@ export function WelcomeView() {
|
||||
{/* Project Initialization Dialog */}
|
||||
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
|
||||
<DialogContent
|
||||
className="bg-zinc-900 border-white/10"
|
||||
className="bg-card border-border"
|
||||
data-testid="project-init-dialog"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<DialogTitle className="text-foreground flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-brand-500" />
|
||||
{initStatus?.isNewProject ? "Project Initialized" : "Project Updated"}
|
||||
{initStatus?.isNewProject
|
||||
? "Project Initialized"
|
||||
: "Project Updated"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-zinc-400">
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
{initStatus?.isNewProject
|
||||
? `Created .automaker directory structure for ${initStatus?.projectName}`
|
||||
: `Updated missing files in .automaker for ${initStatus?.projectName}`}
|
||||
@@ -533,15 +546,17 @@ export function WelcomeView() {
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-zinc-300 font-medium">Created files:</p>
|
||||
<p className="text-sm text-foreground font-medium">
|
||||
Created files:
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{initStatus?.createdFiles.map((file) => (
|
||||
<li
|
||||
key={file}
|
||||
className="flex items-center gap-2 text-sm text-zinc-400"
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||
<code className="text-xs bg-zinc-800 px-2 py-0.5 rounded">
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
{file}
|
||||
</code>
|
||||
</li>
|
||||
@@ -550,7 +565,7 @@ export function WelcomeView() {
|
||||
</div>
|
||||
|
||||
{initStatus?.isNewProject && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-zinc-800/50 border border-white/5">
|
||||
<div className="mt-4 p-3 rounded-lg bg-muted/50 border border-border-glass">
|
||||
{isAnalyzing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 text-brand-500 animate-spin" />
|
||||
@@ -559,9 +574,9 @@ export function WelcomeView() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-brand-400">Tip:</span> Edit the{" "}
|
||||
<code className="text-xs bg-zinc-800 px-1.5 py-0.5 rounded">
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
app_spec.txt
|
||||
</code>{" "}
|
||||
file to describe your project. The AI agent will use this to
|
||||
@@ -574,7 +589,7 @@ export function WelcomeView() {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setShowInitDialog(false)}
|
||||
className="bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
||||
data-testid="close-init-dialog"
|
||||
>
|
||||
Get Started
|
||||
@@ -586,12 +601,14 @@ export function WelcomeView() {
|
||||
{/* Loading overlay when opening project */}
|
||||
{isOpening && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
|
||||
data-testid="project-opening-overlay"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-zinc-900 border border-white/10">
|
||||
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border">
|
||||
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
|
||||
<p className="text-white font-medium">Initializing project...</p>
|
||||
<p className="text-foreground font-medium">
|
||||
Initializing project...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -113,7 +113,9 @@ export function useAutoMode() {
|
||||
|
||||
case "auto_mode_phase":
|
||||
// Log phase transitions (Planning, Action, Verification)
|
||||
console.log(`[AutoMode] Phase: ${event.phase} for ${event.featureId}`);
|
||||
console.log(
|
||||
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: event.phase,
|
||||
@@ -125,7 +127,13 @@ export function useAutoMode() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [addRunningTask, removeRunningTask, clearRunningTasks, setAutoModeRunning, addAutoModeActivity]);
|
||||
}, [
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
setAutoModeRunning,
|
||||
addAutoModeActivity,
|
||||
]);
|
||||
|
||||
// Start auto mode
|
||||
const start = useCallback(async () => {
|
||||
@@ -181,33 +189,36 @@ export function useAutoMode() {
|
||||
}, [setAutoModeRunning, clearRunningTasks]);
|
||||
|
||||
// Stop a specific feature
|
||||
const stopFeature = useCallback(async (featureId: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stopFeature) {
|
||||
throw new Error("Stop feature API not available");
|
||||
}
|
||||
const stopFeature = useCallback(
|
||||
async (featureId: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stopFeature) {
|
||||
throw new Error("Stop feature API not available");
|
||||
}
|
||||
|
||||
const result = await api.autoMode.stopFeature(featureId);
|
||||
const result = await api.autoMode.stopFeature(featureId);
|
||||
|
||||
if (result.success) {
|
||||
removeRunningTask(featureId);
|
||||
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
||||
addAutoModeActivity({
|
||||
featureId,
|
||||
type: "complete",
|
||||
message: "Feature stopped by user",
|
||||
passes: false,
|
||||
});
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop feature:", result.error);
|
||||
throw new Error(result.error || "Failed to stop feature");
|
||||
if (result.success) {
|
||||
removeRunningTask(featureId);
|
||||
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
||||
addAutoModeActivity({
|
||||
featureId,
|
||||
type: "complete",
|
||||
message: "Feature stopped by user",
|
||||
passes: false,
|
||||
});
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop feature:", result.error);
|
||||
throw new Error(result.error || "Failed to stop feature");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error stopping feature:", error);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error stopping feature:", error);
|
||||
throw error;
|
||||
}
|
||||
}, [removeRunningTask, addAutoModeActivity]);
|
||||
},
|
||||
[removeRunningTask, addAutoModeActivity]
|
||||
);
|
||||
|
||||
return {
|
||||
isRunning: isAutoModeRunning,
|
||||
|
||||
@@ -2,8 +2,31 @@ import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { Project } from "@/lib/electron";
|
||||
|
||||
export type ViewMode = "welcome" | "spec" | "board" | "agent" | "settings" | "tools" | "interview" | "context";
|
||||
export type ThemeMode = "light" | "dark" | "system";
|
||||
export type ViewMode =
|
||||
| "welcome"
|
||||
| "spec"
|
||||
| "board"
|
||||
| "agent"
|
||||
| "settings"
|
||||
| "tools"
|
||||
| "interview"
|
||||
| "context";
|
||||
|
||||
export type ThemeMode =
|
||||
| "light"
|
||||
| "dark"
|
||||
| "system"
|
||||
| "retro"
|
||||
| "dracula"
|
||||
| "nord"
|
||||
| "monokai"
|
||||
| "tokyonight"
|
||||
| "solarized"
|
||||
| "gruvbox"
|
||||
| "catppuccin"
|
||||
| "onedark"
|
||||
| "synthwave";
|
||||
|
||||
export type KanbanCardDetailLevel = "minimal" | "standard" | "detailed";
|
||||
|
||||
export interface ApiKeys {
|
||||
@@ -116,7 +139,15 @@ export interface AutoModeActivity {
|
||||
id: string;
|
||||
featureId: string;
|
||||
timestamp: Date;
|
||||
type: "start" | "progress" | "tool" | "complete" | "error" | "planning" | "action" | "verification";
|
||||
type:
|
||||
| "start"
|
||||
| "progress"
|
||||
| "tool"
|
||||
| "complete"
|
||||
| "error"
|
||||
| "planning"
|
||||
| "action"
|
||||
| "verification";
|
||||
message: string;
|
||||
tool?: string;
|
||||
passes?: boolean;
|
||||
@@ -170,7 +201,9 @@ export interface AppActions {
|
||||
addRunningTask: (taskId: string) => void;
|
||||
removeRunningTask: (taskId: string) => void;
|
||||
clearRunningTasks: () => void;
|
||||
addAutoModeActivity: (activity: Omit<AutoModeActivity, "id" | "timestamp">) => void;
|
||||
addAutoModeActivity: (
|
||||
activity: Omit<AutoModeActivity, "id" | "timestamp">
|
||||
) => void;
|
||||
clearAutoModeActivity: () => void;
|
||||
setMaxConcurrency: (max: number) => void;
|
||||
|
||||
@@ -217,11 +250,17 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
const existing = projects.findIndex((p) => p.path === project.path);
|
||||
if (existing >= 0) {
|
||||
const updated = [...projects];
|
||||
updated[existing] = { ...project, lastOpened: new Date().toISOString() };
|
||||
updated[existing] = {
|
||||
...project,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
set({ projects: updated });
|
||||
} else {
|
||||
set({
|
||||
projects: [...projects, { ...project, lastOpened: new Date().toISOString() }],
|
||||
projects: [
|
||||
...projects,
|
||||
{ ...project, lastOpened: new Date().toISOString() },
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -259,7 +298,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
},
|
||||
|
||||
addFeature: (feature) => {
|
||||
const id = `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const id = `feature-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
set({ features: [...get().features, { ...feature, id }] });
|
||||
},
|
||||
|
||||
@@ -294,14 +335,19 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
const now = new Date();
|
||||
const session: ChatSession = {
|
||||
id: `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
title: title || `Chat ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
|
||||
title:
|
||||
title ||
|
||||
`Chat ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
|
||||
projectId: currentProject.id,
|
||||
messages: [{
|
||||
id: "welcome",
|
||||
role: "assistant",
|
||||
content: "Hello! I'm the Automaker Agent. I can help you build software autonomously. What would you like to create today?",
|
||||
timestamp: now,
|
||||
}],
|
||||
messages: [
|
||||
{
|
||||
id: "welcome",
|
||||
role: "assistant",
|
||||
content:
|
||||
"Hello! I'm the Automaker Agent. I can help you build software autonomously. What would you like to create today?",
|
||||
timestamp: now,
|
||||
},
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
archived: false,
|
||||
@@ -328,14 +374,18 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
const currentSession = get().currentChatSession;
|
||||
if (currentSession && currentSession.id === sessionId) {
|
||||
set({
|
||||
currentChatSession: { ...currentSession, ...updates, updatedAt: new Date() }
|
||||
currentChatSession: {
|
||||
...currentSession,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addMessageToSession: (sessionId, message) => {
|
||||
const sessions = get().chatSessions;
|
||||
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
|
||||
const sessionIndex = sessions.findIndex((s) => s.id === sessionId);
|
||||
|
||||
if (sessionIndex >= 0) {
|
||||
const updatedSessions = [...sessions];
|
||||
@@ -351,7 +401,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
const currentSession = get().currentChatSession;
|
||||
if (currentSession && currentSession.id === sessionId) {
|
||||
set({
|
||||
currentChatSession: updatedSessions[sessionIndex]
|
||||
currentChatSession: updatedSessions[sessionIndex],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -373,7 +423,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
const currentSession = get().currentChatSession;
|
||||
set({
|
||||
chatSessions: get().chatSessions.filter((s) => s.id !== sessionId),
|
||||
currentChatSession: currentSession?.id === sessionId ? null : currentSession,
|
||||
currentChatSession:
|
||||
currentSession?.id === sessionId ? null : currentSession,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -392,13 +443,19 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
},
|
||||
|
||||
removeRunningTask: (taskId) => {
|
||||
set({ runningAutoTasks: get().runningAutoTasks.filter(id => id !== taskId) });
|
||||
set({
|
||||
runningAutoTasks: get().runningAutoTasks.filter(
|
||||
(id) => id !== taskId
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
clearRunningTasks: () => set({ runningAutoTasks: [] }),
|
||||
|
||||
addAutoModeActivity: (activity) => {
|
||||
const id = `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const id = `activity-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
const newActivity: AutoModeActivity = {
|
||||
...activity,
|
||||
id,
|
||||
@@ -417,7 +474,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
setMaxConcurrency: (max) => set({ maxConcurrency: max }),
|
||||
|
||||
// Kanban Card Settings actions
|
||||
setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }),
|
||||
setKanbanCardDetailLevel: (level) =>
|
||||
set({ kanbanCardDetailLevel: level }),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState),
|
||||
|
||||
Reference in New Issue
Block a user