mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat(automaker): enhance feature management and UI components
- Updated `.gitignore` to include agent context files and user-uploaded images for better organization. - Added "Uncategorized" category to `categories.json` for improved feature classification. - Populated `feature_list.json` with new features, including detailed descriptions and image attachments for better context. - Changed application icon to `icon_gold.png` for a refreshed look. - Enhanced `AutoModeService` to support max concurrency and periodic checks, improving feature execution management. - Updated image handling in `DescriptionImageDropZone` to save images in the project directory. - Improved UI components with better styling and responsiveness, including drag-and-drop functionality for project management. This update significantly enhances the feature management process and user experience within the application.
This commit is contained in:
6
.automaker/.gitignore
vendored
6
.automaker/.gitignore
vendored
@@ -1,2 +1,8 @@
|
|||||||
# Backup files - these are created automatically by the UpdateFeatureStatus tool
|
# Backup files - these are created automatically by the UpdateFeatureStatus tool
|
||||||
feature_list.backup.json
|
feature_list.backup.json
|
||||||
|
|
||||||
|
# Agent context files - created during feature execution
|
||||||
|
agents-context/
|
||||||
|
|
||||||
|
# Attached images - uploaded by users as feature context
|
||||||
|
images/
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"Core",
|
"Core",
|
||||||
"Kanban",
|
"Kanban",
|
||||||
"Other",
|
"Other",
|
||||||
"Settings"
|
"Settings",
|
||||||
|
"Uncategorized"
|
||||||
]
|
]
|
||||||
@@ -1 +1,106 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"id": "feature-1765328064583-6zpz7ddil",
|
||||||
|
"category": "Kanban",
|
||||||
|
"description": "remove the auto mode activity panel completley.",
|
||||||
|
"steps": [],
|
||||||
|
"status": "waiting_approval",
|
||||||
|
"startedAt": "2025-12-10T00:55:21.540Z",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765328011980-j8d2r6b78",
|
||||||
|
"path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765328011979_Screenshot_2025-12-09_at_7.53.30_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 7.53.30 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true,
|
||||||
|
"summary": "Removed auto mode activity panel completely. Deleted: auto-mode-log.tsx. Modified: board-view.tsx - removed AutoModeLog import, showActivityLog state, activity toggle button, and activity panel rendering. Also removed unused cn import and ChevronUp/ChevronDown icons."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765330657132-oapdvbygc",
|
||||||
|
"category": "Uncategorized",
|
||||||
|
"description": "these buttons should be refactored to match more with selected theme, make sure they are set to use the button component variant styles",
|
||||||
|
"steps": [],
|
||||||
|
"status": "waiting_approval",
|
||||||
|
"startedAt": "2025-12-10T01:37:40.700Z",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765330619380-q9tu8blks",
|
||||||
|
"path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330619376_Screenshot_2025-12-09_at_8.36.56_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 8.36.56 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true,
|
||||||
|
"summary": "Refactored theme selector and kanban detail level buttons to use Button component variants. Modified: settings-view.tsx. Changed 12 theme buttons and 3 kanban detail buttons from raw <button> to <Button> with dynamic variant (secondary when selected, outline when unselected) and brand-500 ring highlight for selected state."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765330774043-35l9kw70q",
|
||||||
|
"category": "Kanban",
|
||||||
|
"description": "Increase the width of this modal and reduce font size of log output to make it easier to fit more output in modal",
|
||||||
|
"steps": [],
|
||||||
|
"status": "waiting_approval",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765330741800-jhmtz9ttc",
|
||||||
|
"path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330741799_Screenshot_2025-12-09_at_8.38.59_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 8.38.59 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true,
|
||||||
|
"summary": "Increased modal width from max-w-4xl to max-w-6xl and reduced log output font sizes from text-sm to text-xs. Modified: agent-output-modal.tsx (modal width + container font), log-viewer.tsx (log entry content + preview text fonts)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765330800921-uwy5iu3lp",
|
||||||
|
"category": "Uncategorized",
|
||||||
|
"description": "what color is the screenshot button? don't change code just answer.",
|
||||||
|
"steps": [],
|
||||||
|
"status": "waiting_approval",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765330783407-msplpgmwk",
|
||||||
|
"path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330783407_Screenshot_2025-12-09_at_8.39.40_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 8.39.40 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true,
|
||||||
|
"summary": "Answered question about screenshot button color. The image attachment button (Paperclip icon) is blue when active (bg-blue-100/text-blue-600 light, bg-blue-900/text-blue-400 dark) and uses standard outline styling when inactive. No code changes made."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765331813319-jzlk7eku2",
|
||||||
|
"category": "Uncategorized",
|
||||||
|
"description": "describe the attached image do not change code",
|
||||||
|
"steps": [],
|
||||||
|
"status": "verified",
|
||||||
|
"startedAt": "2025-12-10T02:02:54.785Z",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765331797511-v4ssc1hha",
|
||||||
|
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765331797510-ypiiz13rt_Screenshot_2025-12-09_at_8.56.34_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 8.56.34 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "feature-1765333165618-qmik9gy7p",
|
||||||
|
"category": "Uncategorized",
|
||||||
|
"description": "what is the text in the attache image say?",
|
||||||
|
"steps": [],
|
||||||
|
"status": "in_progress",
|
||||||
|
"startedAt": "2025-12-10T02:19:28.342Z",
|
||||||
|
"imagePaths": [
|
||||||
|
{
|
||||||
|
"id": "img-1765333155109-on4lk435f",
|
||||||
|
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765333155106-czd46vc93_Screenshot_2025-12-09_at_9.19.13_PM.png",
|
||||||
|
"filename": "Screenshot 2025-12-09 at 9.19.13 PM.png",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipTests": true
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -21,6 +21,9 @@ class AutoModeService {
|
|||||||
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
|
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
|
||||||
this.autoLoopRunning = false; // Separate flag for the auto loop
|
this.autoLoopRunning = false; // Separate flag for the auto loop
|
||||||
this.autoLoopAbortController = null;
|
this.autoLoopAbortController = null;
|
||||||
|
this.autoLoopInterval = null; // Timer for periodic checking
|
||||||
|
this.checkIntervalMs = 5000; // Check every 5 seconds
|
||||||
|
this.maxConcurrency = 3; // Default max concurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,20 +43,20 @@ class AutoModeService {
|
|||||||
/**
|
/**
|
||||||
* Start auto mode - continuously implement features
|
* Start auto mode - continuously implement features
|
||||||
*/
|
*/
|
||||||
async start({ projectPath, sendToRenderer }) {
|
async start({ projectPath, sendToRenderer, maxConcurrency }) {
|
||||||
if (this.autoLoopRunning) {
|
if (this.autoLoopRunning) {
|
||||||
throw new Error("Auto mode loop is already running");
|
throw new Error("Auto mode loop is already running");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoLoopRunning = true;
|
this.autoLoopRunning = true;
|
||||||
|
this.maxConcurrency = maxConcurrency || 3;
|
||||||
|
|
||||||
console.log("[AutoMode] Starting auto mode for project:", projectPath);
|
console.log(
|
||||||
|
`[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${this.maxConcurrency}`
|
||||||
|
);
|
||||||
|
|
||||||
// Run the autonomous loop
|
// Start the periodic checking loop
|
||||||
this.runLoop(projectPath, sendToRenderer).catch((error) => {
|
this.runPeriodicLoop(projectPath, sendToRenderer);
|
||||||
console.error("[AutoMode] Loop error:", error);
|
|
||||||
this.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,12 @@ class AutoModeService {
|
|||||||
|
|
||||||
this.autoLoopRunning = false;
|
this.autoLoopRunning = false;
|
||||||
|
|
||||||
|
// Clear the interval timer
|
||||||
|
if (this.autoLoopInterval) {
|
||||||
|
clearInterval(this.autoLoopInterval);
|
||||||
|
this.autoLoopInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Abort auto loop if running
|
// Abort auto loop if running
|
||||||
if (this.autoLoopAbortController) {
|
if (this.autoLoopAbortController) {
|
||||||
this.autoLoopAbortController.abort();
|
this.autoLoopAbortController.abort();
|
||||||
@@ -160,10 +169,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete context file only if verified (not for waiting_approval)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
if (newStatus === "verified") {
|
|
||||||
await contextManager.deleteContextFile(projectPath, feature.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
@@ -242,10 +248,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete context file if verified
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
if (newStatus === "verified") {
|
|
||||||
await contextManager.deleteContextFile(projectPath, featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
@@ -385,10 +388,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete context file only if verified (not for waiting_approval)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
if (newStatus === "verified") {
|
|
||||||
await contextManager.deleteContextFile(projectPath, featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
@@ -413,114 +413,146 @@ class AutoModeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main autonomous loop - picks and implements features
|
* New periodic loop - checks available slots and starts features up to max concurrency
|
||||||
|
* This loop continues running even if there are no backlog items
|
||||||
*/
|
*/
|
||||||
async runLoop(projectPath, sendToRenderer) {
|
runPeriodicLoop(projectPath, sendToRenderer) {
|
||||||
while (this.autoLoopRunning) {
|
console.log(
|
||||||
let currentFeatureId = null;
|
`[AutoMode] Starting periodic loop with interval: ${this.checkIntervalMs}ms`
|
||||||
try {
|
);
|
||||||
// Load features from .automaker/feature_list.json
|
|
||||||
const features = await featureLoader.loadFeatures(projectPath);
|
|
||||||
|
|
||||||
// Find highest priority incomplete feature
|
// Initial check immediately
|
||||||
const nextFeature = featureLoader.selectNextFeature(features);
|
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
||||||
|
|
||||||
if (!nextFeature) {
|
// Then check periodically
|
||||||
console.log("[AutoMode] No more features to implement");
|
this.autoLoopInterval = setInterval(() => {
|
||||||
sendToRenderer({
|
if (this.autoLoopRunning) {
|
||||||
type: "auto_mode_complete",
|
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
||||||
message: "All features completed!",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentFeatureId = nextFeature.id;
|
|
||||||
|
|
||||||
// Skip if this feature is already running (via manual trigger)
|
|
||||||
if (this.runningFeatures.has(currentFeatureId)) {
|
|
||||||
console.log(
|
|
||||||
`[AutoMode] Skipping ${currentFeatureId} - already running`
|
|
||||||
);
|
|
||||||
await this.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[AutoMode] Selected feature: ${nextFeature.description}`);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_feature_start",
|
|
||||||
featureId: nextFeature.id,
|
|
||||||
feature: nextFeature,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register this feature as running
|
|
||||||
const execution = this.createExecutionContext(currentFeatureId);
|
|
||||||
execution.projectPath = projectPath;
|
|
||||||
execution.sendToRenderer = sendToRenderer;
|
|
||||||
this.runningFeatures.set(currentFeatureId, execution);
|
|
||||||
|
|
||||||
// Implement the feature
|
|
||||||
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
|
|
||||||
let newStatus;
|
|
||||||
if (result.passes) {
|
|
||||||
newStatus = nextFeature.skipTests ? "waiting_approval" : "verified";
|
|
||||||
} else {
|
|
||||||
newStatus = "backlog";
|
|
||||||
}
|
|
||||||
await featureLoader.updateFeatureStatus(
|
|
||||||
nextFeature.id,
|
|
||||||
newStatus,
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete context file only if verified (not for waiting_approval)
|
|
||||||
if (newStatus === "verified") {
|
|
||||||
await contextManager.deleteContextFile(projectPath, nextFeature.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_feature_complete",
|
|
||||||
featureId: nextFeature.id,
|
|
||||||
passes: result.passes,
|
|
||||||
message: result.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
this.runningFeatures.delete(currentFeatureId);
|
|
||||||
|
|
||||||
// Small delay before next feature
|
|
||||||
if (this.autoLoopRunning) {
|
|
||||||
await this.sleep(3000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AutoMode] Error in loop iteration:", error);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_error",
|
|
||||||
error: error.message,
|
|
||||||
featureId: currentFeatureId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up on error
|
|
||||||
if (currentFeatureId) {
|
|
||||||
this.runningFeatures.delete(currentFeatureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before retrying
|
|
||||||
await this.sleep(5000);
|
|
||||||
}
|
}
|
||||||
|
}, this.checkIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check how many features are running and start new ones if under max concurrency
|
||||||
|
*/
|
||||||
|
async checkAndStartFeatures(projectPath, sendToRenderer) {
|
||||||
|
try {
|
||||||
|
// Check how many are currently running
|
||||||
|
const currentRunningCount = this.runningFeatures.size;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate available slots
|
||||||
|
const availableSlots = this.maxConcurrency - currentRunningCount;
|
||||||
|
|
||||||
|
if (availableSlots <= 0) {
|
||||||
|
console.log("[AutoMode] At max concurrency, waiting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load features from backlog
|
||||||
|
const features = await featureLoader.loadFeatures(projectPath);
|
||||||
|
const backlogFeatures = features.filter((f) => f.status === "backlog");
|
||||||
|
|
||||||
|
if (backlogFeatures.length === 0) {
|
||||||
|
console.log("[AutoMode] No backlog features available, waiting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab up to availableSlots features from backlog
|
||||||
|
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AutoMode] Starting ${featuresToStart.length} feature(s) from backlog`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start each feature (don't await - run in parallel like drag operations)
|
||||||
|
for (const feature of featuresToStart) {
|
||||||
|
this.startFeatureAsync(feature, projectPath, sendToRenderer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AutoMode] Error checking/starting features:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a feature asynchronously (similar to drag operation)
|
||||||
|
*/
|
||||||
|
async startFeatureAsync(feature, projectPath, sendToRenderer) {
|
||||||
|
const featureId = feature.id;
|
||||||
|
|
||||||
|
// Skip if already running
|
||||||
|
if (this.runningFeatures.has(featureId)) {
|
||||||
|
console.log(`[AutoMode] Feature ${featureId} already running, skipping`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[AutoMode] Loop ended");
|
try {
|
||||||
this.autoLoopRunning = false;
|
console.log(
|
||||||
|
`[AutoMode] Starting feature: ${feature.description.slice(0, 50)}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register this feature as running
|
||||||
|
const execution = this.createExecutionContext(featureId);
|
||||||
|
execution.projectPath = projectPath;
|
||||||
|
execution.sendToRenderer = sendToRenderer;
|
||||||
|
this.runningFeatures.set(featureId, execution);
|
||||||
|
|
||||||
|
// Update status to in_progress with timestamp
|
||||||
|
await featureLoader.updateFeatureStatus(
|
||||||
|
featureId,
|
||||||
|
"in_progress",
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "auto_mode_feature_start",
|
||||||
|
featureId: feature.id,
|
||||||
|
feature: feature,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Implement the feature (this runs async in background)
|
||||||
|
const result = await featureExecutor.implementFeature(
|
||||||
|
feature,
|
||||||
|
projectPath,
|
||||||
|
sendToRenderer,
|
||||||
|
execution
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update feature status based on result
|
||||||
|
let newStatus;
|
||||||
|
if (result.passes) {
|
||||||
|
newStatus = feature.skipTests ? "waiting_approval" : "verified";
|
||||||
|
} else {
|
||||||
|
newStatus = "backlog";
|
||||||
|
}
|
||||||
|
await featureLoader.updateFeatureStatus(
|
||||||
|
feature.id,
|
||||||
|
newStatus,
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "auto_mode_feature_complete",
|
||||||
|
featureId: feature.id,
|
||||||
|
passes: result.passes,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[AutoMode] Error running feature ${featureId}:`, error);
|
||||||
|
sendToRenderer({
|
||||||
|
type: "auto_mode_error",
|
||||||
|
error: error.message,
|
||||||
|
featureId: featureId,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Clean up this feature's execution
|
||||||
|
this.runningFeatures.delete(featureId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,10 +751,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete context file if verified (only for non-skipTests)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
if (newStatus === "verified") {
|
|
||||||
await contextManager.deleteContextFile(projectPath, feature.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
@@ -778,7 +807,7 @@ class AutoModeService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Run git commit via the agent
|
// Run git commit via the agent
|
||||||
const commitResult = await featureExecutor.commitChangesOnly(
|
await featureExecutor.commitChangesOnly(
|
||||||
feature,
|
feature,
|
||||||
projectPath,
|
projectPath,
|
||||||
sendToRenderer,
|
sendToRenderer,
|
||||||
@@ -792,8 +821,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete context file
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
await contextManager.deleteContextFile(projectPath, featureId);
|
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
|
|||||||
@@ -5,18 +5,27 @@ require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
|||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
||||||
const fs = require("fs/promises");
|
const fs = require("fs/promises");
|
||||||
const os = require("os");
|
|
||||||
const agentService = require("./agent-service");
|
const agentService = require("./agent-service");
|
||||||
const autoModeService = require("./auto-mode-service");
|
const autoModeService = require("./auto-mode-service");
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
|
// Get icon path - works in both dev and production
|
||||||
|
function getIconPath() {
|
||||||
|
// In dev: __dirname is electron/, so ../public/icon_gold.png
|
||||||
|
// In production: public folder is included in the app bundle
|
||||||
|
return app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "app", "public", "icon_gold.png")
|
||||||
|
: path.join(__dirname, "../public/icon_gold.png");
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
height: 900,
|
height: 900,
|
||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 700,
|
minHeight: 700,
|
||||||
|
icon: getIconPath(),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@@ -41,6 +50,11 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
// Set app icon (dock icon on macOS)
|
||||||
|
if (process.platform === "darwin" && app.dock) {
|
||||||
|
app.dock.setIcon(getIconPath());
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize agent service
|
// Initialize agent service
|
||||||
const appDataPath = app.getPath("userData");
|
const appDataPath = app.getPath("userData");
|
||||||
await agentService.initialize(appDataPath);
|
await agentService.initialize(appDataPath);
|
||||||
@@ -160,32 +174,43 @@ ipcMain.handle("app:getPath", (_, name) => {
|
|||||||
return app.getPath(name);
|
return app.getPath(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save image to temp directory
|
// Save image to .automaker/images directory
|
||||||
ipcMain.handle("app:saveImageToTemp", async (_, { data, filename, mimeType }) => {
|
ipcMain.handle(
|
||||||
try {
|
"app:saveImageToTemp",
|
||||||
// Create temp directory for images if it doesn't exist
|
async (_, { data, filename, mimeType, projectPath }) => {
|
||||||
const tempDir = path.join(os.tmpdir(), "automaker-images");
|
try {
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
// Use .automaker/images directory instead of /tmp
|
||||||
|
// If projectPath is provided, use it; otherwise fall back to app data directory
|
||||||
|
let imagesDir;
|
||||||
|
if (projectPath) {
|
||||||
|
imagesDir = path.join(projectPath, ".automaker", "images");
|
||||||
|
} else {
|
||||||
|
// Fallback for cases where project isn't loaded yet
|
||||||
|
const appDataPath = app.getPath("userData");
|
||||||
|
imagesDir = path.join(appDataPath, "images");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate unique filename
|
await fs.mkdir(imagesDir, { recursive: true });
|
||||||
const timestamp = Date.now();
|
|
||||||
const ext = mimeType.split("/")[1] || "png";
|
|
||||||
const safeName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
||||||
const tempFilePath = path.join(tempDir, `${timestamp}_${safeName}`);
|
|
||||||
|
|
||||||
// Remove data URL prefix if present (data:image/png;base64,...)
|
// Generate unique filename with unique ID
|
||||||
const base64Data = data.includes(",") ? data.split(",")[1] : data;
|
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||||
|
const safeName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||||
|
const imageFilePath = path.join(imagesDir, `${uniqueId}_${safeName}`);
|
||||||
|
|
||||||
// Write image to temp file
|
// Remove data URL prefix if present (data:image/png;base64,...)
|
||||||
await fs.writeFile(tempFilePath, base64Data, "base64");
|
const base64Data = data.includes(",") ? data.split(",")[1] : data;
|
||||||
|
|
||||||
console.log("[IPC] Saved image to temp:", tempFilePath);
|
// Write image to file
|
||||||
return { success: true, path: tempFilePath };
|
await fs.writeFile(imageFilePath, base64Data, "base64");
|
||||||
} catch (error) {
|
|
||||||
console.error("[IPC] Failed to save image to temp:", error);
|
console.log("[IPC] Saved image to .automaker/images:", imageFilePath);
|
||||||
return { success: false, error: error.message };
|
return { success: true, path: imageFilePath };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] Failed to save image:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// IPC ping for testing communication
|
// IPC ping for testing communication
|
||||||
ipcMain.handle("ping", () => {
|
ipcMain.handle("ping", () => {
|
||||||
@@ -201,7 +226,10 @@ ipcMain.handle("ping", () => {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
|
ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
|
||||||
try {
|
try {
|
||||||
return await agentService.startConversation({ sessionId, workingDirectory });
|
return await agentService.startConversation({
|
||||||
|
sessionId,
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[IPC] agent:start error:", error);
|
console.error("[IPC] agent:start error:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -211,42 +239,45 @@ ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
|
|||||||
/**
|
/**
|
||||||
* Send a message to the agent - returns immediately, streams via events
|
* Send a message to the agent - returns immediately, streams via events
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("agent:send", async (event, { sessionId, message, workingDirectory, imagePaths }) => {
|
ipcMain.handle(
|
||||||
try {
|
"agent:send",
|
||||||
// Create a function to send updates to the renderer
|
async (event, { sessionId, message, workingDirectory, imagePaths }) => {
|
||||||
const sendToRenderer = (data) => {
|
try {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
// Create a function to send updates to the renderer
|
||||||
mainWindow.webContents.send("agent:stream", {
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("agent:stream", {
|
||||||
|
sessionId,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing (runs in background)
|
||||||
|
agentService
|
||||||
|
.sendMessage({
|
||||||
sessionId,
|
sessionId,
|
||||||
...data,
|
message,
|
||||||
|
workingDirectory,
|
||||||
|
imagePaths,
|
||||||
|
sendToRenderer,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[IPC] agent:send background error:", error);
|
||||||
|
sendToRenderer({
|
||||||
|
type: "error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start processing (runs in background)
|
// Return immediately
|
||||||
agentService
|
return { success: true };
|
||||||
.sendMessage({
|
} catch (error) {
|
||||||
sessionId,
|
console.error("[IPC] agent:send error:", error);
|
||||||
message,
|
return { success: false, error: error.message };
|
||||||
workingDirectory,
|
}
|
||||||
imagePaths,
|
|
||||||
sendToRenderer,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[IPC] agent:send background error:", error);
|
|
||||||
sendToRenderer({
|
|
||||||
type: "error",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return immediately
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[IPC] agent:send error:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get conversation history
|
* Get conversation history
|
||||||
@@ -304,14 +335,21 @@ ipcMain.handle("sessions:list", async (_, { includeArchived }) => {
|
|||||||
/**
|
/**
|
||||||
* Create a new session
|
* Create a new session
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("sessions:create", async (_, { name, projectPath, workingDirectory }) => {
|
ipcMain.handle(
|
||||||
try {
|
"sessions:create",
|
||||||
return await agentService.createSession({ name, projectPath, workingDirectory });
|
async (_, { name, projectPath, workingDirectory }) => {
|
||||||
} catch (error) {
|
try {
|
||||||
console.error("[IPC] sessions:create error:", error);
|
return await agentService.createSession({
|
||||||
return { success: false, error: error.message };
|
name,
|
||||||
|
projectPath,
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] sessions:create error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update session metadata
|
* Update session metadata
|
||||||
@@ -368,20 +406,27 @@ ipcMain.handle("sessions:delete", async (_, { sessionId }) => {
|
|||||||
/**
|
/**
|
||||||
* Start auto mode - autonomous feature implementation
|
* Start auto mode - autonomous feature implementation
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:start", async (_, { projectPath }) => {
|
ipcMain.handle(
|
||||||
try {
|
"auto-mode:start",
|
||||||
const sendToRenderer = (data) => {
|
async (_, { projectPath, maxConcurrency }) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
try {
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
const sendToRenderer = (data) => {
|
||||||
}
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
};
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.start({ projectPath, sendToRenderer });
|
return await autoModeService.start({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:start error:", error);
|
sendToRenderer,
|
||||||
return { success: false, error: error.message };
|
maxConcurrency,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:start error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop auto mode
|
* Stop auto mode
|
||||||
@@ -410,76 +455,111 @@ ipcMain.handle("auto-mode:status", () => {
|
|||||||
/**
|
/**
|
||||||
* Run a specific feature
|
* Run a specific feature
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:run-feature", async (_, { projectPath, featureId }) => {
|
ipcMain.handle(
|
||||||
try {
|
"auto-mode:run-feature",
|
||||||
const sendToRenderer = (data) => {
|
async (_, { projectPath, featureId }) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
try {
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
const sendToRenderer = (data) => {
|
||||||
}
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
};
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.runFeature({ projectPath, featureId, sendToRenderer });
|
return await autoModeService.runFeature({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:run-feature error:", error);
|
featureId,
|
||||||
return { success: false, error: error.message };
|
sendToRenderer,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:run-feature error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a specific feature by running its tests
|
* Verify a specific feature by running its tests
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:verify-feature", async (_, { projectPath, featureId }) => {
|
ipcMain.handle(
|
||||||
console.log("[IPC] auto-mode:verify-feature called with:", { projectPath, featureId });
|
"auto-mode:verify-feature",
|
||||||
try {
|
async (_, { projectPath, featureId }) => {
|
||||||
const sendToRenderer = (data) => {
|
console.log("[IPC] auto-mode:verify-feature called with:", {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
projectPath,
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
featureId,
|
||||||
}
|
});
|
||||||
};
|
try {
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.verifyFeature({ projectPath, featureId, sendToRenderer });
|
return await autoModeService.verifyFeature({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:verify-feature error:", error);
|
featureId,
|
||||||
return { success: false, error: error.message };
|
sendToRenderer,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:verify-feature error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume a specific feature with previous context
|
* Resume a specific feature with previous context
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:resume-feature", async (_, { projectPath, featureId }) => {
|
ipcMain.handle(
|
||||||
console.log("[IPC] auto-mode:resume-feature called with:", { projectPath, featureId });
|
"auto-mode:resume-feature",
|
||||||
try {
|
async (_, { projectPath, featureId }) => {
|
||||||
const sendToRenderer = (data) => {
|
console.log("[IPC] auto-mode:resume-feature called with:", {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
projectPath,
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
featureId,
|
||||||
}
|
});
|
||||||
};
|
try {
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.resumeFeature({ projectPath, featureId, sendToRenderer });
|
return await autoModeService.resumeFeature({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:resume-feature error:", error);
|
featureId,
|
||||||
return { success: false, error: error.message };
|
sendToRenderer,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:resume-feature error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a context file exists for a feature
|
* Check if a context file exists for a feature
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:context-exists", async (_, { projectPath, featureId }) => {
|
ipcMain.handle(
|
||||||
try {
|
"auto-mode:context-exists",
|
||||||
const contextPath = path.join(projectPath, ".automaker", "context", `${featureId}.md`);
|
async (_, { projectPath, featureId }) => {
|
||||||
try {
|
try {
|
||||||
await fs.access(contextPath);
|
const contextPath = path.join(
|
||||||
return { success: true, exists: true };
|
projectPath,
|
||||||
} catch {
|
".automaker",
|
||||||
return { success: true, exists: false };
|
"context",
|
||||||
|
`${featureId}.md`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await fs.access(contextPath);
|
||||||
|
return { success: true, exists: true };
|
||||||
|
} catch {
|
||||||
|
return { success: true, exists: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:context-exists error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("[IPC] auto-mode:context-exists error:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze a new project - kicks off an agent to analyze the codebase
|
* Analyze a new project - kicks off an agent to analyze the codebase
|
||||||
@@ -494,7 +574,10 @@ ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return await autoModeService.analyzeProject({ projectPath, sendToRenderer });
|
return await autoModeService.analyzeProject({
|
||||||
|
projectPath,
|
||||||
|
sendToRenderer,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[IPC] auto-mode:analyze-project error:", error);
|
console.error("[IPC] auto-mode:analyze-project error:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -517,37 +600,61 @@ ipcMain.handle("auto-mode:stop-feature", async (_, { featureId }) => {
|
|||||||
/**
|
/**
|
||||||
* Follow-up on a feature with additional prompt
|
* Follow-up on a feature with additional prompt
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:follow-up-feature", async (_, { projectPath, featureId, prompt, imagePaths }) => {
|
ipcMain.handle(
|
||||||
console.log("[IPC] auto-mode:follow-up-feature called with:", { projectPath, featureId, prompt, imagePaths });
|
"auto-mode:follow-up-feature",
|
||||||
try {
|
async (_, { projectPath, featureId, prompt, imagePaths }) => {
|
||||||
const sendToRenderer = (data) => {
|
console.log("[IPC] auto-mode:follow-up-feature called with:", {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
projectPath,
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
featureId,
|
||||||
}
|
prompt,
|
||||||
};
|
imagePaths,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.followUpFeature({ projectPath, featureId, prompt, imagePaths, sendToRenderer });
|
return await autoModeService.followUpFeature({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:follow-up-feature error:", error);
|
featureId,
|
||||||
return { success: false, error: error.message };
|
prompt,
|
||||||
|
imagePaths,
|
||||||
|
sendToRenderer,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:follow-up-feature error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commit changes for a feature (no further work, just commit)
|
* Commit changes for a feature (no further work, just commit)
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("auto-mode:commit-feature", async (_, { projectPath, featureId }) => {
|
ipcMain.handle(
|
||||||
console.log("[IPC] auto-mode:commit-feature called with:", { projectPath, featureId });
|
"auto-mode:commit-feature",
|
||||||
try {
|
async (_, { projectPath, featureId }) => {
|
||||||
const sendToRenderer = (data) => {
|
console.log("[IPC] auto-mode:commit-feature called with:", {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
projectPath,
|
||||||
mainWindow.webContents.send("auto-mode:event", data);
|
featureId,
|
||||||
}
|
});
|
||||||
};
|
try {
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("auto-mode:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return await autoModeService.commitFeature({ projectPath, featureId, sendToRenderer });
|
return await autoModeService.commitFeature({
|
||||||
} catch (error) {
|
projectPath,
|
||||||
console.error("[IPC] auto-mode:commit-feature error:", error);
|
featureId,
|
||||||
return { success: false, error: error.message };
|
sendToRenderer,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] auto-mode:commit-feature error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
// Auto Mode API
|
// Auto Mode API
|
||||||
autoMode: {
|
autoMode: {
|
||||||
// Start auto mode
|
// Start auto mode
|
||||||
start: (projectPath) =>
|
start: (projectPath, maxConcurrency) =>
|
||||||
ipcRenderer.invoke("auto-mode:start", { projectPath }),
|
ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
|
||||||
|
|
||||||
// Stop auto mode
|
// Stop auto mode
|
||||||
stop: () => ipcRenderer.invoke("auto-mode:stop"),
|
stop: () => ipcRenderer.invoke("auto-mode:stop"),
|
||||||
|
|||||||
@@ -75,7 +75,57 @@ class FeatureExecutor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build the prompt for this specific feature
|
// Build the prompt for this specific feature
|
||||||
const prompt = promptBuilder.buildFeaturePrompt(feature);
|
let prompt = promptBuilder.buildFeaturePrompt(feature);
|
||||||
|
|
||||||
|
// Add images to prompt if feature has imagePaths
|
||||||
|
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
||||||
|
const contentBlocks = [];
|
||||||
|
|
||||||
|
// Add text block
|
||||||
|
contentBlocks.push({
|
||||||
|
type: "text",
|
||||||
|
text: prompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add image blocks
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
for (const imagePathObj of feature.imagePaths) {
|
||||||
|
try {
|
||||||
|
const imagePath = imagePathObj.path;
|
||||||
|
const imageBuffer = fs.readFileSync(imagePath);
|
||||||
|
const base64Data = imageBuffer.toString("base64");
|
||||||
|
const ext = path.extname(imagePath).toLowerCase();
|
||||||
|
const mimeTypeMap = {
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
};
|
||||||
|
const mediaType = mimeTypeMap[ext] || imagePathObj.mimeType || "image/png";
|
||||||
|
|
||||||
|
contentBlocks.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: mediaType,
|
||||||
|
data: base64Data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Added image to prompt: ${imagePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use content blocks instead of plain text
|
||||||
|
prompt = contentBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
// Planning: Analyze the codebase and create implementation plan
|
// Planning: Analyze the codebase and create implementation plan
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
@@ -274,7 +324,58 @@ class FeatureExecutor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt with previous context
|
// Build prompt with previous context
|
||||||
const prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
let prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
||||||
|
|
||||||
|
// Add images to prompt if feature has imagePaths or followUpImages
|
||||||
|
const imagePaths = feature.followUpImages || feature.imagePaths;
|
||||||
|
if (imagePaths && imagePaths.length > 0) {
|
||||||
|
const contentBlocks = [];
|
||||||
|
|
||||||
|
// Add text block
|
||||||
|
contentBlocks.push({
|
||||||
|
type: "text",
|
||||||
|
text: prompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add image blocks
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
for (const imagePathObj of imagePaths) {
|
||||||
|
try {
|
||||||
|
const imagePath = imagePathObj.path;
|
||||||
|
const imageBuffer = fs.readFileSync(imagePath);
|
||||||
|
const base64Data = imageBuffer.toString("base64");
|
||||||
|
const ext = path.extname(imagePath).toLowerCase();
|
||||||
|
const mimeTypeMap = {
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
};
|
||||||
|
const mediaType = mimeTypeMap[ext] || imagePathObj.mimeType || "image/png";
|
||||||
|
|
||||||
|
contentBlocks.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: mediaType,
|
||||||
|
data: base64Data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[FeatureExecutor] Added image to resume prompt: ${imagePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use content blocks instead of plain text
|
||||||
|
prompt = contentBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
const currentQuery = query({ prompt, options });
|
const currentQuery = query({ prompt, options });
|
||||||
execution.query = currentQuery;
|
execution.query = currentQuery;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class PromptBuilder {
|
|||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||||
|
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully before implementing.\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
return `You are working on a feature implementation task.
|
return `You are working on a feature implementation task.
|
||||||
|
|
||||||
**Current Feature to Implement:**
|
**Current Feature to Implement:**
|
||||||
@@ -17,7 +21,7 @@ class PromptBuilder {
|
|||||||
ID: ${feature.id}
|
ID: ${feature.id}
|
||||||
Category: ${feature.category}
|
Category: ${feature.category}
|
||||||
Description: ${feature.description}
|
Description: ${feature.description}
|
||||||
${skipTestsNote}
|
${skipTestsNote}${imagesNote}
|
||||||
**Steps to Complete:**
|
**Steps to Complete:**
|
||||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||||
|
|
||||||
@@ -117,6 +121,10 @@ Begin by reading the project structure and then implementing the feature.`;
|
|||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||||
|
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully before implementing.\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
return `You are implementing and verifying a feature until it is complete and working correctly.
|
return `You are implementing and verifying a feature until it is complete and working correctly.
|
||||||
|
|
||||||
**Feature to Implement/Verify:**
|
**Feature to Implement/Verify:**
|
||||||
@@ -125,7 +133,7 @@ ID: ${feature.id}
|
|||||||
Category: ${feature.category}
|
Category: ${feature.category}
|
||||||
Description: ${feature.description}
|
Description: ${feature.description}
|
||||||
Current Status: ${feature.status}
|
Current Status: ${feature.status}
|
||||||
${skipTestsNote}
|
${skipTestsNote}${imagesNote}
|
||||||
**Steps that should be implemented:**
|
**Steps that should be implemented:**
|
||||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||||
|
|
||||||
@@ -216,6 +224,10 @@ Begin by reading the project structure and understanding what needs to be implem
|
|||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||||
|
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully.\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
return `You are resuming work on a feature implementation that was previously started.
|
return `You are resuming work on a feature implementation that was previously started.
|
||||||
|
|
||||||
**Current Feature:**
|
**Current Feature:**
|
||||||
@@ -223,7 +235,7 @@ Begin by reading the project structure and understanding what needs to be implem
|
|||||||
ID: ${feature.id}
|
ID: ${feature.id}
|
||||||
Category: ${feature.category}
|
Category: ${feature.category}
|
||||||
Description: ${feature.description}
|
Description: ${feature.description}
|
||||||
${skipTestsNote}
|
${skipTestsNote}${imagesNote}
|
||||||
**Steps to Complete:**
|
**Steps to Complete:**
|
||||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
"arch": ["x64", "arm64"]
|
"arch": ["x64", "arm64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "public/icon.png"
|
"icon": "public/icon_gold.png"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"arch": ["x64"]
|
"arch": ["x64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "public/icon.png"
|
"icon": "public/icon_gold.png"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"icon": "public/icon.png"
|
"icon": "public/icon_gold.png"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
|
|||||||
BIN
app/public/icon.png
Normal file
BIN
app/public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
app/public/icon_gold.png
Normal file
BIN
app/public/icon_gold.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@@ -19,10 +19,10 @@ import {
|
|||||||
PanelLeft,
|
PanelLeft,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Cpu,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Check,
|
Check,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
GripVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -37,9 +37,23 @@ import {
|
|||||||
ACTION_SHORTCUTS,
|
ACTION_SHORTCUTS,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI, Project } from "@/lib/electron";
|
||||||
import { initializeProject } from "@/lib/project-init";
|
import { initializeProject } from "@/lib/project-init";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
closestCenter,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
interface NavSection {
|
interface NavSection {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -53,6 +67,81 @@ interface NavItem {
|
|||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sortable Project Item Component
|
||||||
|
interface SortableProjectItemProps {
|
||||||
|
project: Project;
|
||||||
|
index: number;
|
||||||
|
currentProjectId: string | undefined;
|
||||||
|
onSelect: (project: Project) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableProjectItem({
|
||||||
|
project,
|
||||||
|
index,
|
||||||
|
currentProjectId,
|
||||||
|
onSelect,
|
||||||
|
}: SortableProjectItemProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: project.id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent",
|
||||||
|
isDragging && "bg-accent shadow-lg"
|
||||||
|
)}
|
||||||
|
data-testid={`project-option-${project.id}`}
|
||||||
|
>
|
||||||
|
{/* Drag Handle */}
|
||||||
|
<button
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="p-0.5 rounded hover:bg-sidebar-accent/20 cursor-grab active:cursor-grabbing"
|
||||||
|
data-testid={`project-drag-handle-${project.id}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<GripVertical className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Hotkey indicator */}
|
||||||
|
{index < 9 && (
|
||||||
|
<span
|
||||||
|
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}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project content - clickable area */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 flex-1 min-w-0"
|
||||||
|
onClick={() => onSelect(project)}
|
||||||
|
>
|
||||||
|
<Folder className="h-4 w-4 shrink-0" />
|
||||||
|
<span className="flex-1 truncate text-sm">{project.name}</span>
|
||||||
|
{currentProjectId === project.id && (
|
||||||
|
<Check className="h-4 w-4 text-brand-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const {
|
const {
|
||||||
projects,
|
projects,
|
||||||
@@ -64,11 +153,38 @@ export function Sidebar() {
|
|||||||
setCurrentView,
|
setCurrentView,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
removeProject,
|
removeProject,
|
||||||
|
reorderProjects,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// State for project picker dropdown
|
// State for project picker dropdown
|
||||||
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
|
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
|
||||||
|
|
||||||
|
// Sensors for drag-and-drop
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 5, // Small distance to start drag
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle drag end for reordering projects
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
const oldIndex = projects.findIndex((p) => p.id === active.id);
|
||||||
|
const newIndex = projects.findIndex((p) => p.id === over.id);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
reorderProjects(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[projects, reorderProjects]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the system folder selection dialog and initializes the selected project.
|
* Opens the system folder selection dialog and initializes the selected project.
|
||||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||||
@@ -312,8 +428,12 @@ export function Sidebar() {
|
|||||||
onClick={() => setCurrentView("welcome")}
|
onClick={() => setCurrentView("welcome")}
|
||||||
data-testid="logo-button"
|
data-testid="logo-button"
|
||||||
>
|
>
|
||||||
<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">
|
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg group">
|
||||||
<Cpu className="text-primary-foreground w-5 h-5 group-hover:rotate-12 transition-transform" />
|
<img
|
||||||
|
src="/icon_gold.png"
|
||||||
|
alt="Automaker Logo"
|
||||||
|
className="w-8 h-8 group-hover:rotate-12 transition-transform"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -387,35 +507,33 @@ export function Sidebar() {
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-56 bg-popover border-border"
|
className="w-64 bg-popover border-border p-1"
|
||||||
align="start"
|
align="start"
|
||||||
data-testid="project-picker-dropdown"
|
data-testid="project-picker-dropdown"
|
||||||
>
|
>
|
||||||
{projects.map((project, index) => (
|
<DndContext
|
||||||
<DropdownMenuItem
|
sensors={sensors}
|
||||||
key={project.id}
|
collisionDetection={closestCenter}
|
||||||
onClick={() => {
|
onDragEnd={handleDragEnd}
|
||||||
setCurrentProject(project);
|
>
|
||||||
setIsProjectPickerOpen(false);
|
<SortableContext
|
||||||
}}
|
items={projects.map((p) => p.id)}
|
||||||
className="flex items-center gap-2 cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent"
|
strategy={verticalListSortingStrategy}
|
||||||
data-testid={`project-option-${project.id}`}
|
|
||||||
>
|
>
|
||||||
{index < 9 && (
|
{projects.map((project, index) => (
|
||||||
<span
|
<SortableProjectItem
|
||||||
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"
|
key={project.id}
|
||||||
data-testid={`project-hotkey-${index + 1}`}
|
project={project}
|
||||||
>
|
index={index}
|
||||||
{index + 1}
|
currentProjectId={currentProject?.id}
|
||||||
</span>
|
onSelect={(p) => {
|
||||||
)}
|
setCurrentProject(p);
|
||||||
<Folder className="h-4 w-4" />
|
setIsProjectPickerOpen(false);
|
||||||
<span className="flex-1 truncate">{project.name}</span>
|
}}
|
||||||
{currentProject?.id === project.id && (
|
/>
|
||||||
<Check className="h-4 w-4 text-brand-500" />
|
))}
|
||||||
)}
|
</SortableContext>
|
||||||
</DropdownMenuItem>
|
</DndContext>
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { ImageIcon, X, Loader2 } from "lucide-react";
|
import { ImageIcon, X, Loader2 } from "lucide-react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
|
import { useAppStore } from "@/store/app-store";
|
||||||
|
|
||||||
export interface FeatureImagePath {
|
export interface FeatureImagePath {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -51,6 +52,7 @@ export function DescriptionImageDropZone({
|
|||||||
new Map()
|
new Map()
|
||||||
);
|
);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const currentProject = useAppStore((state) => state.currentProject);
|
||||||
|
|
||||||
const fileToBase64 = (file: File): Promise<string> => {
|
const fileToBase64 = (file: File): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -76,11 +78,14 @@ export function DescriptionImageDropZone({
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
// Check if saveImageToTemp method exists
|
// Check if saveImageToTemp method exists
|
||||||
if (!api.saveImageToTemp) {
|
if (!api.saveImageToTemp) {
|
||||||
// Fallback for mock API - return a mock path
|
// Fallback for mock API - return a mock path in .automaker/images
|
||||||
console.log("[DescriptionImageDropZone] Using mock path for image");
|
console.log("[DescriptionImageDropZone] Using mock path for image");
|
||||||
return `/tmp/automaker-images/${Date.now()}_${filename}`;
|
return `.automaker/images/${Date.now()}_${filename}`;
|
||||||
}
|
}
|
||||||
const result = await api.saveImageToTemp(base64Data, filename, mimeType);
|
|
||||||
|
// Get projectPath from the store if available
|
||||||
|
const projectPath = currentProject?.path;
|
||||||
|
const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath);
|
||||||
if (result.success && result.path) {
|
if (result.success && result.path) {
|
||||||
return result.path;
|
return result.path;
|
||||||
}
|
}
|
||||||
@@ -130,7 +135,7 @@ export function DescriptionImageDropZone({
|
|||||||
const tempPath = await saveImageToTemp(base64, file.name, file.type);
|
const tempPath = await saveImageToTemp(base64, file.name, file.type);
|
||||||
|
|
||||||
if (tempPath) {
|
if (tempPath) {
|
||||||
const imageId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||||
const imagePathRef: FeatureImagePath = {
|
const imagePathRef: FeatureImagePath = {
|
||||||
id: imageId,
|
id: imageId,
|
||||||
path: tempPath,
|
path: tempPath,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ 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 grid translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
|||||||
{entry.title}
|
{entry.title}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="text-sm text-zinc-400 truncate flex-1 ml-2">
|
<span className="text-xs text-zinc-400 truncate flex-1 ml-2">
|
||||||
{!isExpanded &&
|
{!isExpanded &&
|
||||||
entry.content.slice(0, 80) +
|
entry.content.slice(0, 80) +
|
||||||
(entry.content.length > 80 ? "..." : "")}
|
(entry.content.length > 80 ? "..." : "")}
|
||||||
@@ -153,11 +153,11 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
|||||||
className="px-4 pb-3 pt-1"
|
className="px-4 pb-3 pt-1"
|
||||||
data-testid={`log-entry-content-${entry.id}`}
|
data-testid={`log-entry-content-${entry.id}`}
|
||||||
>
|
>
|
||||||
<div className="font-mono text-sm space-y-1">
|
<div className="font-mono text-xs space-y-1">
|
||||||
{formattedContent.map((part, index) => (
|
{formattedContent.map((part, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{part.type === "json" ? (
|
{part.type === "json" ? (
|
||||||
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-purple-300">
|
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
|
||||||
{part.content}
|
{part.content}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -202,13 +202,13 @@ export function AgentOutputModal({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-w-4xl max-h-[80vh] flex flex-col"
|
className="w-[90vw] max-w-[90vw] max-h-[80vh] flex flex-col"
|
||||||
data-testid="agent-output-modal"
|
data-testid="agent-output-modal"
|
||||||
>
|
>
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
||||||
Agent Output
|
Agent Output
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="flex items-center gap-1 bg-zinc-900/50 rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-zinc-900/50 rounded-lg p-1">
|
||||||
@@ -216,7 +216,7 @@ export function AgentOutputModal({
|
|||||||
onClick={() => setViewMode("parsed")}
|
onClick={() => setViewMode("parsed")}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
||||||
viewMode === "parsed"
|
viewMode === "parsed"
|
||||||
? "bg-purple-500/20 text-purple-300 shadow-sm"
|
? "bg-primary/20 text-primary shadow-sm"
|
||||||
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
||||||
}`}
|
}`}
|
||||||
data-testid="view-mode-parsed"
|
data-testid="view-mode-parsed"
|
||||||
@@ -228,7 +228,7 @@ export function AgentOutputModal({
|
|||||||
onClick={() => setViewMode("raw")}
|
onClick={() => setViewMode("raw")}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
||||||
viewMode === "raw"
|
viewMode === "raw"
|
||||||
? "bg-purple-500/20 text-purple-300 shadow-sm"
|
? "bg-primary/20 text-primary shadow-sm"
|
||||||
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
||||||
}`}
|
}`}
|
||||||
data-testid="view-mode-raw"
|
data-testid="view-mode-raw"
|
||||||
@@ -249,7 +249,7 @@ export function AgentOutputModal({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-sm min-h-[400px] max-h-[60vh]"
|
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh]"
|
||||||
>
|
>
|
||||||
{isLoading && !output ? (
|
{isLoading && !output ? (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useAppStore, AutoModeActivity } from "@/store/app-store";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
CheckCircle2,
|
|
||||||
Loader2,
|
|
||||||
AlertCircle,
|
|
||||||
Wrench,
|
|
||||||
Play,
|
|
||||||
X,
|
|
||||||
ClipboardList,
|
|
||||||
Zap,
|
|
||||||
ShieldCheck,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface AutoModeLogProps {
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AutoModeLog({ onClose }: AutoModeLogProps) {
|
|
||||||
const { autoModeActivityLog, features, clearAutoModeActivity } =
|
|
||||||
useAppStore();
|
|
||||||
|
|
||||||
const getActivityIcon = (type: AutoModeActivity["type"]) => {
|
|
||||||
switch (type) {
|
|
||||||
case "start":
|
|
||||||
return <Play className="w-4 h-4 text-blue-500" />;
|
|
||||||
case "progress":
|
|
||||||
return <Loader2 className="w-4 h-4 text-purple-500 animate-spin" />;
|
|
||||||
case "tool":
|
|
||||||
return <Wrench className="w-4 h-4 text-yellow-500" />;
|
|
||||||
case "complete":
|
|
||||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "action":
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getActivityColor = (type: AutoModeActivity["type"]) => {
|
|
||||||
switch (type) {
|
|
||||||
case "start":
|
|
||||||
return "border-l-blue-500";
|
|
||||||
case "progress":
|
|
||||||
return "border-l-purple-500";
|
|
||||||
case "tool":
|
|
||||||
return "border-l-yellow-500";
|
|
||||||
case "complete":
|
|
||||||
return "border-l-green-500";
|
|
||||||
case "error":
|
|
||||||
return "border-l-red-500";
|
|
||||||
case "planning":
|
|
||||||
return "border-l-cyan-500";
|
|
||||||
case "action":
|
|
||||||
return "border-l-orange-500";
|
|
||||||
case "verification":
|
|
||||||
return "border-l-emerald-500";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFeatureDescription = (featureId: string) => {
|
|
||||||
const feature = features.find((f) => f.id === featureId);
|
|
||||||
return feature?.description || "Unknown feature";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (date: Date) => {
|
|
||||||
return new Date(date).toLocaleTimeString("en-US", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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" />
|
|
||||||
<CardTitle className="text-lg">Auto Mode Activity</CardTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearAutoModeActivity}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
{onClose && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onClose}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0 flex-1 overflow-hidden">
|
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<div className="p-4 space-y-2">
|
|
||||||
{autoModeActivityLog.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<p className="text-sm">No activity yet</p>
|
|
||||||
<p className="text-xs mt-1">
|
|
||||||
Start auto mode to see activity here
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
autoModeActivityLog
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((activity) => (
|
|
||||||
<div
|
|
||||||
key={activity.id}
|
|
||||||
className={cn(
|
|
||||||
"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="flex-1 min-w-0">
|
|
||||||
<div className="flex items-baseline gap-2 mb-1">
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{formatTime(activity.timestamp)}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-medium text-blue-400 truncate">
|
|
||||||
{getFeatureDescription(activity.featureId)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-foreground break-words">
|
|
||||||
{activity.message}
|
|
||||||
</p>
|
|
||||||
{activity.tool && (
|
|
||||||
<div className="mt-1 flex items-center gap-1">
|
|
||||||
<Wrench className="w-3 h-3 text-muted-foreground" />
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{activity.tool}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
FeatureImagePath,
|
FeatureImagePath,
|
||||||
} from "@/store/app-store";
|
} from "@/store/app-store";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
@@ -50,7 +49,6 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { KanbanColumn } from "./kanban-column";
|
import { KanbanColumn } from "./kanban-column";
|
||||||
import { KanbanCard } from "./kanban-card";
|
import { KanbanCard } from "./kanban-card";
|
||||||
import { AutoModeLog } from "./auto-mode-log";
|
|
||||||
import { AgentOutputModal } from "./agent-output-modal";
|
import { AgentOutputModal } from "./agent-output-modal";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
@@ -58,8 +56,6 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
StopCircle,
|
StopCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
ChevronUp,
|
|
||||||
ChevronDown,
|
|
||||||
Users,
|
Users,
|
||||||
Trash2,
|
Trash2,
|
||||||
FastForward,
|
FastForward,
|
||||||
@@ -114,7 +110,6 @@ export function BoardView() {
|
|||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const [showActivityLog, setShowActivityLog] = useState(false);
|
|
||||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||||
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
||||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
|
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
|
||||||
@@ -342,12 +337,6 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}, [showAddDialog, defaultSkipTests]);
|
}, [showAddDialog, defaultSkipTests]);
|
||||||
|
|
||||||
// Auto-show activity log when auto mode starts
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoMode.isRunning && !showActivityLog) {
|
|
||||||
setShowActivityLog(true);
|
|
||||||
}
|
|
||||||
}, [autoMode.isRunning, showActivityLog]);
|
|
||||||
|
|
||||||
// Listen for auto mode feature completion and reload features
|
// Listen for auto mode feature completion and reload features
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -408,11 +397,12 @@ export function BoardView() {
|
|||||||
// Check which features have context files
|
// Check which features have context files
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAllContexts = async () => {
|
const checkAllContexts = async () => {
|
||||||
const inProgressFeatures = features.filter(
|
// Check context for in_progress, waiting_approval, and verified features
|
||||||
(f) => f.status === "in_progress"
|
const featuresWithPotentialContext = features.filter(
|
||||||
|
(f) => f.status === "in_progress" || f.status === "waiting_approval" || f.status === "verified"
|
||||||
);
|
);
|
||||||
const contextChecks = await Promise.all(
|
const contextChecks = await Promise.all(
|
||||||
inProgressFeatures.map(async (f) => ({
|
featuresWithPotentialContext.map(async (f) => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
hasContext: await checkContextExists(f.id),
|
hasContext: await checkContextExists(f.id),
|
||||||
}))
|
}))
|
||||||
@@ -659,6 +649,34 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete agent context file if it exists
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
const contextPath = `${currentProject.path}/.automaker/agents-context/${featureId}.md`;
|
||||||
|
await api.deleteFile(contextPath);
|
||||||
|
console.log(`[Board] Deleted agent context for feature ${featureId}`);
|
||||||
|
} catch (error) {
|
||||||
|
// Context file might not exist, which is fine
|
||||||
|
console.log(`[Board] Context file not found or already deleted for feature ${featureId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete attached images if they exist
|
||||||
|
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
for (const imagePathObj of feature.imagePaths) {
|
||||||
|
try {
|
||||||
|
await api.deleteFile(imagePathObj.path);
|
||||||
|
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the feature immediately without confirmation
|
// Remove the feature immediately without confirmation
|
||||||
removeFeature(featureId);
|
removeFeature(featureId);
|
||||||
};
|
};
|
||||||
@@ -1175,23 +1193,6 @@ export function BoardView() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isMounted && autoMode.isRunning && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowActivityLog(!showActivityLog)}
|
|
||||||
data-testid="toggle-activity-log"
|
|
||||||
>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin text-purple-500" />
|
|
||||||
Activity
|
|
||||||
{showActivityLog ? (
|
|
||||||
<ChevronDown className="w-4 h-4 ml-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronUp className="w-4 h-4 ml-2" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowAddDialog(true)}
|
onClick={() => setShowAddDialog(true)}
|
||||||
@@ -1212,12 +1213,7 @@ export function BoardView() {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* Kanban Columns */}
|
{/* Kanban Columns */}
|
||||||
<div
|
<div className="flex-1 overflow-x-auto p-4">
|
||||||
className={cn(
|
|
||||||
"flex-1 overflow-x-auto p-4",
|
|
||||||
showActivityLog && "transition-all"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={collisionDetectionStrategy}
|
collisionDetection={collisionDetectionStrategy}
|
||||||
@@ -1320,13 +1316,6 @@ export function BoardView() {
|
|||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Activity Log Panel */}
|
|
||||||
{showActivityLog && (
|
|
||||||
<div className="w-96 border-l border-border flex-shrink-0">
|
|
||||||
<AutoModeLog onClose={() => setShowActivityLog(false)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Feature Dialog */}
|
{/* Add Feature Dialog */}
|
||||||
@@ -1605,6 +1594,8 @@ export function BoardView() {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const verifiedFeatures = getColumnFeatures("verified");
|
const verifiedFeatures = getColumnFeatures("verified");
|
||||||
|
const api = getElectronAPI();
|
||||||
|
|
||||||
for (const feature of verifiedFeatures) {
|
for (const feature of verifiedFeatures) {
|
||||||
// Check if the feature is currently running
|
// Check if the feature is currently running
|
||||||
const isRunning = runningAutoTasks.includes(feature.id);
|
const isRunning = runningAutoTasks.includes(feature.id);
|
||||||
@@ -1621,6 +1612,16 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete agent context file if it exists
|
||||||
|
try {
|
||||||
|
const contextPath = `${currentProject.path}/.automaker/agents-context/${feature.id}.md`;
|
||||||
|
await api.deleteFile(contextPath);
|
||||||
|
console.log(`[Board] Deleted agent context for feature ${feature.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
// Context file might not exist, which is fine
|
||||||
|
console.debug("[Board] No context file to delete for feature:", feature.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the feature
|
// Remove the feature
|
||||||
removeFeature(feature.id);
|
removeFeature(feature.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Feature, useAppStore } from "@/store/app-store";
|
import { Feature, useAppStore } from "@/store/app-store";
|
||||||
import {
|
import {
|
||||||
GripVertical,
|
GripVertical,
|
||||||
@@ -32,7 +38,7 @@ import {
|
|||||||
PlayCircle,
|
PlayCircle,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
StopCircle,
|
StopCircle,
|
||||||
FlaskConical,
|
Hand,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
@@ -41,6 +47,8 @@ import {
|
|||||||
ListTodo,
|
ListTodo,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Expand,
|
Expand,
|
||||||
|
FileText,
|
||||||
|
MoreVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
@@ -101,9 +109,6 @@ export function KanbanCard({
|
|||||||
kanbanCardDetailLevel === "standard" ||
|
kanbanCardDetailLevel === "standard" ||
|
||||||
kanbanCardDetailLevel === "detailed";
|
kanbanCardDetailLevel === "detailed";
|
||||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||||
const showProgressBar =
|
|
||||||
kanbanCardDetailLevel === "standard" ||
|
|
||||||
kanbanCardDetailLevel === "detailed";
|
|
||||||
|
|
||||||
// Load context file for in_progress, waiting_approval, and verified features
|
// Load context file for in_progress, waiting_approval, and verified features
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -219,17 +224,14 @@ export function KanbanCard({
|
|||||||
data-testid={`skip-tests-badge-${feature.id}`}
|
data-testid={`skip-tests-badge-${feature.id}`}
|
||||||
title="Manual verification required"
|
title="Manual verification required"
|
||||||
>
|
>
|
||||||
<FlaskConical className="w-3 h-3" />
|
<Hand className="w-3 h-3" />
|
||||||
<span>Manual</span>
|
<span>Manual</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CardHeader className="p-3 pb-2">
|
<CardHeader className="p-3 pb-2">
|
||||||
{isCurrentAutoTask && (
|
{isCurrentAutoTask && (
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
||||||
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
|
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
|
||||||
<span className="text-xs text-running-indicator font-medium">
|
|
||||||
Running...
|
|
||||||
</span>
|
|
||||||
{feature.startedAt && (
|
{feature.startedAt && (
|
||||||
<CountUpTimer
|
<CountUpTimer
|
||||||
startedAt={feature.startedAt}
|
startedAt={feature.startedAt}
|
||||||
@@ -238,17 +240,36 @@ export function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Show timer for in_progress cards that aren't currently running */}
|
{!isCurrentAutoTask && (
|
||||||
{!isCurrentAutoTask &&
|
<div className="absolute top-2 right-2">
|
||||||
feature.status === "in_progress" &&
|
<DropdownMenu>
|
||||||
feature.startedAt && (
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="absolute top-2 right-2">
|
<Button
|
||||||
<CountUpTimer
|
variant="ghost"
|
||||||
startedAt={feature.startedAt}
|
size="sm"
|
||||||
className="text-yellow-500"
|
className="h-6 w-6 p-0 hover:bg-white/10"
|
||||||
/>
|
onClick={(e) => e.stopPropagation()}
|
||||||
</div>
|
data-testid={`menu-${feature.id}`}
|
||||||
)}
|
>
|
||||||
|
<MoreVertical className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteClick(e as unknown as React.MouseEvent);
|
||||||
|
}}
|
||||||
|
data-testid={`delete-feature-${feature.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
{isDraggable && (
|
{isDraggable && (
|
||||||
<div
|
<div
|
||||||
@@ -295,29 +316,6 @@ export function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
{/* 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
|
|
||||||
}%)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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 */}
|
{/* Detailed mode: Show all agent info */}
|
||||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||||
<div className="mb-3 space-y-2">
|
<div className="mb-3 space-y-2">
|
||||||
@@ -346,39 +344,6 @@ export function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
|
||||||
{(isCurrentAutoTask || feature.status === "in_progress") && (
|
|
||||||
<div className="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">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Wrench className="w-2.5 h-2.5" />
|
|
||||||
{agentInfo.toolCallCount} tools
|
|
||||||
</span>
|
|
||||||
{agentInfo.lastToolUsed && (
|
|
||||||
<span
|
|
||||||
className="text-zinc-500 truncate max-w-[80px]"
|
|
||||||
title={agentInfo.lastToolUsed}
|
|
||||||
>
|
|
||||||
{agentInfo.lastToolUsed}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Task List Progress (if todos found) */}
|
{/* Task List Progress (if todos found) */}
|
||||||
{agentInfo.todos.length > 0 && (
|
{agentInfo.todos.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -498,8 +463,8 @@ export function KanbanCard({
|
|||||||
}}
|
}}
|
||||||
data-testid={`view-output-${feature.id}`}
|
data-testid={`view-output-${feature.id}`}
|
||||||
>
|
>
|
||||||
<Eye className="w-3 h-3 mr-1" />
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
View Output
|
Logs
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onForceStop && (
|
{onForceStop && (
|
||||||
@@ -526,7 +491,7 @@ export function KanbanCard({
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
|
className="flex-1 h-7 text-xs bg-primary hover:bg-primary/90"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onManualVerify();
|
onManualVerify();
|
||||||
@@ -576,23 +541,30 @@ export function KanbanCard({
|
|||||||
}}
|
}}
|
||||||
data-testid={`view-output-inprogress-${feature.id}`}
|
data-testid={`view-output-inprogress-${feature.id}`}
|
||||||
>
|
>
|
||||||
<Eye className="w-3 h-3 mr-1" />
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
Output
|
Logs
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
data-testid={`delete-inprogress-feature-${feature.id}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isCurrentAutoTask && feature.status === "verified" && (
|
{!isCurrentAutoTask && feature.status === "verified" && (
|
||||||
<>
|
<>
|
||||||
|
{/* Logs button if context exists */}
|
||||||
|
{hasContext && onViewOutput && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewOutput();
|
||||||
|
}}
|
||||||
|
data-testid={`view-output-verified-${feature.id}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{/* Move back button for skipTests verified features */}
|
{/* Move back button for skipTests verified features */}
|
||||||
{feature.skipTests && onMoveBackToInProgress && (
|
{feature.skipTests && onMoveBackToInProgress && (
|
||||||
<Button
|
<Button
|
||||||
@@ -622,25 +594,32 @@ export function KanbanCard({
|
|||||||
<Edit className="w-3 h-3 mr-1" />
|
<Edit className="w-3 h-3 mr-1" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
data-testid={`delete-feature-${feature.id}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
|
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
|
||||||
<>
|
<>
|
||||||
|
{/* Logs button if context exists */}
|
||||||
|
{hasContext && onViewOutput && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewOutput();
|
||||||
|
}}
|
||||||
|
data-testid={`view-output-waiting-${feature.id}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{/* Follow-up prompt button */}
|
{/* Follow-up prompt button */}
|
||||||
{onFollowUp && (
|
{onFollowUp && (
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1 h-7 text-xs bg-action-followup hover:bg-action-followup-hover"
|
className="flex-1 h-7 text-xs"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onFollowUp();
|
onFollowUp();
|
||||||
@@ -656,7 +635,7 @@ export function KanbanCard({
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1 h-7 text-xs bg-action-commit hover:bg-action-commit-hover"
|
className="flex-1 h-7 text-xs"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onCommit();
|
onCommit();
|
||||||
@@ -667,19 +646,25 @@ export function KanbanCard({
|
|||||||
Commit
|
Commit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
data-testid={`delete-waiting-feature-${feature.id}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isCurrentAutoTask && feature.status === "backlog" && (
|
{!isCurrentAutoTask && feature.status === "backlog" && (
|
||||||
<>
|
<>
|
||||||
|
{onViewOutput && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewOutput();
|
||||||
|
}}
|
||||||
|
data-testid={`view-logs-backlog-${feature.id}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -693,15 +678,6 @@ export function KanbanCard({
|
|||||||
<Edit className="w-3 h-3 mr-1" />
|
<Edit className="w-3 h-3 mr-1" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
data-testid={`delete-feature-${feature.id}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -407,150 +407,162 @@ export function SettingsView() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-foreground">Theme</Label>
|
<Label className="text-foreground">Theme</Label>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "dark" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("dark")}
|
onClick={() => setTheme("dark")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="dark-mode-button"
|
data-testid="dark-mode-button"
|
||||||
>
|
>
|
||||||
<Moon className="w-4 h-4" />
|
<Moon className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Dark</span>
|
<span className="font-medium text-sm">Dark</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "light" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("light")}
|
onClick={() => setTheme("light")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "light"
|
theme === "light"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="light-mode-button"
|
data-testid="light-mode-button"
|
||||||
>
|
>
|
||||||
<Sun className="w-4 h-4" />
|
<Sun className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Light</span>
|
<span className="font-medium text-sm">Light</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "retro" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("retro")}
|
onClick={() => setTheme("retro")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "retro"
|
theme === "retro"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="retro-mode-button"
|
data-testid="retro-mode-button"
|
||||||
>
|
>
|
||||||
<Terminal className="w-4 h-4" />
|
<Terminal className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Retro</span>
|
<span className="font-medium text-sm">Retro</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "dracula" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("dracula")}
|
onClick={() => setTheme("dracula")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "dracula"
|
theme === "dracula"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="dracula-mode-button"
|
data-testid="dracula-mode-button"
|
||||||
>
|
>
|
||||||
<Ghost className="w-4 h-4" />
|
<Ghost className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Dracula</span>
|
<span className="font-medium text-sm">Dracula</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "nord" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("nord")}
|
onClick={() => setTheme("nord")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "nord"
|
theme === "nord"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="nord-mode-button"
|
data-testid="nord-mode-button"
|
||||||
>
|
>
|
||||||
<Snowflake className="w-4 h-4" />
|
<Snowflake className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Nord</span>
|
<span className="font-medium text-sm">Nord</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "monokai" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("monokai")}
|
onClick={() => setTheme("monokai")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "monokai"
|
theme === "monokai"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="monokai-mode-button"
|
data-testid="monokai-mode-button"
|
||||||
>
|
>
|
||||||
<Flame className="w-4 h-4" />
|
<Flame className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Monokai</span>
|
<span className="font-medium text-sm">Monokai</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "tokyonight" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("tokyonight")}
|
onClick={() => setTheme("tokyonight")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "tokyonight"
|
theme === "tokyonight"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="tokyonight-mode-button"
|
data-testid="tokyonight-mode-button"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-4 h-4" />
|
<Sparkles className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Tokyo Night</span>
|
<span className="font-medium text-sm">Tokyo Night</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "solarized" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("solarized")}
|
onClick={() => setTheme("solarized")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "solarized"
|
theme === "solarized"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="solarized-mode-button"
|
data-testid="solarized-mode-button"
|
||||||
>
|
>
|
||||||
<Eclipse className="w-4 h-4" />
|
<Eclipse className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Solarized</span>
|
<span className="font-medium text-sm">Solarized</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "gruvbox" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("gruvbox")}
|
onClick={() => setTheme("gruvbox")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "gruvbox"
|
theme === "gruvbox"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="gruvbox-mode-button"
|
data-testid="gruvbox-mode-button"
|
||||||
>
|
>
|
||||||
<Trees className="w-4 h-4" />
|
<Trees className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Gruvbox</span>
|
<span className="font-medium text-sm">Gruvbox</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "catppuccin" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("catppuccin")}
|
onClick={() => setTheme("catppuccin")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "catppuccin"
|
theme === "catppuccin"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="catppuccin-mode-button"
|
data-testid="catppuccin-mode-button"
|
||||||
>
|
>
|
||||||
<Cat className="w-4 h-4" />
|
<Cat className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Catppuccin</span>
|
<span className="font-medium text-sm">Catppuccin</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "onedark" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("onedark")}
|
onClick={() => setTheme("onedark")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "onedark"
|
theme === "onedark"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="onedark-mode-button"
|
data-testid="onedark-mode-button"
|
||||||
>
|
>
|
||||||
<Atom className="w-4 h-4" />
|
<Atom className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">One Dark</span>
|
<span className="font-medium text-sm">One Dark</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={theme === "synthwave" ? "secondary" : "outline"}
|
||||||
onClick={() => setTheme("synthwave")}
|
onClick={() => setTheme("synthwave")}
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||||
theme === "synthwave"
|
theme === "synthwave"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="synthwave-mode-button"
|
data-testid="synthwave-mode-button"
|
||||||
>
|
>
|
||||||
<Radio className="w-4 h-4" />
|
<Radio className="w-4 h-4" />
|
||||||
<span className="font-medium text-sm">Synthwave</span>
|
<span className="font-medium text-sm">Synthwave</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,12 +585,13 @@ export function SettingsView() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-foreground">Detail Level</Label>
|
<Label className="text-foreground">Detail Level</Label>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<button
|
<Button
|
||||||
|
variant={kanbanCardDetailLevel === "minimal" ? "secondary" : "outline"}
|
||||||
onClick={() => setKanbanCardDetailLevel("minimal")}
|
onClick={() => setKanbanCardDetailLevel("minimal")}
|
||||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||||
kanbanCardDetailLevel === "minimal"
|
kanbanCardDetailLevel === "minimal"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="kanban-detail-minimal"
|
data-testid="kanban-detail-minimal"
|
||||||
>
|
>
|
||||||
@@ -587,13 +600,14 @@ export function SettingsView() {
|
|||||||
<span className="text-xs text-muted-foreground text-center">
|
<span className="text-xs text-muted-foreground text-center">
|
||||||
Title & category only
|
Title & category only
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={kanbanCardDetailLevel === "standard" ? "secondary" : "outline"}
|
||||||
onClick={() => setKanbanCardDetailLevel("standard")}
|
onClick={() => setKanbanCardDetailLevel("standard")}
|
||||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||||
kanbanCardDetailLevel === "standard"
|
kanbanCardDetailLevel === "standard"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="kanban-detail-standard"
|
data-testid="kanban-detail-standard"
|
||||||
>
|
>
|
||||||
@@ -602,13 +616,14 @@ export function SettingsView() {
|
|||||||
<span className="text-xs text-muted-foreground text-center">
|
<span className="text-xs text-muted-foreground text-center">
|
||||||
Steps & progress
|
Steps & progress
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={kanbanCardDetailLevel === "detailed" ? "secondary" : "outline"}
|
||||||
onClick={() => setKanbanCardDetailLevel("detailed")}
|
onClick={() => setKanbanCardDetailLevel("detailed")}
|
||||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||||
kanbanCardDetailLevel === "detailed"
|
kanbanCardDetailLevel === "detailed"
|
||||||
? "bg-accent border-brand-500 text-foreground"
|
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
data-testid="kanban-detail-detailed"
|
data-testid="kanban-detail-detailed"
|
||||||
>
|
>
|
||||||
@@ -617,7 +632,7 @@ export function SettingsView() {
|
|||||||
<span className="text-xs text-muted-foreground text-center">
|
<span className="text-xs text-muted-foreground text-center">
|
||||||
Model, tools & tasks
|
Model, tools & tasks
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
<strong>Minimal:</strong> Shows only title and category
|
<strong>Minimal:</strong> Shows only title and category
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { initializeProject } from "@/lib/project-init";
|
|||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Plus,
|
Plus,
|
||||||
Cpu,
|
|
||||||
Folder,
|
Folder,
|
||||||
Clock,
|
Clock,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -284,8 +283,12 @@ export function WelcomeView() {
|
|||||||
<div className="flex-shrink-0 border-b border-border bg-glass 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="px-8 py-6">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<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">
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center">
|
||||||
<Cpu className="w-5 h-5 text-primary-foreground" />
|
<img
|
||||||
|
src="/icon_gold.png"
|
||||||
|
alt="Automaker Logo"
|
||||||
|
className="w-10 h-10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">
|
<h1 className="text-2xl font-bold text-foreground">
|
||||||
|
|||||||
@@ -148,11 +148,11 @@ export function useAutoMode() {
|
|||||||
throw new Error("Auto mode API not available");
|
throw new Error("Auto mode API not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.autoMode.start(currentProject.path);
|
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAutoModeRunning(true);
|
setAutoModeRunning(true);
|
||||||
console.log("[AutoMode] Started successfully");
|
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to start:", result.error);
|
console.error("[AutoMode] Failed to start:", result.error);
|
||||||
throw new Error(result.error || "Failed to start auto mode");
|
throw new Error(result.error || "Failed to start auto mode");
|
||||||
@@ -162,7 +162,7 @@ export function useAutoMode() {
|
|||||||
setAutoModeRunning(false);
|
setAutoModeRunning(false);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [currentProject, setAutoModeRunning]);
|
}, [currentProject, setAutoModeRunning, maxConcurrency]);
|
||||||
|
|
||||||
// Stop auto mode
|
// Stop auto mode
|
||||||
const stop = useCallback(async () => {
|
const stop = useCallback(async () => {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function useElectronAgent({
|
|||||||
imageCount: images?.length || 0
|
imageCount: images?.length || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save images to temp files and get paths
|
// Save images to .automaker/images and get paths
|
||||||
let imagePaths: string[] | undefined;
|
let imagePaths: string[] | undefined;
|
||||||
if (images && images.length > 0) {
|
if (images && images.length > 0) {
|
||||||
imagePaths = [];
|
imagePaths = [];
|
||||||
@@ -70,11 +70,12 @@ export function useElectronAgent({
|
|||||||
const result = await window.electronAPI.saveImageToTemp(
|
const result = await window.electronAPI.saveImageToTemp(
|
||||||
image.data,
|
image.data,
|
||||||
image.filename,
|
image.filename,
|
||||||
image.mimeType
|
image.mimeType,
|
||||||
|
workingDirectory // Pass workingDirectory as projectPath
|
||||||
);
|
);
|
||||||
if (result.success && result.path) {
|
if (result.success && result.path) {
|
||||||
imagePaths.push(result.path);
|
imagePaths.push(result.path);
|
||||||
console.log("[useElectronAgent] Saved image to temp:", result.path);
|
console.log("[useElectronAgent] Saved image to .automaker/images:", result.path);
|
||||||
} else {
|
} else {
|
||||||
console.error("[useElectronAgent] Failed to save image:", result.error);
|
console.error("[useElectronAgent] Failed to save image:", result.error);
|
||||||
}
|
}
|
||||||
@@ -304,7 +305,7 @@ export function useElectronAgent({
|
|||||||
imageCount: images?.length || 0
|
imageCount: images?.length || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save images to temp files and get paths
|
// Save images to .automaker/images and get paths
|
||||||
let imagePaths: string[] | undefined;
|
let imagePaths: string[] | undefined;
|
||||||
if (images && images.length > 0) {
|
if (images && images.length > 0) {
|
||||||
imagePaths = [];
|
imagePaths = [];
|
||||||
@@ -312,11 +313,12 @@ export function useElectronAgent({
|
|||||||
const result = await window.electronAPI.saveImageToTemp(
|
const result = await window.electronAPI.saveImageToTemp(
|
||||||
image.data,
|
image.data,
|
||||||
image.filename,
|
image.filename,
|
||||||
image.mimeType
|
image.mimeType,
|
||||||
|
workingDirectory // Pass workingDirectory as projectPath
|
||||||
);
|
);
|
||||||
if (result.success && result.path) {
|
if (result.success && result.path) {
|
||||||
imagePaths.push(result.path);
|
imagePaths.push(result.path);
|
||||||
console.log("[useElectronAgent] Saved image to temp:", result.path);
|
console.log("[useElectronAgent] Saved image to .automaker/images:", result.path);
|
||||||
} else {
|
} else {
|
||||||
console.error("[useElectronAgent] Failed to save image:", result.error);
|
console.error("[useElectronAgent] Failed to save image:", result.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export interface AutoModeEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
start: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
|
start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>;
|
||||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||||
stopFeature: (featureId: string) => Promise<{ success: boolean; error?: string }>;
|
stopFeature: (featureId: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; error?: string }>;
|
status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; error?: string }>;
|
||||||
@@ -370,12 +370,13 @@ let mockAutoModeTimeouts = new Map<string, NodeJS.Timeout>(); // Track timeouts
|
|||||||
|
|
||||||
function createMockAutoModeAPI(): AutoModeAPI {
|
function createMockAutoModeAPI(): AutoModeAPI {
|
||||||
return {
|
return {
|
||||||
start: async (projectPath: string) => {
|
start: async (projectPath: string, maxConcurrency?: number) => {
|
||||||
if (mockAutoModeRunning) {
|
if (mockAutoModeRunning) {
|
||||||
return { success: false, error: "Auto mode is already running" };
|
return { success: false, error: "Auto mode is already running" };
|
||||||
}
|
}
|
||||||
|
|
||||||
mockAutoModeRunning = true;
|
mockAutoModeRunning = true;
|
||||||
|
console.log(`[Mock] Auto mode started with maxConcurrency: ${maxConcurrency || 3}`);
|
||||||
const featureId = "auto-mode-0";
|
const featureId = "auto-mode-0";
|
||||||
mockRunningFeatures.add(featureId);
|
mockRunningFeatures.add(featureId);
|
||||||
|
|
||||||
|
|||||||
@@ -323,11 +323,11 @@ export function getLogTypeColors(type: LogEntryType): {
|
|||||||
};
|
};
|
||||||
case "debug":
|
case "debug":
|
||||||
return {
|
return {
|
||||||
bg: "bg-purple-500/10",
|
bg: "bg-primary/10",
|
||||||
border: "border-l-purple-500",
|
border: "border-l-primary",
|
||||||
text: "text-purple-300",
|
text: "text-primary",
|
||||||
icon: "text-purple-400",
|
icon: "text-primary",
|
||||||
badge: "bg-purple-500/20 text-purple-300",
|
badge: "bg-primary/20 text-primary",
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export interface AppActions {
|
|||||||
addProject: (project: Project) => void;
|
addProject: (project: Project) => void;
|
||||||
removeProject: (projectId: string) => void;
|
removeProject: (projectId: string) => void;
|
||||||
setCurrentProject: (project: Project | null) => void;
|
setCurrentProject: (project: Project | null) => void;
|
||||||
|
reorderProjects: (oldIndex: number, newIndex: number) => void;
|
||||||
|
|
||||||
// View actions
|
// View actions
|
||||||
setCurrentView: (view: ViewMode) => void;
|
setCurrentView: (view: ViewMode) => void;
|
||||||
@@ -268,6 +269,13 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
set({ projects: get().projects.filter((p) => p.id !== projectId) });
|
set({ projects: get().projects.filter((p) => p.id !== projectId) });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reorderProjects: (oldIndex, newIndex) => {
|
||||||
|
const projects = [...get().projects];
|
||||||
|
const [movedProject] = projects.splice(oldIndex, 1);
|
||||||
|
projects.splice(newIndex, 0, movedProject);
|
||||||
|
set({ projects });
|
||||||
|
},
|
||||||
|
|
||||||
setCurrentProject: (project) => {
|
setCurrentProject: (project) => {
|
||||||
set({ currentProject: project });
|
set({ currentProject: project });
|
||||||
if (project) {
|
if (project) {
|
||||||
|
|||||||
3
app/src/types/electron.d.ts
vendored
3
app/src/types/electron.d.ts
vendored
@@ -300,7 +300,8 @@ export interface ElectronAPI {
|
|||||||
saveImageToTemp: (
|
saveImageToTemp: (
|
||||||
data: string,
|
data: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
mimeType: string
|
mimeType: string,
|
||||||
|
projectPath?: string
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|||||||
126
docs/release.md
Normal file
126
docs/release.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Release Command
|
||||||
|
|
||||||
|
This command creates a git tag with a version bump and description of changes.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/release [major|minor|patch] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/release minor "✨ Added inventory drag and drop functionality"`
|
||||||
|
- `/release patch "🐛 Fixed bug with item selection"`
|
||||||
|
- `/release major "💥 Breaking: Refactored API endpoints"`
|
||||||
|
- `/release minor "Version 0.20.0: Added new features and improvements"`
|
||||||
|
|
||||||
|
## Steps to Execute
|
||||||
|
|
||||||
|
### 1. Parse Version Type and Description
|
||||||
|
|
||||||
|
- Extract the version type from the command: `major`, `minor`, or `patch`
|
||||||
|
- Extract the description (rest of the command, if provided)
|
||||||
|
- If no version type provided or invalid, show usage and exit
|
||||||
|
- Description is optional - if not provided, will auto-generate from commits
|
||||||
|
|
||||||
|
### 2. Generate Changelog from Commits
|
||||||
|
|
||||||
|
- Find the last git tag (version tag):
|
||||||
|
```bash
|
||||||
|
git describe --tags --abbrev=0
|
||||||
|
```
|
||||||
|
- If no previous tag exists, use the initial commit or handle gracefully
|
||||||
|
- Get all commits between the last tag and HEAD:
|
||||||
|
```bash
|
||||||
|
git log <last-tag>..HEAD --pretty=format:"%h %s" --no-merges
|
||||||
|
```
|
||||||
|
- Parse commit messages and generate a changelog description:
|
||||||
|
- Group commits by type (feature, fix, improvement, etc.) based on commit message patterns
|
||||||
|
- Use emojis to categorize changes (see Emoji Usage section)
|
||||||
|
- Format as a multi-line changelog with categorized entries
|
||||||
|
- If user provided a description, prepend it to the auto-generated changelog
|
||||||
|
- If no commits found, use a default message or prompt user
|
||||||
|
|
||||||
|
### 3. Read Current Version
|
||||||
|
|
||||||
|
- Read `app/package.json` to get the current version (e.g., "0.1.0")
|
||||||
|
- Parse the version into major, minor, and patch components
|
||||||
|
- Calculate the new version based on the type:
|
||||||
|
- **major**: `${major + 1}.0.0` (e.g., 0.1.0 → 1.0.0)
|
||||||
|
- **minor**: `${major}.${minor + 1}.0` (e.g., 0.1.0 → 0.2.0)
|
||||||
|
- **patch**: `${major}.${minor}.${patch + 1}` (e.g., 0.1.0 → 0.1.1)
|
||||||
|
|
||||||
|
### 4. Create Git Tag
|
||||||
|
|
||||||
|
- Create an annotated git tag with the new version and description:
|
||||||
|
```bash
|
||||||
|
git tag -a v<new-version> -m "<description>"
|
||||||
|
```
|
||||||
|
- Example: `git tag -a v0.2.0 -m "✨ Added inventory drag and drop functionality"`
|
||||||
|
|
||||||
|
### 5. Push Tag to Remote
|
||||||
|
|
||||||
|
- Push the tag to remote:
|
||||||
|
```bash
|
||||||
|
git push origin v<new-version>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Emoji Usage
|
||||||
|
|
||||||
|
You can use emojis in release notes to categorize changes:
|
||||||
|
|
||||||
|
- ✨ **New features** - New functionality, features, additions
|
||||||
|
- 🐛 **Bug fixes** - Bug fixes and error corrections
|
||||||
|
- 🔧 **Improvements** - Refactoring, optimizations, code quality
|
||||||
|
- ⚡ **Performance** - Performance improvements
|
||||||
|
- 💥 **Breaking changes** - Breaking API changes, major refactors
|
||||||
|
- 🎨 **UI/UX** - Visual and user experience updates
|
||||||
|
- ⚙️ **Configuration** - Config and environment changes
|
||||||
|
- 📝 **Documentation** - Documentation updates
|
||||||
|
- 🏗️ **Infrastructure** - Build, deployment, infrastructure
|
||||||
|
- 🎵 **Audio** - Sound effects, music, audio changes
|
||||||
|
|
||||||
|
## Changelog Generation
|
||||||
|
|
||||||
|
The release command automatically generates a changelog by analyzing commits between the last tag and HEAD:
|
||||||
|
|
||||||
|
1. **Find Last Tag**: Uses `git describe --tags --abbrev=0` to find the most recent version tag
|
||||||
|
2. **Get Commits**: Retrieves all commits between the last tag and HEAD using `git log <last-tag>..HEAD`
|
||||||
|
3. **Parse and Categorize**: Analyzes commit messages to categorize changes:
|
||||||
|
- Looks for conventional commit patterns (feat:, fix:, refactor:, etc.)
|
||||||
|
- Detects emoji prefixes in commit messages
|
||||||
|
- Groups similar changes together
|
||||||
|
4. **Generate Description**: Creates a formatted changelog with:
|
||||||
|
- User-provided description (if any) at the top
|
||||||
|
- Categorized list of changes with appropriate emojis
|
||||||
|
- Commit hash references for traceability
|
||||||
|
|
||||||
|
### Example Generated Changelog
|
||||||
|
|
||||||
|
```
|
||||||
|
✨ Added inventory drag and drop functionality
|
||||||
|
|
||||||
|
Changes since v0.1.0:
|
||||||
|
|
||||||
|
✨ Features:
|
||||||
|
- Add drag and drop support for inventory items (abc1234)
|
||||||
|
- Implement new sidebar navigation (def5678)
|
||||||
|
|
||||||
|
🐛 Bug Fixes:
|
||||||
|
- Fix item selection bug in list view (ghi9012)
|
||||||
|
- Resolve memory leak in component cleanup (jkl3456)
|
||||||
|
|
||||||
|
🔧 Improvements:
|
||||||
|
- Refactor API endpoint structure (mno7890)
|
||||||
|
- Optimize database queries (pqr2345)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The tag message should describe what changed in this release
|
||||||
|
- Use descriptive messages with emojis to categorize changes
|
||||||
|
- Tags follow semantic versioning (e.g., v0.1.0, v0.2.0, v1.0.0)
|
||||||
|
- Version is automatically calculated based on the type specified
|
||||||
|
- If no previous tag exists, all commits from the repository start will be included
|
||||||
|
- User-provided description (if any) will be prepended to the auto-generated changelog
|
||||||
Reference in New Issue
Block a user