diff --git a/apps/server/src/routes/features/routes/create.ts b/apps/server/src/routes/features/routes/create.ts
index a4df45a6..29f7d075 100644
--- a/apps/server/src/routes/features/routes/create.ts
+++ b/apps/server/src/routes/features/routes/create.ts
@@ -43,7 +43,7 @@ export function createCreateHandler(featureLoader: FeatureLoader, events?: Event
if (events) {
events.emit('feature:created', {
featureId: created.id,
- featureName: created.name,
+ featureName: created.title || 'Untitled Feature',
projectPath,
});
}
diff --git a/apps/server/src/routes/fs/routes/save-board-background.ts b/apps/server/src/routes/fs/routes/save-board-background.ts
index e8988c6c..a0c2164a 100644
--- a/apps/server/src/routes/fs/routes/save-board-background.ts
+++ b/apps/server/src/routes/fs/routes/save-board-background.ts
@@ -31,7 +31,9 @@ export function createSaveBoardBackgroundHandler() {
await secureFs.mkdir(boardDir, { recursive: true });
// Decode base64 data (remove data URL prefix if present)
- const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
+ // Use a regex that handles all data URL formats including those with extra params
+ // e.g., data:image/gif;charset=utf-8;base64,R0lGOD...
+ const base64Data = data.replace(/^data:[^,]+,/, '');
const buffer = Buffer.from(base64Data, 'base64');
// Use a fixed filename for the board background (overwrite previous)
diff --git a/apps/server/src/routes/fs/routes/save-image.ts b/apps/server/src/routes/fs/routes/save-image.ts
index 059abfaf..c8cfdda7 100644
--- a/apps/server/src/routes/fs/routes/save-image.ts
+++ b/apps/server/src/routes/fs/routes/save-image.ts
@@ -31,7 +31,9 @@ export function createSaveImageHandler() {
await secureFs.mkdir(imagesDir, { recursive: true });
// Decode base64 data (remove data URL prefix if present)
- const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
+ // Use a regex that handles all data URL formats including those with extra params
+ // e.g., data:image/gif;charset=utf-8;base64,R0lGOD...
+ const base64Data = data.replace(/^data:[^,]+,/, '');
const buffer = Buffer.from(base64Data, 'base64');
// Generate unique filename with timestamp
diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts
index 1fae8907..36ae4a2e 100644
--- a/apps/server/src/services/auto-mode-service.ts
+++ b/apps/server/src/services/auto-mode-service.ts
@@ -4597,21 +4597,54 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
planVersion,
});
- // Build revision prompt
- let revisionPrompt = `The user has requested revisions to the plan/specification.
+ // Build revision prompt using customizable template
+ const revisionPrompts = await getPromptCustomization(
+ this.settingsService,
+ '[AutoMode]'
+ );
-## Previous Plan (v${planVersion - 1})
-${hasEdits ? approvalResult.editedPlan : currentPlanContent}
+ // Get task format example based on planning mode
+ const taskFormatExample =
+ planningMode === 'full'
+ ? `\`\`\`tasks
+## Phase 1: Foundation
+- [ ] T001: [Description] | File: [path/to/file]
+- [ ] T002: [Description] | File: [path/to/file]
-## User Feedback
-${approvalResult.feedback || 'Please revise the plan based on the edits above.'}
+## Phase 2: Core Implementation
+- [ ] T003: [Description] | File: [path/to/file]
+- [ ] T004: [Description] | File: [path/to/file]
+\`\`\``
+ : `\`\`\`tasks
+- [ ] T001: [Description] | File: [path/to/file]
+- [ ] T002: [Description] | File: [path/to/file]
+- [ ] T003: [Description] | File: [path/to/file]
+\`\`\``;
-## Instructions
-Please regenerate the specification incorporating the user's feedback.
-Keep the same format with the \`\`\`tasks block for task definitions.
-After generating the revised spec, output:
-"[SPEC_GENERATED] Please review the revised specification above."
-`;
+ let revisionPrompt = revisionPrompts.taskExecution.planRevisionTemplate;
+ revisionPrompt = revisionPrompt.replace(
+ /\{\{planVersion\}\}/g,
+ String(planVersion - 1)
+ );
+ revisionPrompt = revisionPrompt.replace(
+ /\{\{previousPlan\}\}/g,
+ hasEdits
+ ? approvalResult.editedPlan || currentPlanContent
+ : currentPlanContent
+ );
+ revisionPrompt = revisionPrompt.replace(
+ /\{\{userFeedback\}\}/g,
+ approvalResult.feedback ||
+ 'Please revise the plan based on the edits above.'
+ );
+ revisionPrompt = revisionPrompt.replace(
+ /\{\{planningMode\}\}/g,
+ planningMode
+ );
+ revisionPrompt = revisionPrompt.replace(
+ /\{\{taskFormatExample\}\}/g,
+ taskFormatExample
+ );
// Update status to regenerating
await this.updateFeaturePlanSpec(projectPath, featureId, {
@@ -4663,6 +4696,26 @@ After generating the revised spec, output:
const revisedTasks = parseTasksFromSpec(currentPlanContent);
logger.info(`Revised plan has ${revisedTasks.length} tasks`);
+ // Warn if no tasks found in spec/full mode - this may cause fallback to single-agent
+ if (
+ revisedTasks.length === 0 &&
+ (planningMode === 'spec' || planningMode === 'full')
+ ) {
+ logger.warn(
+ `WARNING: Revised plan in ${planningMode} mode has no tasks! ` +
+ `This will cause fallback to single-agent execution. ` +
+ `The AI may have omitted the required \`\`\`tasks block.`
+ );
+ this.emitAutoModeEvent('plan_revision_warning', {
+ featureId,
+ projectPath,
+ branchName,
+ planningMode,
+ warning:
+ 'Revised plan missing tasks block - will use single-agent execution',
+ });
+ }
+
// Update planSpec with revised content
await this.updateFeaturePlanSpec(projectPath, featureId, {
status: 'generated',
diff --git a/apps/server/src/services/event-hook-service.ts b/apps/server/src/services/event-hook-service.ts
index 9f73155f..2aedc7f4 100644
--- a/apps/server/src/services/event-hook-service.ts
+++ b/apps/server/src/services/event-hook-service.ts
@@ -169,9 +169,10 @@ export class EventHookService {
}
// Build context for variable substitution
+ // Use loaded featureName (from feature.title) or fall back to payload.featureName
const context: HookContext = {
featureId: payload.featureId,
- featureName: payload.featureName,
+ featureName: featureName || payload.featureName,
projectPath: payload.projectPath,
projectName: payload.projectPath ? this.extractProjectName(payload.projectPath) : undefined,
error: payload.error || payload.message,
diff --git a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx
index 0cb598b2..70ada5ee 100644
--- a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx
+++ b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx
@@ -15,6 +15,7 @@ import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import { getHttpApiClient } from '@/lib/http-api-client';
import type { Project } from '@/lib/electron';
import { IconPicker } from './icon-picker';
+import { toast } from 'sonner';
interface EditProjectDialogProps {
project: Project;
@@ -52,11 +53,18 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
+ toast.error(
+ `Invalid file type: ${file.type || 'unknown'}. Please use JPG, PNG, GIF or WebP.`
+ );
return;
}
- // Validate file size (max 2MB for icons)
- if (file.size > 2 * 1024 * 1024) {
+ // Validate file size (max 5MB for icons - allows animated GIFs)
+ const maxSize = 5 * 1024 * 1024;
+ if (file.size > maxSize) {
+ toast.error(
+ `File too large (${(file.size / 1024 / 1024).toFixed(2)} MB). Maximum size is 5 MB.`
+ );
return;
}
@@ -72,15 +80,24 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
file.type,
project.path
);
+
if (result.success && result.path) {
setCustomIconPath(result.path);
// Clear the Lucide icon when custom icon is set
setIcon(null);
+ toast.success('Icon uploaded successfully');
+ } else {
+ toast.error('Failed to upload icon');
}
setIsUploadingIcon(false);
};
+ reader.onerror = () => {
+ toast.error('Failed to read file');
+ setIsUploadingIcon(false);
+ };
reader.readAsDataURL(file);
} catch {
+ toast.error('Failed to upload icon');
setIsUploadingIcon(false);
}
};
@@ -162,7 +179,7 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
{isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
- PNG, JPG, GIF or WebP. Max 2MB.
+ PNG, JPG, GIF or WebP. Max 5MB.
diff --git a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx
index 249aa6a1..e8cf2f3f 100644
--- a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx
+++ b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx
@@ -59,7 +59,7 @@ interface ThemeButtonProps {
/** Handler for pointer leave events (used to clear preview) */
onPointerLeave: (e: React.PointerEvent) => void;
/** Handler for click events (used to select theme) */
- onClick: () => void;
+ onClick: (e: React.MouseEvent) => void;
}
/**
@@ -77,6 +77,7 @@ const ThemeButton = memo(function ThemeButton({
const Icon = option.icon;
return (