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 (

- PNG, JPG, GIF or WebP. Max 2MB. + PNG, JPG, GIF or WebP. Max 5MB.

diff --git a/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx b/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx index e9c5a071..a1c6bcba 100644 --- a/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx +++ b/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx @@ -19,6 +19,7 @@ import type { StoredEventSummary, StoredEvent, EventHookTrigger } from '@automak import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types'; import { getHttpApiClient } from '@/lib/http-api-client'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { toast } from 'sonner'; export function EventHistoryView() { const currentProject = useAppStore((state) => state.currentProject); @@ -85,16 +86,18 @@ export function EventHistoryView() { const failCount = hookResults.filter((r) => !r.success).length; if (hooksTriggered === 0) { - alert('No matching hooks found for this event trigger.'); + toast.info('No matching hooks found for this event trigger.'); } else if (failCount === 0) { - alert(`Successfully ran ${successCount} hook(s).`); + toast.success(`Successfully ran ${successCount} hook(s).`); } else { - alert(`Ran ${hooksTriggered} hook(s): ${successCount} succeeded, ${failCount} failed.`); + toast.warning( + `Ran ${hooksTriggered} hook(s): ${successCount} succeeded, ${failCount} failed.` + ); } } } catch (error) { console.error('Failed to replay event:', error); - alert('Failed to replay event. Check console for details.'); + toast.error('Failed to replay event. Check console for details.'); } finally { setReplayingEvent(null); } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index f468f07c..1f79ff07 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -946,7 +946,7 @@ export class HttpApiClient implements ElectronAPI { return response.json(); } - private async get(endpoint: string): Promise { + async get(endpoint: string): Promise { // Ensure API key is initialized before making request await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { @@ -976,7 +976,7 @@ export class HttpApiClient implements ElectronAPI { return response.json(); } - private async put(endpoint: string, body?: unknown): Promise { + async put(endpoint: string, body?: unknown): Promise { // Ensure API key is initialized before making request await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index a6d048fb..e36791f0 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; // Note: persist middleware removed - settings now sync via API (use-settings-sync.ts) import type { Project, TrashedProject } from '@/lib/electron'; -import { getElectronAPI } from '@/lib/electron'; +import { saveProjects, saveTrashedProjects } from '@/lib/electron'; import { getHttpApiClient } from '@/lib/http-api-client'; import { createLogger } from '@automaker/utils/logger'; // Note: setItem/getItem moved to ./utils/theme-utils.ts @@ -419,7 +419,7 @@ export const useAppStore = create()((set, get, store) => const trashedProject: TrashedProject = { ...project, - trashedAt: Date.now(), + trashedAt: new Date().toISOString(), }; set((state) => ({ @@ -428,12 +428,9 @@ export const useAppStore = create()((set, get, store) => currentProject: state.currentProject?.id === projectId ? null : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveProjects(get().projects); + saveTrashedProjects(get().trashedProjects); }, restoreTrashedProject: (projectId: string) => { @@ -449,12 +446,9 @@ export const useAppStore = create()((set, get, store) => trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveProjects(get().projects); + saveTrashedProjects(get().trashedProjects); }, deleteTrashedProject: (projectId: string) => { @@ -462,21 +456,15 @@ export const useAppStore = create()((set, get, store) => trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveTrashedProjects(get().trashedProjects); }, emptyTrash: () => { set({ trashedProjects: [] }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setTrashedProjects([]); - } + // Persist to storage + saveTrashedProjects([]); }, setCurrentProject: (project) => { @@ -533,14 +521,10 @@ export const useAppStore = create()((set, get, store) => get().addProject(newProject); get().setCurrentProject(newProject); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - // Small delay to ensure state is updated before persisting - setTimeout(() => { - electronAPI.projects.setProjects(get().projects); - }, 0); - } + // Persist to storage (small delay to ensure state is updated) + setTimeout(() => { + saveProjects(get().projects); + }, 0); return newProject; }, @@ -623,11 +607,8 @@ export const useAppStore = create()((set, get, store) => ), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectIcon: (projectId: string, icon: string | null) => { @@ -635,27 +616,31 @@ export const useAppStore = create()((set, get, store) => projects: state.projects.map((p) => p.id === projectId ? { ...p, icon: icon ?? undefined } : p ), + // Also update currentProject if it's the one being modified + currentProject: + state.currentProject?.id === projectId + ? { ...state.currentProject, icon: icon ?? undefined } + : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectCustomIcon: (projectId: string, customIconPath: string | null) => { set((state) => ({ projects: state.projects.map((p) => - p.id === projectId ? { ...p, customIcon: customIconPath ?? undefined } : p + p.id === projectId ? { ...p, customIconPath: customIconPath ?? undefined } : p ), + // Also update currentProject if it's the one being modified + currentProject: + state.currentProject?.id === projectId + ? { ...state.currentProject, customIconPath: customIconPath ?? undefined } + : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectName: (projectId: string, name: string) => { @@ -668,11 +653,8 @@ export const useAppStore = create()((set, get, store) => : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // View actions - provided by UI slice @@ -699,11 +681,8 @@ export const useAppStore = create()((set, get, store) => ); } - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Font actions (setFontSans, setFontMono, getEffectiveFontSans, getEffectiveFontMono provided by UI slice) @@ -719,11 +698,8 @@ export const useAppStore = create()((set, get, store) => : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectFontMono: (projectId: string, fontFamily: string | null) => { set((state) => ({ @@ -737,11 +713,8 @@ export const useAppStore = create()((set, get, store) => : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Claude API Profile actions (per-project override) @@ -757,11 +730,8 @@ export const useAppStore = create()((set, get, store) => : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Project Phase Model Overrides @@ -794,11 +764,8 @@ export const useAppStore = create()((set, get, store) => }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, clearAllProjectPhaseModelOverrides: (projectId: string) => { @@ -817,11 +784,8 @@ export const useAppStore = create()((set, get, store) => }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Project Default Feature Model Override @@ -843,11 +807,8 @@ export const useAppStore = create()((set, get, store) => }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Feature actions @@ -858,7 +819,7 @@ export const useAppStore = create()((set, get, store) => })), addFeature: (feature) => { const id = feature.id ?? `feature-${Date.now()}-${Math.random().toString(36).slice(2)}`; - const newFeature: Feature = { ...feature, id }; + const newFeature = { ...feature, id } as Feature; set((state) => ({ features: [...state.features, newFeature] })); return newFeature; }, @@ -2346,8 +2307,7 @@ export const useAppStore = create()((set, get, store) => try { const httpApi = getHttpApiClient(); - const response = await httpApi.get('/api/codex/models'); - const data = response.data as { + const data = await httpApi.get<{ success: boolean; models?: Array<{ id: string; @@ -2359,7 +2319,7 @@ export const useAppStore = create()((set, get, store) => isDefault: boolean; }>; error?: string; - }; + }>('/api/codex/models'); if (data.success && data.models) { set({ @@ -2417,8 +2377,7 @@ export const useAppStore = create()((set, get, store) => try { const httpApi = getHttpApiClient(); - const response = await httpApi.get('/api/opencode/models'); - const data = response.data as { + const data = await httpApi.get<{ success: boolean; models?: ModelDefinition[]; providers?: Array<{ @@ -2428,7 +2387,7 @@ export const useAppStore = create()((set, get, store) => authMethod?: string; }>; error?: string; - }; + }>('/api/setup/opencode/models'); if (data.success && data.models) { // Filter out Bedrock models diff --git a/libs/prompts/src/defaults.ts b/libs/prompts/src/defaults.ts index 48772de9..bcbc6feb 100644 --- a/libs/prompts/src/defaults.ts +++ b/libs/prompts/src/defaults.ts @@ -965,8 +965,20 @@ export const DEFAULT_PLAN_REVISION_TEMPLATE = `The user has requested revisions ## 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: +**Current planning mode: {{planningMode}}** + +**CRITICAL REQUIREMENT**: Your revised specification MUST include a \`\`\`tasks code block containing task definitions in the EXACT format shown below. This is MANDATORY - without the tasks block, the system cannot track or execute tasks properly. + +### Required Task Format +{{taskFormatExample}} + +**IMPORTANT**: +1. The \`\`\`tasks block must appear in your response +2. Each task MUST start with "- [ ] T###:" where ### is a sequential number (T001, T002, T003, etc.) +3. Each task MUST include "| File:" followed by the primary file path +4. Tasks should be ordered by dependencies (foundational tasks first) + +After generating the revised spec with the tasks block, output: "[SPEC_GENERATED] Please review the revised specification above."`; export const DEFAULT_CONTINUATION_AFTER_APPROVAL_TEMPLATE = `The plan/specification has been approved. Now implement it.