Merge remote-tracking branch 'origin/main' into feat/extend-models-support

This commit is contained in:
Kacper
2025-12-10 01:15:14 +01:00
24 changed files with 4102 additions and 701 deletions

View 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.

View File

@@ -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);

View File

@@ -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

View File

@@ -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" />

View File

@@ -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]);

View File

@@ -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>
)}

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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>

View File

@@ -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">

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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>
);
}

View File

@@ -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

View File

@@ -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 ? (

View File

@@ -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>

View File

@@ -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}

View File

@@ -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 ? (
<>

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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,

View File

@@ -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),