From d7c333733066e26a1bc0d3e738c4d49adf612ff0 Mon Sep 17 00:00:00 2001 From: Kacper Date: Mon, 26 Jan 2026 19:53:07 +0100 Subject: [PATCH 1/7] refactor(auto-mode): Enhance revision prompt customization and task format validation - Updated the revision prompt generation to utilize a customizable template, allowing for dynamic insertion of plan version, previous plan content, user feedback, and task format examples. - Added validation to ensure the presence of a tasks block in the revised specification, with clear instructions on the required format to prevent execution issues. - Introduced logging for scenarios where no tasks are found in the revised plan, warning about potential fallback to single-agent execution. --- apps/server/src/services/auto-mode-service.ts | 77 ++++++++++++++++--- libs/prompts/src/defaults.ts | 16 +++- 2 files changed, 79 insertions(+), 14 deletions(-) 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/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. From a60904bd5185d6c2047705b98c76e9f5ab76d29d Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 27 Jan 2026 00:09:55 +0100 Subject: [PATCH 2/7] fix(ui,server): Fix project icon updates and image upload issues - Fix setProjectCustomIcon using wrong property name (customIcon -> customIconPath) - Add currentProject state update to setProjectIcon and setProjectCustomIcon - Fix data URL regex to handle all formats (e.g., charset=utf-8 in GIFs) - Increase project icon size limit from 2MB to 5MB for animated GIFs - Add toast notifications for upload validation errors - Add image error fallback to folder icon in project switcher - Make HttpApiClient get/put methods public for store access - Fix TypeScript errors in app-store.ts (trashedAt type, font properties) Co-Authored-By: Claude Opus 4.5 --- .../routes/fs/routes/save-board-background.ts | 4 +- .../server/src/routes/fs/routes/save-image.ts | 4 +- .../components/edit-project-dialog.tsx | 23 ++- .../components/project-context-menu.tsx | 32 +++- .../components/project-switcher-item.tsx | 6 +- .../project-selector-with-options.tsx | 17 +- .../sidebar/components/theme-menu-item.tsx | 18 +-- .../project-identity-section.tsx | 8 +- apps/ui/src/lib/http-api-client.ts | 4 +- apps/ui/src/store/app-store.ts | 153 +++++++----------- 10 files changed, 129 insertions(+), 140 deletions(-) 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/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/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 52744339..aa4ff227 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 @@ -360,7 +360,7 @@ export const useAppStore = create()((set, get) => ({ const trashedProject: TrashedProject = { ...project, - trashedAt: Date.now(), + trashedAt: new Date().toISOString(), }; set((state) => ({ @@ -369,12 +369,9 @@ export const useAppStore = create()((set, get) => ({ 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) => { @@ -390,12 +387,9 @@ export const useAppStore = create()((set, get) => ({ 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) => { @@ -403,21 +397,15 @@ export const useAppStore = create()((set, get) => ({ 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) => { @@ -474,14 +462,10 @@ export const useAppStore = create()((set, get) => ({ 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; }, @@ -564,11 +548,8 @@ export const useAppStore = create()((set, get) => ({ ), })); - // 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) => { @@ -576,27 +557,31 @@ export const useAppStore = create()((set, get) => ({ 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) => { @@ -609,11 +594,8 @@ export const useAppStore = create()((set, get) => ({ : 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 @@ -659,11 +641,8 @@ export const useAppStore = create()((set, get) => ({ ); } - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, getEffectiveTheme: () => { const state = get(); @@ -696,11 +675,8 @@ export const useAppStore = create()((set, get) => ({ : 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) => ({ @@ -714,20 +690,17 @@ export const useAppStore = create()((set, get) => ({ : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, getEffectiveFontSans: () => { const state = get(); - const projectFont = state.currentProject?.fontSans; + const projectFont = state.currentProject?.fontFamilySans; return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS); }, getEffectiveFontMono: () => { const state = get(); - const projectFont = state.currentProject?.fontMono; + const projectFont = state.currentProject?.fontFamilyMono; return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS); }, @@ -744,11 +717,8 @@ export const useAppStore = create()((set, get) => ({ : 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 @@ -781,11 +751,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // 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) => { @@ -804,11 +771,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // 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 @@ -830,11 +794,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Feature actions @@ -845,7 +806,7 @@ export const useAppStore = create()((set, get) => ({ })), 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; }, @@ -2471,8 +2432,7 @@ export const useAppStore = create()((set, get) => ({ 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; @@ -2484,7 +2444,7 @@ export const useAppStore = create()((set, get) => ({ isDefault: boolean; }>; error?: string; - }; + }>('/api/codex/models'); if (data.success && data.models) { set({ @@ -2542,8 +2502,7 @@ export const useAppStore = create()((set, get) => ({ 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<{ @@ -2553,7 +2512,7 @@ export const useAppStore = create()((set, get) => ({ authMethod?: string; }>; error?: string; - }; + }>('/api/opencode/models'); if (data.success && data.models) { // Filter out Bedrock models From b7c6b8bfc64b53e87f407010dbb53e55d036f1fd Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 27 Jan 2026 00:15:38 +0100 Subject: [PATCH 3/7] feat(ui): Show project name in classic sidebar layout Add project name display at the top of the navigation for the classic (discord) sidebar style, which previously didn't show the project name anywhere. Shows the project icon (custom or Lucide) and name with a separator below. Co-Authored-By: Claude Opus 4.5 --- .../sidebar/components/sidebar-navigation.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index 46872b6a..905448cd 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -1,8 +1,11 @@ import { useCallback, useEffect, useRef } from 'react'; import type { NavigateOptions } from '@tanstack/react-router'; -import { ChevronDown, Wrench, Github } from 'lucide-react'; +import { ChevronDown, Wrench, Github, Folder } from 'lucide-react'; +import * as LucideIcons from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatShortcut, useAppStore } from '@/store/app-store'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import type { NavSection } from '../types'; import type { Project } from '@/lib/electron'; import type { SidebarStyle } from '@automaker/types'; @@ -97,6 +100,17 @@ export function SidebarNavigation({ return !!currentProject; }); + // Get the icon component for the current project + const getProjectIcon = (): LucideIcon => { + if (currentProject?.icon && currentProject.icon in LucideIcons) { + return (LucideIcons as unknown as Record)[currentProject.icon]; + } + return Folder; + }; + + const ProjectIcon = getProjectIcon(); + const hasCustomIcon = !!currentProject?.customIconPath; + return (