mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
feat(cli): implement Claude CLI detection and model selection for features
- Added a new service to detect the installation status of Claude Code CLI, providing users with installation recommendations and commands. - Integrated CLI detection into the SettingsView to inform users about the CLI status and its benefits for ultrathink tasks. - Enhanced feature creation and editing dialogs to allow users to select from multiple models (Haiku, Sonnet, Opus) and specify thinking levels (None, Low, Medium, High, Ultrathink). - Updated the feature executor to utilize the selected model and thinking configuration during task execution, improving flexibility and performance. This update enhances user experience by providing clearer options for model selection and ensuring optimal performance with the Claude CLI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
@@ -71,5 +71,22 @@
|
|||||||
"startedAt": "2025-12-09T22:31:41.946Z",
|
"startedAt": "2025-12-09T22:31:41.946Z",
|
||||||
"imagePaths": [],
|
"imagePaths": [],
|
||||||
"skipTests": true
|
"skipTests": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765321570899-oefrfast6",
|
||||||
|
"category": "Core",
|
||||||
|
"description": "I would like to have abbility to set correct model for new feautres. so inteas of only using claude opus we could use other models like sonnet or haiku for easier / light one tasks as well to add abbility how much thinking lvl we wanna use on each task as well",
|
||||||
|
"steps": [
|
||||||
|
"User add new feature",
|
||||||
|
"User Describe it",
|
||||||
|
"Select the automated testing or manual one",
|
||||||
|
"If the task is light / easy to implement he use lighter model from anthropi sdk such as sonnet / haiku",
|
||||||
|
"agent execute task with correct model "
|
||||||
|
],
|
||||||
|
"status": "verified",
|
||||||
|
"startedAt": "2025-12-09T23:07:37.223Z",
|
||||||
|
"imagePaths": [],
|
||||||
|
"skipTests": false,
|
||||||
|
"summary": "Added model selection (Haiku/Sonnet/Opus) and thinking level (None/Low/Medium/High) controls to feature creation and edit dialogs. Modified: app-store.ts (added AgentModel and ThinkingLevel types), board-view.tsx (UI controls), feature-executor.js (dynamic model/thinking config), feature-loader.js (field persistence). Agent now executes with user-selected model and extended thinking settings."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -551,3 +551,21 @@ ipcMain.handle("auto-mode:commit-feature", async (_, { projectPath, featureId })
|
|||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Claude CLI Detection IPC Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Claude Code CLI installation status
|
||||||
|
*/
|
||||||
|
ipcMain.handle("claude:check-cli", async () => {
|
||||||
|
try {
|
||||||
|
const claudeCliDetector = require("./services/claude-cli-detector");
|
||||||
|
const info = claudeCliDetector.getInstallationInfo();
|
||||||
|
return { success: true, ...info };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] claude:check-cli error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Claude CLI Detection API
|
||||||
|
checkClaudeCli: () => ipcRenderer.invoke("claude:check-cli"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also expose a flag to detect if we're in Electron
|
// Also expose a flag to detect if we're in Electron
|
||||||
|
|||||||
119
app/electron/services/claude-cli-detector.js
Normal file
119
app/electron/services/claude-cli-detector.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
class ClaudeCliDetector {
|
||||||
|
/**
|
||||||
|
* Check if Claude Code CLI is installed and accessible
|
||||||
|
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'sdk'|'none' }
|
||||||
|
*/
|
||||||
|
static detectClaudeInstallation() {
|
||||||
|
try {
|
||||||
|
// Method 1: Check if 'claude' command is in PATH
|
||||||
|
try {
|
||||||
|
const claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
|
||||||
|
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
path: claudePath,
|
||||||
|
version: version,
|
||||||
|
method: 'cli'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// CLI not in PATH, check local installation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check for local installation
|
||||||
|
const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude');
|
||||||
|
if (fs.existsSync(localClaudePath)) {
|
||||||
|
try {
|
||||||
|
const version = execSync(`${localClaudePath} --version`, { encoding: 'utf-8' }).trim();
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
path: localClaudePath,
|
||||||
|
version: version,
|
||||||
|
method: 'cli-local'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Local CLI exists but may not be executable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Check Windows path
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
try {
|
||||||
|
const claudePath = execSync('where claude', { encoding: 'utf-8' }).trim();
|
||||||
|
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
path: claudePath,
|
||||||
|
version: version,
|
||||||
|
method: 'cli'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: SDK mode (using OAuth token)
|
||||||
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
path: null,
|
||||||
|
version: 'SDK Mode',
|
||||||
|
method: 'sdk'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
path: null,
|
||||||
|
version: null,
|
||||||
|
method: 'none'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ClaudeCliDetector] Error detecting Claude installation:', error);
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
path: null,
|
||||||
|
version: null,
|
||||||
|
method: 'none',
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get installation recommendations
|
||||||
|
*/
|
||||||
|
static getInstallationInfo() {
|
||||||
|
const detection = this.detectClaudeInstallation();
|
||||||
|
|
||||||
|
if (detection.installed) {
|
||||||
|
return {
|
||||||
|
status: 'installed',
|
||||||
|
method: detection.method,
|
||||||
|
version: detection.version,
|
||||||
|
path: detection.path,
|
||||||
|
recommendation: detection.method === 'cli'
|
||||||
|
? 'Using Claude Code CLI - optimal for long-running tasks'
|
||||||
|
: 'Using SDK mode - works well but CLI may provide better performance'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'not_installed',
|
||||||
|
recommendation: 'Consider installing Claude Code CLI for better performance with ultrathink',
|
||||||
|
installCommands: {
|
||||||
|
macos: 'curl -fsSL claude.ai/install.sh | bash',
|
||||||
|
windows: 'irm https://claude.ai/install.ps1 | iex',
|
||||||
|
linux: 'curl -fsSL claude.ai/install.sh | bash',
|
||||||
|
npm: 'npm install -g @anthropic-ai/claude-code'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClaudeCliDetector;
|
||||||
|
|
||||||
@@ -4,10 +4,97 @@ const contextManager = require("./context-manager");
|
|||||||
const featureLoader = require("./feature-loader");
|
const featureLoader = require("./feature-loader");
|
||||||
const mcpServerFactory = require("./mcp-server-factory");
|
const mcpServerFactory = require("./mcp-server-factory");
|
||||||
|
|
||||||
|
// Model name mappings
|
||||||
|
const MODEL_MAP = {
|
||||||
|
haiku: "claude-haiku-4-20250514",
|
||||||
|
sonnet: "claude-sonnet-4-20250514",
|
||||||
|
opus: "claude-opus-4-5-20251101",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Thinking level to budget_tokens mapping
|
||||||
|
// These values control how much "thinking time" the model gets for extended thinking
|
||||||
|
const THINKING_BUDGET_MAP = {
|
||||||
|
none: null, // No extended thinking
|
||||||
|
low: 4096, // Light thinking
|
||||||
|
medium: 16384, // Moderate thinking
|
||||||
|
high: 65536, // Deep thinking
|
||||||
|
ultrathink: 262144, // Ultra-deep thinking (maximum reasoning)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature Executor - Handles feature implementation using Claude Agent SDK
|
* Feature Executor - Handles feature implementation using Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
class FeatureExecutor {
|
class FeatureExecutor {
|
||||||
|
/**
|
||||||
|
* Get the model string based on feature's model setting
|
||||||
|
*/
|
||||||
|
getModelString(feature) {
|
||||||
|
const modelKey = feature.model || "opus"; // Default to opus
|
||||||
|
return MODEL_MAP[modelKey] || MODEL_MAP.opus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get thinking configuration based on feature's thinkingLevel
|
||||||
|
*/
|
||||||
|
getThinkingConfig(feature) {
|
||||||
|
const level = feature.thinkingLevel || "none";
|
||||||
|
const budgetTokens = THINKING_BUDGET_MAP[level];
|
||||||
|
|
||||||
|
if (budgetTokens === null) {
|
||||||
|
return null; // No extended thinking
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "enabled",
|
||||||
|
budget_tokens: budgetTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare for ultrathink execution - validate and warn
|
||||||
|
*/
|
||||||
|
prepareForUltrathink(feature, thinkingConfig) {
|
||||||
|
if (feature.thinkingLevel !== 'ultrathink') {
|
||||||
|
return { ready: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = [];
|
||||||
|
const recommendations = [];
|
||||||
|
|
||||||
|
// Check CLI installation
|
||||||
|
const claudeCliDetector = require('./claude-cli-detector');
|
||||||
|
const cliInfo = claudeCliDetector.getInstallationInfo();
|
||||||
|
|
||||||
|
if (cliInfo.status === 'not_installed') {
|
||||||
|
warnings.push('Claude Code CLI not detected - ultrathink may have timeout issues');
|
||||||
|
recommendations.push('Install Claude Code CLI for optimal ultrathink performance');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate budget tokens
|
||||||
|
if (thinkingConfig && thinkingConfig.budget_tokens > 32000) {
|
||||||
|
warnings.push(`Ultrathink budget (${thinkingConfig.budget_tokens} tokens) exceeds recommended 32K - may cause long-running requests`);
|
||||||
|
recommendations.push('Consider using batch processing for budgets above 32K');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost estimate (rough)
|
||||||
|
const estimatedCost = (thinkingConfig?.budget_tokens || 0) / 1000 * 0.015; // Rough estimate
|
||||||
|
if (estimatedCost > 1.0) {
|
||||||
|
warnings.push(`Estimated cost: ~$${estimatedCost.toFixed(2)} per execution`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time estimate
|
||||||
|
warnings.push('Ultrathink tasks typically take 45-180 seconds');
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
warnings,
|
||||||
|
recommendations,
|
||||||
|
estimatedCost,
|
||||||
|
estimatedTime: '45-180 seconds',
|
||||||
|
cliInfo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sleep helper
|
* Sleep helper
|
||||||
*/
|
*/
|
||||||
@@ -46,9 +133,39 @@ class FeatureExecutor {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get model and thinking configuration from feature settings
|
||||||
|
const modelString = this.getModelString(feature);
|
||||||
|
const thinkingConfig = this.getThinkingConfig(feature);
|
||||||
|
|
||||||
|
// Prepare for ultrathink if needed
|
||||||
|
if (feature.thinkingLevel === 'ultrathink') {
|
||||||
|
const preparation = this.prepareForUltrathink(feature, thinkingConfig);
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation);
|
||||||
|
|
||||||
|
// Log warnings
|
||||||
|
if (preparation.warnings && preparation.warnings.length > 0) {
|
||||||
|
preparation.warnings.forEach(warning => {
|
||||||
|
console.warn(`[FeatureExecutor] ⚠️ ${warning}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send preparation info to renderer
|
||||||
|
sendToRenderer({
|
||||||
|
type: 'auto_mode_ultrathink_preparation',
|
||||||
|
featureId: feature.id,
|
||||||
|
warnings: preparation.warnings || [],
|
||||||
|
recommendations: preparation.recommendations || [],
|
||||||
|
estimatedCost: preparation.estimatedCost,
|
||||||
|
estimatedTime: preparation.estimatedTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Using model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`);
|
||||||
|
|
||||||
// Configure options for the SDK query
|
// Configure options for the SDK query
|
||||||
const options = {
|
const options = {
|
||||||
model: "claude-opus-4-5-20251101",
|
model: modelString,
|
||||||
systemPrompt: promptBuilder.getCodingPrompt(),
|
systemPrompt: promptBuilder.getCodingPrompt(),
|
||||||
maxTurns: 1000,
|
maxTurns: 1000,
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
@@ -74,6 +191,11 @@ class FeatureExecutor {
|
|||||||
abortController: abortController,
|
abortController: abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add thinking configuration if enabled
|
||||||
|
if (thinkingConfig) {
|
||||||
|
options.thinking = thinkingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
// Build the prompt for this specific feature
|
// Build the prompt for this specific feature
|
||||||
const prompt = promptBuilder.buildFeaturePrompt(feature);
|
const prompt = promptBuilder.buildFeaturePrompt(feature);
|
||||||
|
|
||||||
@@ -256,8 +378,38 @@ class FeatureExecutor {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get model and thinking configuration from feature settings
|
||||||
|
const modelString = this.getModelString(feature);
|
||||||
|
const thinkingConfig = this.getThinkingConfig(feature);
|
||||||
|
|
||||||
|
// Prepare for ultrathink if needed
|
||||||
|
if (feature.thinkingLevel === 'ultrathink') {
|
||||||
|
const preparation = this.prepareForUltrathink(feature, thinkingConfig);
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation);
|
||||||
|
|
||||||
|
// Log warnings
|
||||||
|
if (preparation.warnings && preparation.warnings.length > 0) {
|
||||||
|
preparation.warnings.forEach(warning => {
|
||||||
|
console.warn(`[FeatureExecutor] ⚠️ ${warning}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send preparation info to renderer
|
||||||
|
sendToRenderer({
|
||||||
|
type: 'auto_mode_ultrathink_preparation',
|
||||||
|
featureId: feature.id,
|
||||||
|
warnings: preparation.warnings || [],
|
||||||
|
recommendations: preparation.recommendations || [],
|
||||||
|
estimatedCost: preparation.estimatedCost,
|
||||||
|
estimatedTime: preparation.estimatedTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Resuming with model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
model: "claude-opus-4-5-20251101",
|
model: modelString,
|
||||||
systemPrompt: promptBuilder.getVerificationPrompt(),
|
systemPrompt: promptBuilder.getVerificationPrompt(),
|
||||||
maxTurns: 1000,
|
maxTurns: 1000,
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
@@ -273,6 +425,11 @@ class FeatureExecutor {
|
|||||||
abortController: abortController,
|
abortController: abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add thinking configuration if enabled
|
||||||
|
if (thinkingConfig) {
|
||||||
|
options.thinking = thinkingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
// Build prompt with previous context
|
// Build prompt with previous context
|
||||||
const prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
const prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ class FeatureLoader {
|
|||||||
if (f.summary !== undefined) {
|
if (f.summary !== undefined) {
|
||||||
featureData.summary = f.summary;
|
featureData.summary = f.summary;
|
||||||
}
|
}
|
||||||
|
if (f.model !== undefined) {
|
||||||
|
featureData.model = f.model;
|
||||||
|
}
|
||||||
|
if (f.thinkingLevel !== undefined) {
|
||||||
|
featureData.thinkingLevel = f.thinkingLevel;
|
||||||
|
}
|
||||||
return featureData;
|
return featureData;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ function DialogContent({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
|
compact = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean
|
||||||
|
compact?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
@@ -60,7 +62,8 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]",
|
||||||
|
compact ? "max-w-md p-4" : "sm:max-w-lg p-6",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -69,7 +72,10 @@ function DialogContent({
|
|||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className={cn(
|
||||||
|
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
compact ? "top-2 right-2" : "top-4 right-4"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Bug,
|
Bug,
|
||||||
Info,
|
Info,
|
||||||
FileOutput,
|
FileOutput,
|
||||||
|
Brain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +44,8 @@ const getLogIcon = (type: LogEntryType) => {
|
|||||||
return <CheckCircle2 className="w-4 h-4" />;
|
return <CheckCircle2 className="w-4 h-4" />;
|
||||||
case "warning":
|
case "warning":
|
||||||
return <AlertTriangle className="w-4 h-4" />;
|
return <AlertTriangle className="w-4 h-4" />;
|
||||||
|
case "thinking":
|
||||||
|
return <Brain className="w-4 h-4" />;
|
||||||
case "debug":
|
case "debug":
|
||||||
return <Bug className="w-4 h-4" />;
|
return <Bug className="w-4 h-4" />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { Loader2, List, FileText } from "lucide-react";
|
import { Loader2, List, FileText } from "lucide-react";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { LogViewer } from "@/components/ui/log-viewer";
|
import { LogViewer } from "@/components/ui/log-viewer";
|
||||||
|
import type { AutoModeEvent } from "@/types/electron";
|
||||||
|
|
||||||
interface AgentOutputModalProps {
|
interface AgentOutputModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -113,16 +114,18 @@ export function AgentOutputModal({
|
|||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode) return;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
||||||
// Filter events for this specific feature only
|
// Filter events for this specific feature only (skip events without featureId)
|
||||||
if (event.featureId !== featureId) {
|
if ("featureId" in event && event.featureId !== featureId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newContent = "";
|
let newContent = "";
|
||||||
|
|
||||||
if (event.type === "auto_mode_progress") {
|
switch (event.type) {
|
||||||
|
case "auto_mode_progress":
|
||||||
newContent = event.content || "";
|
newContent = event.content || "";
|
||||||
} else if (event.type === "auto_mode_tool") {
|
break;
|
||||||
|
case "auto_mode_tool":
|
||||||
const toolName = event.tool || "Unknown Tool";
|
const toolName = event.tool || "Unknown Tool";
|
||||||
const toolInput = event.input
|
const toolInput = event.input
|
||||||
? JSON.stringify(event.input, null, 2)
|
? JSON.stringify(event.input, null, 2)
|
||||||
@@ -130,7 +133,8 @@ export function AgentOutputModal({
|
|||||||
newContent = `\n🔧 Tool: ${toolName}\n${
|
newContent = `\n🔧 Tool: ${toolName}\n${
|
||||||
toolInput ? `Input: ${toolInput}` : ""
|
toolInput ? `Input: ${toolInput}` : ""
|
||||||
}`;
|
}`;
|
||||||
} else if (event.type === "auto_mode_phase") {
|
break;
|
||||||
|
case "auto_mode_phase":
|
||||||
const phaseEmoji =
|
const phaseEmoji =
|
||||||
event.phase === "planning"
|
event.phase === "planning"
|
||||||
? "📋"
|
? "📋"
|
||||||
@@ -138,9 +142,39 @@ export function AgentOutputModal({
|
|||||||
? "⚡"
|
? "⚡"
|
||||||
: "✅";
|
: "✅";
|
||||||
newContent = `\n${phaseEmoji} ${event.message}\n`;
|
newContent = `\n${phaseEmoji} ${event.message}\n`;
|
||||||
} else if (event.type === "auto_mode_error") {
|
break;
|
||||||
|
case "auto_mode_error":
|
||||||
newContent = `\n❌ Error: ${event.error}\n`;
|
newContent = `\n❌ Error: ${event.error}\n`;
|
||||||
} else if (event.type === "auto_mode_feature_complete") {
|
break;
|
||||||
|
case "auto_mode_ultrathink_preparation":
|
||||||
|
// Format thinking level preparation information
|
||||||
|
let prepContent = `\n🧠 Ultrathink Preparation\n`;
|
||||||
|
|
||||||
|
if (event.warnings && event.warnings.length > 0) {
|
||||||
|
prepContent += `\n⚠️ Warnings:\n`;
|
||||||
|
event.warnings.forEach((warning: string) => {
|
||||||
|
prepContent += ` • ${warning}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.recommendations && event.recommendations.length > 0) {
|
||||||
|
prepContent += `\n💡 Recommendations:\n`;
|
||||||
|
event.recommendations.forEach((rec: string) => {
|
||||||
|
prepContent += ` • ${rec}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.estimatedCost !== undefined) {
|
||||||
|
prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed(2)} per execution\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.estimatedTime) {
|
||||||
|
prepContent += `\n⏱️ Estimated Time: ${event.estimatedTime}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = prepContent;
|
||||||
|
break;
|
||||||
|
case "auto_mode_feature_complete":
|
||||||
const emoji = event.passes ? "✅" : "⚠️";
|
const emoji = event.passes ? "✅" : "⚠️";
|
||||||
newContent = `\n${emoji} Task completed: ${event.message}\n`;
|
newContent = `\n${emoji} Task completed: ${event.message}\n`;
|
||||||
|
|
||||||
@@ -151,6 +185,7 @@ export function AgentOutputModal({
|
|||||||
onClose();
|
onClose();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newContent) {
|
if (newContent) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
SortableContext,
|
SortableContext,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { useAppStore, Feature, FeatureImage, FeatureImagePath } from "@/store/app-store";
|
import { useAppStore, Feature, FeatureImage, FeatureImagePath, AgentModel, ThinkingLevel } from "@/store/app-store";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +44,7 @@ import { KanbanColumn } from "./kanban-column";
|
|||||||
import { KanbanCard } from "./kanban-card";
|
import { KanbanCard } from "./kanban-card";
|
||||||
import { AutoModeLog } from "./auto-mode-log";
|
import { AutoModeLog } from "./auto-mode-log";
|
||||||
import { AgentOutputModal } from "./agent-output-modal";
|
import { AgentOutputModal } from "./agent-output-modal";
|
||||||
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit } from "lucide-react";
|
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit, Brain, Zap } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
ACTION_SHORTCUTS,
|
ACTION_SHORTCUTS,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
|
import { useWindowState } from "@/hooks/use-window-state";
|
||||||
|
|
||||||
type ColumnId = Feature["status"];
|
type ColumnId = Feature["status"];
|
||||||
|
|
||||||
@@ -87,6 +88,8 @@ export function BoardView() {
|
|||||||
images: [] as FeatureImage[],
|
images: [] as FeatureImage[],
|
||||||
imagePaths: [] as DescriptionImagePath[],
|
imagePaths: [] as DescriptionImagePath[],
|
||||||
skipTests: false,
|
skipTests: false,
|
||||||
|
model: "opus" as AgentModel,
|
||||||
|
thinkingLevel: "none" as ThinkingLevel,
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
@@ -118,6 +121,9 @@ export function BoardView() {
|
|||||||
// Auto mode hook
|
// Auto mode hook
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
|
|
||||||
|
// Window state hook for compact dialog mode
|
||||||
|
const { isMaximized } = useWindowState();
|
||||||
|
|
||||||
// Get in-progress features for keyboard shortcuts (memoized for shortcuts)
|
// Get in-progress features for keyboard shortcuts (memoized for shortcuts)
|
||||||
const inProgressFeaturesForShortcuts = useMemo(() => {
|
const inProgressFeaturesForShortcuts = useMemo(() => {
|
||||||
return features.filter((f) => {
|
return features.filter((f) => {
|
||||||
@@ -405,6 +411,8 @@ export function BoardView() {
|
|||||||
imagePaths: f.imagePaths,
|
imagePaths: f.imagePaths,
|
||||||
skipTests: f.skipTests,
|
skipTests: f.skipTests,
|
||||||
summary: f.summary,
|
summary: f.summary,
|
||||||
|
model: f.model,
|
||||||
|
thinkingLevel: f.thinkingLevel,
|
||||||
}));
|
}));
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
`${currentProject.path}/.automaker/feature_list.json`,
|
`${currentProject.path}/.automaker/feature_list.json`,
|
||||||
@@ -531,10 +539,12 @@ export function BoardView() {
|
|||||||
images: newFeature.images,
|
images: newFeature.images,
|
||||||
imagePaths: newFeature.imagePaths,
|
imagePaths: newFeature.imagePaths,
|
||||||
skipTests: newFeature.skipTests,
|
skipTests: newFeature.skipTests,
|
||||||
|
model: newFeature.model,
|
||||||
|
thinkingLevel: newFeature.thinkingLevel,
|
||||||
});
|
});
|
||||||
// Persist the category
|
// Persist the category
|
||||||
saveCategory(category);
|
saveCategory(category);
|
||||||
setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false });
|
setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false, model: "opus", thinkingLevel: "none" });
|
||||||
setShowAddDialog(false);
|
setShowAddDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -546,6 +556,8 @@ export function BoardView() {
|
|||||||
description: editingFeature.description,
|
description: editingFeature.description,
|
||||||
steps: editingFeature.steps,
|
steps: editingFeature.steps,
|
||||||
skipTests: editingFeature.skipTests,
|
skipTests: editingFeature.skipTests,
|
||||||
|
model: editingFeature.model,
|
||||||
|
thinkingLevel: editingFeature.thinkingLevel,
|
||||||
});
|
});
|
||||||
// Persist the category if it's new
|
// Persist the category if it's new
|
||||||
if (editingFeature.category) {
|
if (editingFeature.category) {
|
||||||
@@ -1179,6 +1191,7 @@ export function BoardView() {
|
|||||||
{/* Add Feature Dialog */}
|
{/* Add Feature Dialog */}
|
||||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
compact={!isMaximized}
|
||||||
data-testid="add-feature-dialog"
|
data-testid="add-feature-dialog"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) {
|
||||||
@@ -1193,7 +1206,7 @@ export function BoardView() {
|
|||||||
Create a new feature card for the Kanban board.
|
Create a new feature card for the Kanban board.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="category">Category</Label>
|
<Label htmlFor="category">Category</Label>
|
||||||
<CategoryAutocomplete
|
<CategoryAutocomplete
|
||||||
@@ -1266,9 +1279,81 @@ export function BoardView() {
|
|||||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
When enabled, this feature will require manual verification instead of automated TDD.
|
When enabled, this feature will require manual verification instead of automated TDD.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Model Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Model
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(["haiku", "sonnet", "opus"] as AgentModel[]).map((model) => (
|
||||||
|
<button
|
||||||
|
key={model}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewFeature({ ...newFeature, model })}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
||||||
|
newFeature.model === model
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-background hover:bg-accent border-input"
|
||||||
|
)}
|
||||||
|
data-testid={`model-select-${model}`}
|
||||||
|
>
|
||||||
|
{model === "haiku" && "Haiku"}
|
||||||
|
{model === "sonnet" && "Sonnet"}
|
||||||
|
{model === "opus" && "Opus"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thinking Level */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Brain className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Thinking Level
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setNewFeature({ ...newFeature, thinkingLevel: level });
|
||||||
|
if (level === "ultrathink") {
|
||||||
|
toast.warning("Ultrathink Selected", {
|
||||||
|
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[80px]",
|
||||||
|
newFeature.thinkingLevel === level
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-background hover:bg-accent border-input"
|
||||||
|
)}
|
||||||
|
data-testid={`thinking-level-${level}`}
|
||||||
|
>
|
||||||
|
{level === "none" && "None"}
|
||||||
|
{level === "low" && "Low"}
|
||||||
|
{level === "medium" && "Med"}
|
||||||
|
{level === "high" && "High"}
|
||||||
|
{level === "ultrathink" && "Ultra"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Higher thinking levels give the model more time to reason through complex problems.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
|
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
|
||||||
@@ -1296,13 +1381,13 @@ export function BoardView() {
|
|||||||
open={!!editingFeature}
|
open={!!editingFeature}
|
||||||
onOpenChange={() => setEditingFeature(null)}
|
onOpenChange={() => setEditingFeature(null)}
|
||||||
>
|
>
|
||||||
<DialogContent data-testid="edit-feature-dialog">
|
<DialogContent compact={!isMaximized} data-testid="edit-feature-dialog">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit Feature</DialogTitle>
|
<DialogTitle>Edit Feature</DialogTitle>
|
||||||
<DialogDescription>Modify the feature details.</DialogDescription>
|
<DialogDescription>Modify the feature details.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{editingFeature && (
|
{editingFeature && (
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-category">Category</Label>
|
<Label htmlFor="edit-category">Category</Label>
|
||||||
<CategoryAutocomplete
|
<CategoryAutocomplete
|
||||||
@@ -1377,9 +1462,81 @@ export function BoardView() {
|
|||||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
When enabled, this feature will require manual verification instead of automated TDD.
|
When enabled, this feature will require manual verification instead of automated TDD.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Model Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Model
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(["haiku", "sonnet", "opus"] as AgentModel[]).map((model) => (
|
||||||
|
<button
|
||||||
|
key={model}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditingFeature({ ...editingFeature, model })}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
||||||
|
(editingFeature.model ?? "opus") === model
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-background hover:bg-accent border-input"
|
||||||
|
)}
|
||||||
|
data-testid={`edit-model-select-${model}`}
|
||||||
|
>
|
||||||
|
{model === "haiku" && "Haiku"}
|
||||||
|
{model === "sonnet" && "Sonnet"}
|
||||||
|
{model === "opus" && "Opus"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thinking Level */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Brain className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Thinking Level
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingFeature({ ...editingFeature, thinkingLevel: level });
|
||||||
|
if (level === "ultrathink") {
|
||||||
|
toast.warning("Ultrathink Selected", {
|
||||||
|
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[80px]",
|
||||||
|
(editingFeature.thinkingLevel ?? "none") === level
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-background hover:bg-accent border-input"
|
||||||
|
)}
|
||||||
|
data-testid={`edit-thinking-level-${level}`}
|
||||||
|
>
|
||||||
|
{level === "none" && "None"}
|
||||||
|
{level === "low" && "Low"}
|
||||||
|
{level === "medium" && "Med"}
|
||||||
|
{level === "high" && "High"}
|
||||||
|
{level === "ultrathink" && "Ultra"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Higher thinking levels give the model more time to reason through complex problems.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -1468,6 +1625,7 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
compact={!isMaximized}
|
||||||
data-testid="follow-up-dialog"
|
data-testid="follow-up-dialog"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && followUpPrompt.trim()) {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && followUpPrompt.trim()) {
|
||||||
@@ -1487,7 +1645,7 @@ export function BoardView() {
|
|||||||
)}
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="follow-up-prompt">Instructions</Label>
|
<Label htmlFor="follow-up-prompt">Instructions</Label>
|
||||||
<DescriptionImageDropZone
|
<DescriptionImageDropZone
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
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 } from "lucide-react";
|
import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, LayoutGrid, Minimize2, Square, Maximize2, Terminal } from "lucide-react";
|
||||||
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
|
|
||||||
export function SettingsView() {
|
export function SettingsView() {
|
||||||
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel } = useAppStore();
|
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel } = useAppStore();
|
||||||
@@ -19,12 +20,42 @@ export function SettingsView() {
|
|||||||
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 [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;
|
||||||
|
method?: string;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
recommendation?: string;
|
||||||
|
installCommands?: {
|
||||||
|
macos?: string;
|
||||||
|
windows?: string;
|
||||||
|
linux?: string;
|
||||||
|
npm?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAnthropicKey(apiKeys.anthropic);
|
setAnthropicKey(apiKeys.anthropic);
|
||||||
setGoogleKey(apiKeys.google);
|
setGoogleKey(apiKeys.google);
|
||||||
}, [apiKeys]);
|
}, [apiKeys]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCliStatus = async () => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api?.checkClaudeCli) {
|
||||||
|
try {
|
||||||
|
const status = await api.checkClaudeCli();
|
||||||
|
setClaudeCliStatus(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check Claude CLI status:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkCliStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
setTestingConnection(true);
|
setTestingConnection(true);
|
||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
@@ -309,6 +340,86 @@ export function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Claude CLI Status Section */}
|
||||||
|
{claudeCliStatus && (
|
||||||
|
<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="flex items-center gap-2 mb-2">
|
||||||
|
<Terminal className="w-5 h-5 text-brand-500" />
|
||||||
|
<h2 className="text-lg font-semibold text-white">Claude Code CLI</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Claude Code CLI provides better performance for long-running tasks, especially with ultrathink.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{claudeCliStatus.success && claudeCliStatus.status === 'installed' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-green-400">Claude Code CLI Installed</p>
|
||||||
|
<div className="text-xs text-green-400/80 mt-1 space-y-1">
|
||||||
|
{claudeCliStatus.method && (
|
||||||
|
<p>Method: <span className="font-mono">{claudeCliStatus.method}</span></p>
|
||||||
|
)}
|
||||||
|
{claudeCliStatus.version && (
|
||||||
|
<p>Version: <span className="font-mono">{claudeCliStatus.version}</span></p>
|
||||||
|
)}
|
||||||
|
{claudeCliStatus.path && (
|
||||||
|
<p className="truncate" title={claudeCliStatus.path}>
|
||||||
|
Path: <span className="font-mono text-[10px]">{claudeCliStatus.path}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{claudeCliStatus.recommendation && (
|
||||||
|
<p className="text-xs text-zinc-400">{claudeCliStatus.recommendation}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||||
|
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-yellow-400">Claude Code CLI Not Detected</p>
|
||||||
|
<p className="text-xs text-yellow-400/80 mt-1">
|
||||||
|
{claudeCliStatus.recommendation || 'Consider installing Claude Code CLI for optimal performance with ultrathink.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{claudeCliStatus.installCommands && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-zinc-300">Installation Commands:</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{claudeCliStatus.installCommands.npm && (
|
||||||
|
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
|
||||||
|
<p className="text-xs text-zinc-400 mb-1">npm:</p>
|
||||||
|
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.npm}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{claudeCliStatus.installCommands.macos && (
|
||||||
|
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
|
||||||
|
<p className="text-xs text-zinc-400 mb-1">macOS/Linux:</p>
|
||||||
|
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.macos}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{claudeCliStatus.installCommands.windows && (
|
||||||
|
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
|
||||||
|
<p className="text-xs text-zinc-400 mb-1">Windows (PowerShell):</p>
|
||||||
|
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.windows}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Appearance Section */}
|
{/* Appearance Section */}
|
||||||
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
|
<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="p-6 border-b border-white/10">
|
||||||
|
|||||||
54
app/src/hooks/use-window-state.ts
Normal file
54
app/src/hooks/use-window-state.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export interface WindowState {
|
||||||
|
isMaximized: boolean;
|
||||||
|
windowWidth: number;
|
||||||
|
windowHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to track window state (dimensions and maximized status)
|
||||||
|
* For Electron apps, considers window maximized if width > 1400px
|
||||||
|
* Also listens for window resize events to update state
|
||||||
|
*/
|
||||||
|
export function useWindowState(): WindowState {
|
||||||
|
const [windowState, setWindowState] = useState<WindowState>(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return { isMaximized: false, windowWidth: 0, windowHeight: 0 };
|
||||||
|
}
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
return {
|
||||||
|
isMaximized: width > 1400,
|
||||||
|
windowWidth: width,
|
||||||
|
windowHeight: height,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const updateWindowState = () => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
setWindowState({
|
||||||
|
isMaximized: width > 1400,
|
||||||
|
windowWidth: width,
|
||||||
|
windowHeight: height,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
updateWindowState();
|
||||||
|
|
||||||
|
// Listen for resize events
|
||||||
|
window.addEventListener("resize", updateWindowState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", updateWindowState);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return windowState;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -41,21 +41,8 @@ export interface StatResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto Mode types
|
// Auto Mode types - Import from electron.d.ts to avoid duplication
|
||||||
export type AutoModePhase = "planning" | "action" | "verification";
|
import type { AutoModeEvent } from "@/types/electron";
|
||||||
|
|
||||||
export interface AutoModeEvent {
|
|
||||||
type: "auto_mode_feature_start" | "auto_mode_progress" | "auto_mode_tool" | "auto_mode_feature_complete" | "auto_mode_error" | "auto_mode_complete" | "auto_mode_phase";
|
|
||||||
featureId?: string;
|
|
||||||
feature?: object;
|
|
||||||
content?: string;
|
|
||||||
tool?: string;
|
|
||||||
input?: unknown;
|
|
||||||
passes?: boolean;
|
|
||||||
message?: string;
|
|
||||||
error?: string;
|
|
||||||
phase?: AutoModePhase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
start: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
|
start: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
@@ -92,6 +79,21 @@ export interface ElectronAPI {
|
|||||||
getPath: (name: string) => Promise<string>;
|
getPath: (name: string) => Promise<string>;
|
||||||
saveImageToTemp?: (data: string, filename: string, mimeType: string) => Promise<SaveImageResult>;
|
saveImageToTemp?: (data: string, filename: string, mimeType: string) => Promise<SaveImageResult>;
|
||||||
autoMode?: AutoModeAPI;
|
autoMode?: AutoModeAPI;
|
||||||
|
checkClaudeCli?: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
status?: string;
|
||||||
|
method?: string;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
recommendation?: string;
|
||||||
|
installCommands?: {
|
||||||
|
macos?: string;
|
||||||
|
windows?: string;
|
||||||
|
linux?: string;
|
||||||
|
npm?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export type LogEntryType =
|
|||||||
| "success"
|
| "success"
|
||||||
| "info"
|
| "info"
|
||||||
| "debug"
|
| "debug"
|
||||||
| "warning";
|
| "warning"
|
||||||
|
| "thinking";
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -75,6 +76,18 @@ function detectEntryType(content: string): LogEntryType {
|
|||||||
return "warning";
|
return "warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thinking/Preparation info
|
||||||
|
if (
|
||||||
|
trimmed.toLowerCase().includes("ultrathink") ||
|
||||||
|
trimmed.toLowerCase().includes("thinking level") ||
|
||||||
|
trimmed.toLowerCase().includes("estimated cost") ||
|
||||||
|
trimmed.toLowerCase().includes("estimated time") ||
|
||||||
|
trimmed.toLowerCase().includes("budget tokens") ||
|
||||||
|
trimmed.match(/thinking.*preparation/i)
|
||||||
|
) {
|
||||||
|
return "thinking";
|
||||||
|
}
|
||||||
|
|
||||||
// Debug info (JSON, stack traces, etc.)
|
// Debug info (JSON, stack traces, etc.)
|
||||||
if (
|
if (
|
||||||
trimmed.startsWith("{") ||
|
trimmed.startsWith("{") ||
|
||||||
@@ -130,6 +143,8 @@ function generateTitle(type: LogEntryType, content: string): string {
|
|||||||
return "Success";
|
return "Success";
|
||||||
case "warning":
|
case "warning":
|
||||||
return "Warning";
|
return "Warning";
|
||||||
|
case "thinking":
|
||||||
|
return "Thinking Level";
|
||||||
case "debug":
|
case "debug":
|
||||||
return "Debug Info";
|
return "Debug Info";
|
||||||
case "prompt":
|
case "prompt":
|
||||||
@@ -180,6 +195,9 @@ export function parseLogOutput(rawOutput: string): LogEntry[] {
|
|||||||
trimmedLine.startsWith("✅") ||
|
trimmedLine.startsWith("✅") ||
|
||||||
trimmedLine.startsWith("❌") ||
|
trimmedLine.startsWith("❌") ||
|
||||||
trimmedLine.startsWith("⚠️") ||
|
trimmedLine.startsWith("⚠️") ||
|
||||||
|
trimmedLine.startsWith("🧠") ||
|
||||||
|
trimmedLine.toLowerCase().includes("ultrathink preparation") ||
|
||||||
|
trimmedLine.toLowerCase().includes("thinking level") ||
|
||||||
(trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call");
|
(trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call");
|
||||||
|
|
||||||
if (isNewEntry) {
|
if (isNewEntry) {
|
||||||
@@ -321,6 +339,14 @@ export function getLogTypeColors(type: LogEntryType): {
|
|||||||
icon: "text-orange-400",
|
icon: "text-orange-400",
|
||||||
badge: "bg-orange-500/20 text-orange-300",
|
badge: "bg-orange-500/20 text-orange-300",
|
||||||
};
|
};
|
||||||
|
case "thinking":
|
||||||
|
return {
|
||||||
|
bg: "bg-indigo-500/10",
|
||||||
|
border: "border-l-indigo-500",
|
||||||
|
text: "text-indigo-300",
|
||||||
|
icon: "text-indigo-400",
|
||||||
|
badge: "bg-indigo-500/20 text-indigo-300",
|
||||||
|
};
|
||||||
case "debug":
|
case "debug":
|
||||||
return {
|
return {
|
||||||
bg: "bg-purple-500/10",
|
bg: "bg-purple-500/10",
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export interface FeatureImagePath {
|
|||||||
mimeType: string;
|
mimeType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available models for feature execution
|
||||||
|
export type AgentModel = "opus" | "sonnet" | "haiku";
|
||||||
|
|
||||||
|
// Thinking level (budget_tokens) options
|
||||||
|
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
||||||
|
|
||||||
export interface Feature {
|
export interface Feature {
|
||||||
id: string;
|
id: string;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -63,6 +69,8 @@ export interface Feature {
|
|||||||
startedAt?: string; // ISO timestamp for when the card moved to in_progress
|
startedAt?: string; // ISO timestamp for when the card moved to in_progress
|
||||||
skipTests?: boolean; // When true, skip TDD approach and require manual verification
|
skipTests?: boolean; // When true, skip TDD approach and require manual verification
|
||||||
summary?: string; // Summary of what was done/modified by the agent
|
summary?: string; // Summary of what was done/modified by the agent
|
||||||
|
model?: AgentModel; // Model to use for this feature (defaults to opus)
|
||||||
|
thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
|
|||||||
25
app/src/types/electron.d.ts
vendored
25
app/src/types/electron.d.ts
vendored
@@ -195,6 +195,14 @@ export type AutoModeEvent =
|
|||||||
featureId: string;
|
featureId: string;
|
||||||
phase: "planning" | "action" | "verification";
|
phase: "planning" | "action" | "verification";
|
||||||
message: string;
|
message: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_ultrathink_preparation";
|
||||||
|
featureId: string;
|
||||||
|
warnings: string[];
|
||||||
|
recommendations: string[];
|
||||||
|
estimatedCost?: number;
|
||||||
|
estimatedTime?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
@@ -315,6 +323,23 @@ export interface ElectronAPI {
|
|||||||
|
|
||||||
// Auto Mode APIs
|
// Auto Mode APIs
|
||||||
autoMode: AutoModeAPI;
|
autoMode: AutoModeAPI;
|
||||||
|
|
||||||
|
// Claude CLI Detection API
|
||||||
|
checkClaudeCli: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
status?: string;
|
||||||
|
method?: string;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
recommendation?: string;
|
||||||
|
installCommands?: {
|
||||||
|
macos?: string;
|
||||||
|
windows?: string;
|
||||||
|
linux?: string;
|
||||||
|
npm?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
Reference in New Issue
Block a user