Compare commits

...

11 Commits

Author SHA1 Message Date
Kacper
6e2f277f63 Merge v0.14.0rc into refactor/store-ui-slice
Resolve merge conflict in app-store.ts by keeping UI slice implementation
of getEffectiveFontSans/getEffectiveFontMono (already provided by ui-slice.ts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:48:41 +01:00
Shirone
ef3f8de33b Merge pull request #715 from OG-Ken/fix/opencode-dynamic-models-404-endpoint
fix: Correct OpenCode dynamic models API endpoint URL
2026-01-27 12:02:33 +00:00
Ken Lopez
d379bf412a fix: Correct OpenCode dynamic models API endpoint URL
The fetchOpencodeModels function was calling '/api/opencode/models' which
returns 404. Changed to '/api/setup/opencode/models' which correctly
returns the dynamic models.

This fixes an issue where enabled OpenCode dynamic models (e.g., local
Ollama models) were not appearing in the Model Defaults dropdown selectors
despite being visible and enabled in the OpenCode Settings page.
2026-01-27 03:06:28 -05:00
Shirone
cf35ca8650 Merge pull request #714 from AutoMaker-Org/feature/bug-request-changes-on-plan-mode-is-not-proceedin-8xpd
refactor(auto-mode): Enhance revision prompt customization
2026-01-26 23:36:30 +00:00
Shirone
4f1555f196 feat(event-history): Replace alert with toast notifications for event replay results
Update the EventHistoryView component to use toast notifications instead of alert dialogs for displaying event replay results, enhancing user experience and providing clearer feedback on success and failure states.
2026-01-27 00:29:34 +01:00
Shirone
5aace0ce0f fix(event-hook): Update featureName assignment to prioritize loaded feature title over payload 2026-01-27 00:25:36 +01:00
Shirone
e439d8a632 fix(routes): Update feature creation event to use title instead of name
Change the feature creation event to emit 'Untitled Feature' when the title is not provided, improving clarity in event handling.
2026-01-27 00:25:16 +01:00
Shirone
b7c6b8bfc6 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 <noreply@anthropic.com>
2026-01-27 00:15:38 +01:00
Shirone
a60904bd51 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 <noreply@anthropic.com>
2026-01-27 00:09:55 +01:00
Kacper
d7c3337330 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.
2026-01-26 19:53:07 +01:00
Shirone
79236ba16e refactor(store): Extract UI slice from app-store.ts
- Extract UI-related state and actions into store/slices/ui-slice.ts
- Add UISliceState and UISliceActions interfaces to store/types/ui-types.ts
- First implementation of Zustand slice pattern in the codebase
- Fix pre-existing bug: fontSans/fontMono -> fontFamilySans/fontFamilyMono
- Maintain backward compatibility through re-exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 00:03:58 +01:00
19 changed files with 813 additions and 392 deletions

View File

@@ -43,7 +43,7 @@ export function createCreateHandler(featureLoader: FeatureLoader, events?: Event
if (events) { if (events) {
events.emit('feature:created', { events.emit('feature:created', {
featureId: created.id, featureId: created.id,
featureName: created.name, featureName: created.title || 'Untitled Feature',
projectPath, projectPath,
}); });
} }

View File

@@ -31,7 +31,9 @@ export function createSaveBoardBackgroundHandler() {
await secureFs.mkdir(boardDir, { recursive: true }); await secureFs.mkdir(boardDir, { recursive: true });
// Decode base64 data (remove data URL prefix if present) // 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'); const buffer = Buffer.from(base64Data, 'base64');
// Use a fixed filename for the board background (overwrite previous) // Use a fixed filename for the board background (overwrite previous)

View File

@@ -31,7 +31,9 @@ export function createSaveImageHandler() {
await secureFs.mkdir(imagesDir, { recursive: true }); await secureFs.mkdir(imagesDir, { recursive: true });
// Decode base64 data (remove data URL prefix if present) // 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'); const buffer = Buffer.from(base64Data, 'base64');
// Generate unique filename with timestamp // Generate unique filename with timestamp

View File

@@ -4597,21 +4597,54 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
planVersion, planVersion,
}); });
// Build revision prompt // Build revision prompt using customizable template
let revisionPrompt = `The user has requested revisions to the plan/specification. const revisionPrompts = await getPromptCustomization(
this.settingsService,
'[AutoMode]'
);
## Previous Plan (v${planVersion - 1}) // Get task format example based on planning mode
${hasEdits ? approvalResult.editedPlan : currentPlanContent} const taskFormatExample =
planningMode === 'full'
? `\`\`\`tasks
## Phase 1: Foundation
- [ ] T001: [Description] | File: [path/to/file]
- [ ] T002: [Description] | File: [path/to/file]
## User Feedback ## Phase 2: Core Implementation
${approvalResult.feedback || 'Please revise the plan based on the edits above.'} - [ ] 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 let revisionPrompt = revisionPrompts.taskExecution.planRevisionTemplate;
Please regenerate the specification incorporating the user's feedback. revisionPrompt = revisionPrompt.replace(
Keep the same format with the \`\`\`tasks block for task definitions. /\{\{planVersion\}\}/g,
After generating the revised spec, output: String(planVersion - 1)
"[SPEC_GENERATED] Please review the revised specification above." );
`; 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 // Update status to regenerating
await this.updateFeaturePlanSpec(projectPath, featureId, { await this.updateFeaturePlanSpec(projectPath, featureId, {
@@ -4663,6 +4696,26 @@ After generating the revised spec, output:
const revisedTasks = parseTasksFromSpec(currentPlanContent); const revisedTasks = parseTasksFromSpec(currentPlanContent);
logger.info(`Revised plan has ${revisedTasks.length} tasks`); 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 // Update planSpec with revised content
await this.updateFeaturePlanSpec(projectPath, featureId, { await this.updateFeaturePlanSpec(projectPath, featureId, {
status: 'generated', status: 'generated',

View File

@@ -169,9 +169,10 @@ export class EventHookService {
} }
// Build context for variable substitution // Build context for variable substitution
// Use loaded featureName (from feature.title) or fall back to payload.featureName
const context: HookContext = { const context: HookContext = {
featureId: payload.featureId, featureId: payload.featureId,
featureName: payload.featureName, featureName: featureName || payload.featureName,
projectPath: payload.projectPath, projectPath: payload.projectPath,
projectName: payload.projectPath ? this.extractProjectName(payload.projectPath) : undefined, projectName: payload.projectPath ? this.extractProjectName(payload.projectPath) : undefined,
error: payload.error || payload.message, error: payload.error || payload.message,

View File

@@ -15,6 +15,7 @@ import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import { getHttpApiClient } from '@/lib/http-api-client'; import { getHttpApiClient } from '@/lib/http-api-client';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
import { IconPicker } from './icon-picker'; import { IconPicker } from './icon-picker';
import { toast } from 'sonner';
interface EditProjectDialogProps { interface EditProjectDialogProps {
project: Project; project: Project;
@@ -52,11 +53,18 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
// Validate file type // Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) { if (!validTypes.includes(file.type)) {
toast.error(
`Invalid file type: ${file.type || 'unknown'}. Please use JPG, PNG, GIF or WebP.`
);
return; return;
} }
// Validate file size (max 2MB for icons) // Validate file size (max 5MB for icons - allows animated GIFs)
if (file.size > 2 * 1024 * 1024) { 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; return;
} }
@@ -72,15 +80,24 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
file.type, file.type,
project.path project.path
); );
if (result.success && result.path) { if (result.success && result.path) {
setCustomIconPath(result.path); setCustomIconPath(result.path);
// Clear the Lucide icon when custom icon is set // Clear the Lucide icon when custom icon is set
setIcon(null); setIcon(null);
toast.success('Icon uploaded successfully');
} else {
toast.error('Failed to upload icon');
} }
setIsUploadingIcon(false); setIsUploadingIcon(false);
}; };
reader.onerror = () => {
toast.error('Failed to read file');
setIsUploadingIcon(false);
};
reader.readAsDataURL(file); reader.readAsDataURL(file);
} catch { } catch {
toast.error('Failed to upload icon');
setIsUploadingIcon(false); setIsUploadingIcon(false);
} }
}; };
@@ -162,7 +179,7 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
{isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'} {isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
</Button> </Button>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
PNG, JPG, GIF or WebP. Max 2MB. PNG, JPG, GIF or WebP. Max 5MB.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -59,7 +59,7 @@ interface ThemeButtonProps {
/** Handler for pointer leave events (used to clear preview) */ /** Handler for pointer leave events (used to clear preview) */
onPointerLeave: (e: React.PointerEvent) => void; onPointerLeave: (e: React.PointerEvent) => void;
/** Handler for click events (used to select theme) */ /** 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; const Icon = option.icon;
return ( return (
<button <button
type="button"
onPointerEnter={onPointerEnter} onPointerEnter={onPointerEnter}
onPointerLeave={onPointerLeave} onPointerLeave={onPointerLeave}
onClick={onClick} onClick={onClick}
@@ -145,7 +146,10 @@ const ThemeColumn = memo(function ThemeColumn({
isSelected={selectedTheme === option.value} isSelected={selectedTheme === option.value}
onPointerEnter={() => onPreviewEnter(option.value)} onPointerEnter={() => onPreviewEnter(option.value)}
onPointerLeave={onPreviewLeave} onPointerLeave={onPreviewLeave}
onClick={() => onSelect(option.value)} onClick={(e) => {
e.stopPropagation();
onSelect(option.value);
}}
/> />
))} ))}
</div> </div>
@@ -193,7 +197,6 @@ export function ProjectContextMenu({
const { const {
moveProjectToTrash, moveProjectToTrash,
theme: globalTheme, theme: globalTheme,
setTheme,
setProjectTheme, setProjectTheme,
setPreviewTheme, setPreviewTheme,
} = useAppStore(); } = useAppStore();
@@ -316,13 +319,24 @@ export function ProjectContextMenu({
const handleThemeSelect = useCallback( const handleThemeSelect = useCallback(
(value: ThemeMode | typeof USE_GLOBAL_THEME) => { (value: ThemeMode | typeof USE_GLOBAL_THEME) => {
// Clear any pending close timeout to prevent race conditions
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = null;
}
// Close menu first
setShowThemeSubmenu(false);
onClose();
// Then apply theme changes
setPreviewTheme(null); setPreviewTheme(null);
const isUsingGlobal = value === USE_GLOBAL_THEME; const isUsingGlobal = value === USE_GLOBAL_THEME;
setTheme(isUsingGlobal ? globalTheme : value); // Only set project theme - don't change global theme
// The UI uses getEffectiveTheme() which handles: previewTheme ?? projectTheme ?? globalTheme
setProjectTheme(project.id, isUsingGlobal ? null : value); setProjectTheme(project.id, isUsingGlobal ? null : value);
setShowThemeSubmenu(false);
}, },
[globalTheme, project.id, setPreviewTheme, setProjectTheme, setTheme] [onClose, project.id, setPreviewTheme, setProjectTheme]
); );
const handleConfirmRemove = useCallback(() => { const handleConfirmRemove = useCallback(() => {
@@ -426,9 +440,13 @@ export function ProjectContextMenu({
<div className="p-2"> <div className="p-2">
{/* Use Global Option */} {/* Use Global Option */}
<button <button
type="button"
onPointerEnter={() => handlePreviewEnter(globalTheme)} onPointerEnter={() => handlePreviewEnter(globalTheme)}
onPointerLeave={handlePreviewLeave} onPointerLeave={handlePreviewLeave}
onClick={() => handleThemeSelect(USE_GLOBAL_THEME)} onClick={(e) => {
e.stopPropagation();
handleThemeSelect(USE_GLOBAL_THEME);
}}
className={cn( className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md', 'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left', 'text-sm font-medium text-left',

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Folder, LucideIcon } from 'lucide-react'; import { Folder, LucideIcon } from 'lucide-react';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import { cn, sanitizeForTestId } from '@/lib/utils'; import { cn, sanitizeForTestId } from '@/lib/utils';
@@ -19,6 +20,8 @@ export function ProjectSwitcherItem({
onClick, onClick,
onContextMenu, onContextMenu,
}: ProjectSwitcherItemProps) { }: ProjectSwitcherItemProps) {
const [imageError, setImageError] = useState(false);
// Convert index to hotkey label: 0 -> "1", 1 -> "2", ..., 8 -> "9", 9 -> "0" // Convert index to hotkey label: 0 -> "1", 1 -> "2", ..., 8 -> "9", 9 -> "0"
const hotkeyLabel = const hotkeyLabel =
hotkeyIndex !== undefined && hotkeyIndex >= 0 && hotkeyIndex <= 9 hotkeyIndex !== undefined && hotkeyIndex >= 0 && hotkeyIndex <= 9
@@ -35,7 +38,7 @@ export function ProjectSwitcherItem({
}; };
const IconComponent = getIconComponent(); const IconComponent = getIconComponent();
const hasCustomIcon = !!project.customIconPath; const hasCustomIcon = !!project.customIconPath && !imageError;
// Combine project.id with sanitized name for uniqueness and readability // Combine project.id with sanitized name for uniqueness and readability
// Format: project-switcher-{id}-{sanitizedName} // Format: project-switcher-{id}-{sanitizedName}
@@ -74,6 +77,7 @@ export function ProjectSwitcherItem({
'w-8 h-8 rounded-lg object-cover transition-all duration-200', 'w-8 h-8 rounded-lg object-cover transition-all duration-200',
isActive ? 'ring-1 ring-brand-500/50' : 'group-hover:scale-110' isActive ? 'ring-1 ring-brand-500/50' : 'group-hover:scale-110'
)} )}
onError={() => setImageError(true)}
/> />
) : ( ) : (
<IconComponent <IconComponent

View File

@@ -100,14 +100,8 @@ export function ProjectSelectorWithOptions({
const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects }); const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects });
const { const { globalTheme, setProjectTheme, setPreviewTheme, handlePreviewEnter, handlePreviewLeave } =
globalTheme, useProjectTheme();
setTheme,
setProjectTheme,
setPreviewTheme,
handlePreviewEnter,
handlePreviewLeave,
} = useProjectTheme();
if (!sidebarOpen || projects.length === 0) { if (!sidebarOpen || projects.length === 0) {
return null; return null;
@@ -281,11 +275,8 @@ export function ProjectSelectorWithOptions({
onValueChange={(value) => { onValueChange={(value) => {
if (currentProject) { if (currentProject) {
setPreviewTheme(null); setPreviewTheme(null);
if (value !== '') { // Only set project theme - don't change global theme
setTheme(value as ThemeMode); // The UI uses getEffectiveTheme() which handles: previewTheme ?? projectTheme ?? globalTheme
} else {
setTheme(globalTheme);
}
setProjectTheme( setProjectTheme(
currentProject.id, currentProject.id,
value === '' ? null : (value as ThemeMode) value === '' ? null : (value as ThemeMode)

View File

@@ -1,8 +1,11 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import type { NavigateOptions } from '@tanstack/react-router'; 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 { cn } from '@/lib/utils';
import { formatShortcut, useAppStore } from '@/store/app-store'; import { formatShortcut, useAppStore } from '@/store/app-store';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import type { NavSection } from '../types'; import type { NavSection } from '../types';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
import type { SidebarStyle } from '@automaker/types'; import type { SidebarStyle } from '@automaker/types';
@@ -97,6 +100,17 @@ export function SidebarNavigation({
return !!currentProject; 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<string, LucideIcon>)[currentProject.icon];
}
return Folder;
};
const ProjectIcon = getProjectIcon();
const hasCustomIcon = !!currentProject?.customIconPath;
return ( return (
<nav <nav
ref={navRef} ref={navRef}
@@ -106,6 +120,27 @@ export function SidebarNavigation({
sidebarStyle === 'discord' ? 'pt-3' : 'mt-1' sidebarStyle === 'discord' ? 'pt-3' : 'mt-1'
)} )}
> >
{/* Project name display for classic/discord mode */}
{sidebarStyle === 'discord' && currentProject && sidebarOpen && (
<div className="mb-3">
<div className="flex items-center gap-2.5 px-3 py-2">
{hasCustomIcon ? (
<img
src={getAuthenticatedImageUrl(currentProject.customIconPath!, currentProject.path)}
alt={currentProject.name}
className="w-5 h-5 rounded object-cover"
/>
) : (
<ProjectIcon className="w-5 h-5 text-brand-500 shrink-0" />
)}
<span className="text-sm font-medium text-foreground truncate">
{currentProject.name}
</span>
</div>
<div className="h-px bg-border/40 mx-1 mt-1" />
</div>
)}
{/* Navigation sections */} {/* Navigation sections */}
{visibleSections.map((section, sectionIdx) => { {visibleSections.map((section, sectionIdx) => {
const isCollapsed = section.label ? collapsedNavSections[section.label] : false; const isCollapsed = section.label ? collapsedNavSections[section.label] : false;

View File

@@ -9,19 +9,15 @@ export const ThemeMenuItem = memo(function ThemeMenuItem({
}: ThemeMenuItemProps) { }: ThemeMenuItemProps) {
const Icon = option.icon; const Icon = option.icon;
return ( return (
<div <DropdownMenuRadioItem
key={option.value} value={option.value}
data-testid={`project-theme-${option.value}`}
className="text-xs py-1.5"
onPointerEnter={() => onPreviewEnter(option.value)} onPointerEnter={() => onPreviewEnter(option.value)}
onPointerLeave={onPreviewLeave} onPointerLeave={onPreviewLeave}
> >
<DropdownMenuRadioItem <Icon className="w-3.5 h-3.5 mr-1.5" style={{ color: option.color }} />
value={option.value} <span>{option.label}</span>
data-testid={`project-theme-${option.value}`} </DropdownMenuRadioItem>
className="text-xs py-1.5"
>
<Icon className="w-3.5 h-3.5 mr-1.5" style={{ color: option.color }} />
<span>{option.label}</span>
</DropdownMenuRadioItem>
</div>
); );
}); });

View File

@@ -68,10 +68,10 @@ export function ProjectIdentitySection({ project }: ProjectIdentitySectionProps)
return; return;
} }
// Validate file size (max 2MB for icons) // Validate file size (max 5MB for icons - allows animated GIFs)
if (file.size > 2 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
toast.error('File too large', { toast.error('File too large', {
description: 'Please upload an image smaller than 2MB.', description: 'Please upload an image smaller than 5MB.',
}); });
return; return;
} }
@@ -208,7 +208,7 @@ export function ProjectIdentitySection({ project }: ProjectIdentitySectionProps)
{isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'} {isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
</Button> </Button>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
PNG, JPG, GIF or WebP. Max 2MB. PNG, JPG, GIF or WebP. Max 5MB.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -19,6 +19,7 @@ import type { StoredEventSummary, StoredEvent, EventHookTrigger } from '@automak
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types'; import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
import { getHttpApiClient } from '@/lib/http-api-client'; import { getHttpApiClient } from '@/lib/http-api-client';
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { toast } from 'sonner';
export function EventHistoryView() { export function EventHistoryView() {
const currentProject = useAppStore((state) => state.currentProject); const currentProject = useAppStore((state) => state.currentProject);
@@ -85,16 +86,18 @@ export function EventHistoryView() {
const failCount = hookResults.filter((r) => !r.success).length; const failCount = hookResults.filter((r) => !r.success).length;
if (hooksTriggered === 0) { 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) { } else if (failCount === 0) {
alert(`Successfully ran ${successCount} hook(s).`); toast.success(`Successfully ran ${successCount} hook(s).`);
} else { } else {
alert(`Ran ${hooksTriggered} hook(s): ${successCount} succeeded, ${failCount} failed.`); toast.warning(
`Ran ${hooksTriggered} hook(s): ${successCount} succeeded, ${failCount} failed.`
);
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to replay event:', 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 { } finally {
setReplayingEvent(null); setReplayingEvent(null);
} }

View File

@@ -946,7 +946,7 @@ export class HttpApiClient implements ElectronAPI {
return response.json(); return response.json();
} }
private async get<T>(endpoint: string): Promise<T> { async get<T>(endpoint: string): Promise<T> {
// Ensure API key is initialized before making request // Ensure API key is initialized before making request
await waitForApiKeyInit(); await waitForApiKeyInit();
const response = await fetch(`${this.serverUrl}${endpoint}`, { const response = await fetch(`${this.serverUrl}${endpoint}`, {
@@ -976,7 +976,7 @@ export class HttpApiClient implements ElectronAPI {
return response.json(); return response.json();
} }
private async put<T>(endpoint: string, body?: unknown): Promise<T> { async put<T>(endpoint: string, body?: unknown): Promise<T> {
// Ensure API key is initialized before making request // Ensure API key is initialized before making request
await waitForApiKeyInit(); await waitForApiKeyInit();
const response = await fetch(`${this.serverUrl}${endpoint}`, { const response = await fetch(`${this.serverUrl}${endpoint}`, {

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
// Note: persist middleware removed - settings now sync via API (use-settings-sync.ts) // Note: persist middleware removed - settings now sync via API (use-settings-sync.ts)
import type { Project, TrashedProject } from '@/lib/electron'; 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 { getHttpApiClient } from '@/lib/http-api-client';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
// Note: setItem/getItem moved to ./utils/theme-utils.ts // Note: setItem/getItem moved to ./utils/theme-utils.ts
@@ -60,6 +60,8 @@ import {
type ShortcutKey, type ShortcutKey,
type KeyboardShortcuts, type KeyboardShortcuts,
type BackgroundSettings, type BackgroundSettings,
type UISliceState,
type UISliceActions,
// Settings types // Settings types
type ApiKeys, type ApiKeys,
// Chat types // Chat types
@@ -109,16 +111,13 @@ import {
} from './utils'; } from './utils';
// Import default values from modular defaults files // Import default values from modular defaults files
import { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES } from './defaults'; import { defaultTerminalState, MAX_INIT_OUTPUT_LINES } from './defaults';
// Import UI slice
import { createUISlice, initialUIState } from './slices';
// Import internal theme utils (not re-exported publicly) // Import internal theme utils (not re-exported publicly)
import { import { persistEffectiveThemeForProject } from './utils/theme-utils';
getEffectiveFont,
saveThemeToStorage,
saveFontSansToStorage,
saveFontMonoToStorage,
persistEffectiveThemeForProject,
} from './utils/theme-utils';
const logger = createLogger('AppStore'); const logger = createLogger('AppStore');
const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock'; const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock';
@@ -146,6 +145,8 @@ export type {
ShortcutKey, ShortcutKey,
KeyboardShortcuts, KeyboardShortcuts,
BackgroundSettings, BackgroundSettings,
UISliceState,
UISliceActions,
ApiKeys, ApiKeys,
ImageAttachment, ImageAttachment,
TextFileAttachment, TextFileAttachment,
@@ -213,56 +214,72 @@ export { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES
// - defaultTerminalState (./defaults/terminal-defaults.ts) // - defaultTerminalState (./defaults/terminal-defaults.ts)
const initialState: AppState = { const initialState: AppState = {
// Spread UI slice state first
...initialUIState,
// Project state
projects: [], projects: [],
currentProject: null, currentProject: null,
trashedProjects: [], trashedProjects: [],
projectHistory: [], projectHistory: [],
projectHistoryIndex: -1, projectHistoryIndex: -1,
currentView: 'welcome',
sidebarOpen: true, // Agent Session state
sidebarStyle: 'unified',
collapsedNavSections: {},
mobileSidebarHidden: false,
lastSelectedSessionByProject: {}, lastSelectedSessionByProject: {},
theme: getStoredTheme() || 'dark',
fontFamilySans: getStoredFontSans(), // Features/Kanban
fontFamilyMono: getStoredFontMono(),
features: [], features: [],
// App spec
appSpec: '', appSpec: '',
// IPC status
ipcConnected: false, ipcConnected: false,
// API Keys
apiKeys: { apiKeys: {
anthropic: '', anthropic: '',
google: '', google: '',
openai: '', openai: '',
}, },
// Chat Sessions
chatSessions: [], chatSessions: [],
currentChatSession: null, currentChatSession: null,
chatHistoryOpen: false,
// Auto Mode
autoModeByWorktree: {}, autoModeByWorktree: {},
autoModeActivityLog: [], autoModeActivityLog: [],
maxConcurrency: DEFAULT_MAX_CONCURRENCY, maxConcurrency: DEFAULT_MAX_CONCURRENCY,
boardViewMode: 'kanban',
// Feature Default Settings
defaultSkipTests: true, defaultSkipTests: true,
enableDependencyBlocking: true, enableDependencyBlocking: true,
skipVerificationInAutoMode: false, skipVerificationInAutoMode: false,
enableAiCommitMessages: true, enableAiCommitMessages: true,
planUseSelectedWorktreeBranch: true, planUseSelectedWorktreeBranch: true,
addFeatureUseSelectedWorktreeBranch: false, addFeatureUseSelectedWorktreeBranch: false,
// Worktree Settings
useWorktrees: true, useWorktrees: true,
currentWorktreeByProject: {}, currentWorktreeByProject: {},
worktreesByProject: {}, worktreesByProject: {},
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
muteDoneSound: false, // Server Settings
disableSplashScreen: false,
serverLogLevel: 'info', serverLogLevel: 'info',
enableRequestLogging: true, enableRequestLogging: true,
showQueryDevtools: true,
// Model Settings
enhancementModel: 'claude-sonnet', enhancementModel: 'claude-sonnet',
validationModel: 'claude-opus', validationModel: 'claude-opus',
phaseModels: DEFAULT_PHASE_MODELS, phaseModels: DEFAULT_PHASE_MODELS,
favoriteModels: [], favoriteModels: [],
// Cursor CLI Settings
enabledCursorModels: getAllCursorModelIds(), enabledCursorModels: getAllCursorModelIds(),
cursorDefaultModel: 'cursor-auto', cursorDefaultModel: 'cursor-auto',
// Codex CLI Settings
enabledCodexModels: getAllCodexModelIds(), enabledCodexModels: getAllCodexModelIds(),
codexDefaultModel: 'codex-gpt-5.2-codex', codexDefaultModel: 'codex-gpt-5.2-codex',
codexAutoLoadAgents: false, codexAutoLoadAgents: false,
@@ -270,6 +287,8 @@ const initialState: AppState = {
codexApprovalPolicy: 'on-request', codexApprovalPolicy: 'on-request',
codexEnableWebSearch: false, codexEnableWebSearch: false,
codexEnableImages: false, codexEnableImages: false,
// OpenCode CLI Settings
enabledOpencodeModels: getAllOpencodeModelIds(), enabledOpencodeModels: getAllOpencodeModelIds(),
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, opencodeDefaultModel: DEFAULT_OPENCODE_MODEL,
dynamicOpencodeModels: [], dynamicOpencodeModels: [],
@@ -279,61 +298,101 @@ const initialState: AppState = {
opencodeModelsError: null, opencodeModelsError: null,
opencodeModelsLastFetched: null, opencodeModelsLastFetched: null,
opencodeModelsLastFailedAt: null, opencodeModelsLastFailedAt: null,
// Gemini CLI Settings
enabledGeminiModels: getAllGeminiModelIds(), enabledGeminiModels: getAllGeminiModelIds(),
geminiDefaultModel: DEFAULT_GEMINI_MODEL, geminiDefaultModel: DEFAULT_GEMINI_MODEL,
// Copilot SDK Settings
enabledCopilotModels: getAllCopilotModelIds(), enabledCopilotModels: getAllCopilotModelIds(),
copilotDefaultModel: DEFAULT_COPILOT_MODEL, copilotDefaultModel: DEFAULT_COPILOT_MODEL,
// Provider Settings
disabledProviders: [], disabledProviders: [],
// Claude Agent SDK Settings
autoLoadClaudeMd: false, autoLoadClaudeMd: false,
skipSandboxWarning: false, skipSandboxWarning: false,
// MCP Servers
mcpServers: [], mcpServers: [],
// Editor Configuration
defaultEditorCommand: null, defaultEditorCommand: null,
// Terminal Configuration
defaultTerminalId: null, defaultTerminalId: null,
// Skills Configuration
enableSkills: true, enableSkills: true,
skillsSources: ['user', 'project'] as Array<'user' | 'project'>, skillsSources: ['user', 'project'] as Array<'user' | 'project'>,
// Subagents Configuration
enableSubagents: true, enableSubagents: true,
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, subagentsSources: ['user', 'project'] as Array<'user' | 'project'>,
// Prompt Customization
promptCustomization: {}, promptCustomization: {},
// Event Hooks
eventHooks: [], eventHooks: [],
// Claude-Compatible Providers
claudeCompatibleProviders: [], claudeCompatibleProviders: [],
claudeApiProfiles: [], claudeApiProfiles: [],
activeClaudeApiProfileId: null, activeClaudeApiProfileId: null,
// Project Analysis
projectAnalysis: null, projectAnalysis: null,
isAnalyzing: false, isAnalyzing: false,
boardBackgroundByProject: {},
previewTheme: null, // Terminal state
terminalState: defaultTerminalState, terminalState: defaultTerminalState,
terminalLayoutByProject: {}, terminalLayoutByProject: {},
// Spec Creation
specCreatingForProject: null, specCreatingForProject: null,
// Planning
defaultPlanningMode: 'skip' as PlanningMode, defaultPlanningMode: 'skip' as PlanningMode,
defaultRequirePlanApproval: false, defaultRequirePlanApproval: false,
defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel, defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel,
pendingPlanApproval: null, pendingPlanApproval: null,
// Claude Usage Tracking
claudeRefreshInterval: 60, claudeRefreshInterval: 60,
claudeUsage: null, claudeUsage: null,
claudeUsageLastUpdated: null, claudeUsageLastUpdated: null,
// Codex Usage Tracking
codexUsage: null, codexUsage: null,
codexUsageLastUpdated: null, codexUsageLastUpdated: null,
// Codex Models
codexModels: [], codexModels: [],
codexModelsLoading: false, codexModelsLoading: false,
codexModelsError: null, codexModelsError: null,
codexModelsLastFetched: null, codexModelsLastFetched: null,
codexModelsLastFailedAt: null, codexModelsLastFailedAt: null,
// Pipeline Configuration
pipelineConfigByProject: {}, pipelineConfigByProject: {},
worktreePanelVisibleByProject: {},
showInitScriptIndicatorByProject: {}, // Project-specific Worktree Settings
defaultDeleteBranchByProject: {}, defaultDeleteBranchByProject: {},
autoDismissInitScriptIndicatorByProject: {},
useWorktreesByProject: {}, useWorktreesByProject: {},
worktreePanelCollapsed: false,
lastProjectDir: '', // Init Script State
recentFolders: [],
initScriptState: {}, initScriptState: {},
}; };
export const useAppStore = create<AppState & AppActions>()((set, get) => ({ export const useAppStore = create<AppState & AppActions>()((set, get, store) => ({
// Spread initial non-UI state
...initialState, ...initialState,
// Spread UI slice (includes UI state and actions)
...createUISlice(set, get, store),
// Project actions // Project actions
setProjects: (projects) => set({ projects }), setProjects: (projects) => set({ projects }),
@@ -360,7 +419,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
const trashedProject: TrashedProject = { const trashedProject: TrashedProject = {
...project, ...project,
trashedAt: Date.now(), trashedAt: new Date().toISOString(),
}; };
set((state) => ({ set((state) => ({
@@ -369,12 +428,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
currentProject: state.currentProject?.id === projectId ? null : state.currentProject, currentProject: state.currentProject?.id === projectId ? null : state.currentProject,
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) { saveTrashedProjects(get().trashedProjects);
electronAPI.projects.setProjects(get().projects);
electronAPI.projects.setTrashedProjects(get().trashedProjects);
}
}, },
restoreTrashedProject: (projectId: string) => { restoreTrashedProject: (projectId: string) => {
@@ -390,12 +446,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId),
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) { saveTrashedProjects(get().trashedProjects);
electronAPI.projects.setProjects(get().projects);
electronAPI.projects.setTrashedProjects(get().trashedProjects);
}
}, },
deleteTrashedProject: (projectId: string) => { deleteTrashedProject: (projectId: string) => {
@@ -403,21 +456,15 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId),
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveTrashedProjects(get().trashedProjects);
if (electronAPI) {
electronAPI.projects.setTrashedProjects(get().trashedProjects);
}
}, },
emptyTrash: () => { emptyTrash: () => {
set({ trashedProjects: [] }); set({ trashedProjects: [] });
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveTrashedProjects([]);
if (electronAPI) {
electronAPI.projects.setTrashedProjects([]);
}
}, },
setCurrentProject: (project) => { setCurrentProject: (project) => {
@@ -474,14 +521,10 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
get().addProject(newProject); get().addProject(newProject);
get().setCurrentProject(newProject); get().setCurrentProject(newProject);
// Persist to Electron store if available // Persist to storage (small delay to ensure state is updated)
const electronAPI = getElectronAPI(); setTimeout(() => {
if (electronAPI) { saveProjects(get().projects);
// Small delay to ensure state is updated before persisting }, 0);
setTimeout(() => {
electronAPI.projects.setProjects(get().projects);
}, 0);
}
return newProject; return newProject;
}, },
@@ -564,11 +607,8 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
), ),
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
setProjectIcon: (projectId: string, icon: string | null) => { setProjectIcon: (projectId: string, icon: string | null) => {
@@ -576,27 +616,31 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
projects: state.projects.map((p) => projects: state.projects.map((p) =>
p.id === projectId ? { ...p, icon: icon ?? undefined } : 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 // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
setProjectCustomIcon: (projectId: string, customIconPath: string | null) => { setProjectCustomIcon: (projectId: string, customIconPath: string | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => 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 // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
setProjectName: (projectId: string, name: string) => { setProjectName: (projectId: string, name: string) => {
@@ -609,35 +653,13 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
: state.currentProject, : state.currentProject,
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
// View actions // View actions - provided by UI slice
setCurrentView: (view) => set({ currentView: view }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setSidebarStyle: (style) => set({ sidebarStyle: style }),
setCollapsedNavSections: (sections) => set({ collapsedNavSections: sections }),
toggleNavSection: (sectionLabel) =>
set((state) => ({
collapsedNavSections: {
...state.collapsedNavSections,
[sectionLabel]: !state.collapsedNavSections[sectionLabel],
},
})),
toggleMobileSidebarHidden: () =>
set((state) => ({ mobileSidebarHidden: !state.mobileSidebarHidden })),
setMobileSidebarHidden: (hidden) => set({ mobileSidebarHidden: hidden }),
// Theme actions // Theme actions (setTheme, getEffectiveTheme, setPreviewTheme provided by UI slice)
setTheme: (theme) => {
set({ theme });
saveThemeToStorage(theme);
},
setProjectTheme: (projectId: string, theme: ThemeMode | null) => { setProjectTheme: (projectId: string, theme: ThemeMode | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => projects: state.projects.map((p) =>
@@ -659,76 +681,40 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
); );
} }
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
getEffectiveTheme: () => {
const state = get();
// If there's a preview theme, use it (for hover preview)
if (state.previewTheme) return state.previewTheme;
// Otherwise, use project theme if set, or fall back to global theme
const projectTheme = state.currentProject?.theme as ThemeMode | undefined;
return projectTheme ?? state.theme;
},
setPreviewTheme: (theme) => set({ previewTheme: theme }),
// Font actions // Font actions (setFontSans, setFontMono, getEffectiveFontSans, getEffectiveFontMono provided by UI slice)
setFontSans: (fontFamily) => {
set({ fontFamilySans: fontFamily });
saveFontSansToStorage(fontFamily);
},
setFontMono: (fontFamily) => {
set({ fontFamilyMono: fontFamily });
saveFontMonoToStorage(fontFamily);
},
setProjectFontSans: (projectId: string, fontFamily: string | null) => { setProjectFontSans: (projectId: string, fontFamily: string | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => projects: state.projects.map((p) =>
p.id === projectId ? { ...p, fontSans: fontFamily ?? undefined } : p p.id === projectId ? { ...p, fontFamilySans: fontFamily ?? undefined } : p
), ),
// Also update currentProject if it's the one being changed // Also update currentProject if it's the one being changed
currentProject: currentProject:
state.currentProject?.id === projectId state.currentProject?.id === projectId
? { ...state.currentProject, fontSans: fontFamily ?? undefined } ? { ...state.currentProject, fontFamilySans: fontFamily ?? undefined }
: state.currentProject, : state.currentProject,
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
setProjectFontMono: (projectId: string, fontFamily: string | null) => { setProjectFontMono: (projectId: string, fontFamily: string | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => projects: state.projects.map((p) =>
p.id === projectId ? { ...p, fontMono: fontFamily ?? undefined } : p p.id === projectId ? { ...p, fontFamilyMono: fontFamily ?? undefined } : p
), ),
// Also update currentProject if it's the one being changed // Also update currentProject if it's the one being changed
currentProject: currentProject:
state.currentProject?.id === projectId state.currentProject?.id === projectId
? { ...state.currentProject, fontMono: fontFamily ?? undefined } ? { ...state.currentProject, fontFamilyMono: fontFamily ?? undefined }
: state.currentProject, : state.currentProject,
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
},
getEffectiveFontSans: () => {
const state = get();
const projectFont = state.currentProject?.fontSans;
return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS);
},
getEffectiveFontMono: () => {
const state = get();
const projectFont = state.currentProject?.fontMono;
return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS);
}, },
// Claude API Profile actions (per-project override) // Claude API Profile actions (per-project override)
@@ -744,11 +730,8 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
: state.currentProject, : state.currentProject,
})); }));
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
// Project Phase Model Overrides // Project Phase Model Overrides
@@ -781,11 +764,8 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}; };
}); });
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
clearAllProjectPhaseModelOverrides: (projectId: string) => { clearAllProjectPhaseModelOverrides: (projectId: string) => {
@@ -804,11 +784,8 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}; };
}); });
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
// Project Default Feature Model Override // Project Default Feature Model Override
@@ -830,11 +807,8 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}; };
}); });
// Persist to Electron store if available // Persist to storage
const electronAPI = getElectronAPI(); saveProjects(get().projects);
if (electronAPI) {
electronAPI.projects.setProjects(get().projects);
}
}, },
// Feature actions // Feature actions
@@ -845,7 +819,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})), })),
addFeature: (feature) => { addFeature: (feature) => {
const id = feature.id ?? `feature-${Date.now()}-${Math.random().toString(36).slice(2)}`; 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] })); set((state) => ({ features: [...state.features, newFeature] }));
return newFeature; return newFeature;
}, },
@@ -925,8 +899,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
currentChatSession: currentChatSession:
state.currentChatSession?.id === sessionId ? null : state.currentChatSession, state.currentChatSession?.id === sessionId ? null : state.currentChatSession,
})), })),
setChatHistoryOpen: (open) => set({ chatHistoryOpen: open }), // setChatHistoryOpen and toggleChatHistory - provided by UI slice
toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })),
// Auto Mode actions (per-worktree) // Auto Mode actions (per-worktree)
getWorktreeKey: (projectId: string, branchName: string | null) => getWorktreeKey: (projectId: string, branchName: string | null) =>
@@ -1057,8 +1030,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})); }));
}, },
// Kanban Card Settings actions // Kanban Card Settings actions - setBoardViewMode provided by UI slice
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
// Feature Default Settings actions // Feature Default Settings actions
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
@@ -1133,29 +1105,17 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
return mainWorktree?.branch ?? null; return mainWorktree?.branch ?? null;
}, },
// Keyboard Shortcuts actions // Keyboard Shortcuts actions - provided by UI slice
setKeyboardShortcut: (key, value) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, [key]: value },
})),
setKeyboardShortcuts: (shortcuts) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, ...shortcuts },
})),
resetKeyboardShortcuts: () => set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }),
// Audio Settings actions // Audio Settings actions - setMuteDoneSound provided by UI slice
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
// Splash Screen actions // Splash Screen actions - setDisableSplashScreen provided by UI slice
setDisableSplashScreen: (disabled) => set({ disableSplashScreen: disabled }),
// Server Log Level actions // Server Log Level actions
setServerLogLevel: (level) => set({ serverLogLevel: level }), setServerLogLevel: (level) => set({ serverLogLevel: level }),
setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }), setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }),
// Developer Tools actions // Developer Tools actions - setShowQueryDevtools provided by UI slice
setShowQueryDevtools: (show) => set({ showQueryDevtools: show }),
// Enhancement Model actions // Enhancement Model actions
setEnhancementModel: (model) => set({ enhancementModel: model }), setEnhancementModel: (model) => set({ enhancementModel: model }),
@@ -1525,96 +1485,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})), })),
getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null, getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null,
// Board Background actions // Board Background actions - provided by UI slice
setBoardBackground: (projectPath, imagePath) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
imagePath,
imageVersion: Date.now(), // Bust cache on image change
},
},
})),
setCardOpacity: (projectPath, opacity) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardOpacity: opacity,
},
},
})),
setColumnOpacity: (projectPath, opacity) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
columnOpacity: opacity,
},
},
})),
setColumnBorderEnabled: (projectPath, enabled) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
columnBorderEnabled: enabled,
},
},
})),
getBoardBackground: (projectPath) =>
get().boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings,
setCardGlassmorphism: (projectPath, enabled) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardGlassmorphism: enabled,
},
},
})),
setCardBorderEnabled: (projectPath, enabled) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardBorderEnabled: enabled,
},
},
})),
setCardBorderOpacity: (projectPath, opacity) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardBorderOpacity: opacity,
},
},
})),
setHideScrollbar: (projectPath, hide) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
hideScrollbar: hide,
},
},
})),
clearBoardBackground: (projectPath) =>
set((state) => {
const newBackgrounds = { ...state.boardBackgroundByProject };
delete newBackgrounds[projectPath];
return { boardBackgroundByProject: newBackgrounds };
}),
// Terminal actions // Terminal actions
setTerminalUnlocked: (unlocked, token) => setTerminalUnlocked: (unlocked, token) =>
@@ -2364,27 +2235,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}; };
}), }),
// Worktree Panel Visibility actions // Worktree Panel Visibility actions - provided by UI slice
setWorktreePanelVisible: (projectPath, visible) =>
set((state) => ({
worktreePanelVisibleByProject: {
...state.worktreePanelVisibleByProject,
[projectPath]: visible,
},
})),
getWorktreePanelVisible: (projectPath) =>
get().worktreePanelVisibleByProject[projectPath] ?? true,
// Init Script Indicator Visibility actions // Init Script Indicator Visibility actions - provided by UI slice
setShowInitScriptIndicator: (projectPath, visible) =>
set((state) => ({
showInitScriptIndicatorByProject: {
...state.showInitScriptIndicatorByProject,
[projectPath]: visible,
},
})),
getShowInitScriptIndicator: (projectPath) =>
get().showInitScriptIndicatorByProject[projectPath] ?? true,
// Default Delete Branch actions // Default Delete Branch actions
setDefaultDeleteBranch: (projectPath, deleteBranch) => setDefaultDeleteBranch: (projectPath, deleteBranch) =>
@@ -2396,16 +2249,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})), })),
getDefaultDeleteBranch: (projectPath) => get().defaultDeleteBranchByProject[projectPath] ?? false, getDefaultDeleteBranch: (projectPath) => get().defaultDeleteBranchByProject[projectPath] ?? false,
// Auto-dismiss Init Script Indicator actions // Auto-dismiss Init Script Indicator actions - provided by UI slice
setAutoDismissInitScriptIndicator: (projectPath, autoDismiss) =>
set((state) => ({
autoDismissInitScriptIndicatorByProject: {
...state.autoDismissInitScriptIndicatorByProject,
[projectPath]: autoDismiss,
},
})),
getAutoDismissInitScriptIndicator: (projectPath) =>
get().autoDismissInitScriptIndicatorByProject[projectPath] ?? true,
// Use Worktrees Override actions // Use Worktrees Override actions
setProjectUseWorktrees: (projectPath, useWorktrees) => setProjectUseWorktrees: (projectPath, useWorktrees) =>
@@ -2421,15 +2265,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
return projectOverride !== undefined ? projectOverride : get().useWorktrees; return projectOverride !== undefined ? projectOverride : get().useWorktrees;
}, },
// UI State actions // UI State actions - provided by UI slice
setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }),
setLastProjectDir: (dir) => set({ lastProjectDir: dir }),
setRecentFolders: (folders) => set({ recentFolders: folders }),
addRecentFolder: (folder) =>
set((state) => {
const filtered = state.recentFolders.filter((f) => f !== folder);
return { recentFolders: [folder, ...filtered].slice(0, 10) };
}),
// Claude Usage Tracking actions // Claude Usage Tracking actions
setClaudeRefreshInterval: (interval) => set({ claudeRefreshInterval: interval }), setClaudeRefreshInterval: (interval) => set({ claudeRefreshInterval: interval }),
@@ -2471,8 +2307,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
try { try {
const httpApi = getHttpApiClient(); const httpApi = getHttpApiClient();
const response = await httpApi.get('/api/codex/models'); const data = await httpApi.get<{
const data = response.data as {
success: boolean; success: boolean;
models?: Array<{ models?: Array<{
id: string; id: string;
@@ -2484,7 +2319,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
isDefault: boolean; isDefault: boolean;
}>; }>;
error?: string; error?: string;
}; }>('/api/codex/models');
if (data.success && data.models) { if (data.success && data.models) {
set({ set({
@@ -2542,8 +2377,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
try { try {
const httpApi = getHttpApiClient(); const httpApi = getHttpApiClient();
const response = await httpApi.get('/api/opencode/models'); const data = await httpApi.get<{
const data = response.data as {
success: boolean; success: boolean;
models?: ModelDefinition[]; models?: ModelDefinition[];
providers?: Array<{ providers?: Array<{
@@ -2553,7 +2387,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
authMethod?: string; authMethod?: string;
}>; }>;
error?: string; error?: string;
}; }>('/api/setup/opencode/models');
if (data.success && data.models) { if (data.success && data.models) {
// Filter out Bedrock models // Filter out Bedrock models

View File

@@ -0,0 +1 @@
export { createUISlice, initialUIState, type UISlice } from './ui-slice';

View File

@@ -0,0 +1,343 @@
import type { StateCreator } from 'zustand';
import { UI_SANS_FONT_OPTIONS, UI_MONO_FONT_OPTIONS } from '@/config/ui-font-options';
import type { SidebarStyle } from '@automaker/types';
import type {
ViewMode,
ThemeMode,
BoardViewMode,
KeyboardShortcuts,
BackgroundSettings,
UISliceState,
UISliceActions,
} from '../types/ui-types';
import type { AppState, AppActions } from '../types/state-types';
import {
getStoredTheme,
getStoredFontSans,
getStoredFontMono,
DEFAULT_KEYBOARD_SHORTCUTS,
} from '../utils';
import { defaultBackgroundSettings } from '../defaults';
import {
getEffectiveFont,
saveThemeToStorage,
saveFontSansToStorage,
saveFontMonoToStorage,
} from '../utils/theme-utils';
/**
* UI Slice
* Contains all UI-related state and actions extracted from the main app store.
* This is the first slice pattern implementation in the codebase.
*/
export type UISlice = UISliceState & UISliceActions;
/**
* Initial UI state values
*/
export const initialUIState: UISliceState = {
// Core UI State
currentView: 'welcome',
sidebarOpen: true,
sidebarStyle: 'unified',
collapsedNavSections: {},
mobileSidebarHidden: false,
// Theme State
theme: getStoredTheme() || 'dark',
previewTheme: null,
// Font State
fontFamilySans: getStoredFontSans(),
fontFamilyMono: getStoredFontMono(),
// Board UI State
boardViewMode: 'kanban',
boardBackgroundByProject: {},
// Settings UI State
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
muteDoneSound: false,
disableSplashScreen: false,
showQueryDevtools: true,
chatHistoryOpen: false,
// Panel Visibility State
worktreePanelCollapsed: false,
worktreePanelVisibleByProject: {},
showInitScriptIndicatorByProject: {},
autoDismissInitScriptIndicatorByProject: {},
// File Picker UI State
lastProjectDir: '',
recentFolders: [],
};
/**
* Creates the UI slice for the Zustand store.
*
* Uses the StateCreator pattern to allow the slice to access other parts
* of the combined store state (e.g., currentProject for theme resolution).
*/
export const createUISlice: StateCreator<AppState & AppActions, [], [], UISlice> = (set, get) => ({
// Spread initial state
...initialUIState,
// ============================================================================
// View Actions
// ============================================================================
setCurrentView: (view: ViewMode) => set({ currentView: view }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }),
setSidebarStyle: (style: SidebarStyle) => set({ sidebarStyle: style }),
setCollapsedNavSections: (sections: Record<string, boolean>) =>
set({ collapsedNavSections: sections }),
toggleNavSection: (sectionLabel: string) =>
set((state) => ({
collapsedNavSections: {
...state.collapsedNavSections,
[sectionLabel]: !state.collapsedNavSections[sectionLabel],
},
})),
toggleMobileSidebarHidden: () =>
set((state) => ({ mobileSidebarHidden: !state.mobileSidebarHidden })),
setMobileSidebarHidden: (hidden: boolean) => set({ mobileSidebarHidden: hidden }),
// ============================================================================
// Theme Actions
// ============================================================================
setTheme: (theme: ThemeMode) => {
set({ theme });
saveThemeToStorage(theme);
},
getEffectiveTheme: (): ThemeMode => {
const state = get();
// If there's a preview theme, use it (for hover preview)
if (state.previewTheme) return state.previewTheme;
// Otherwise, use project theme if set, or fall back to global theme
const projectTheme = state.currentProject?.theme as ThemeMode | undefined;
return projectTheme ?? state.theme;
},
setPreviewTheme: (theme: ThemeMode | null) => set({ previewTheme: theme }),
// ============================================================================
// Font Actions
// ============================================================================
setFontSans: (fontFamily: string | null) => {
set({ fontFamilySans: fontFamily });
saveFontSansToStorage(fontFamily);
},
setFontMono: (fontFamily: string | null) => {
set({ fontFamilyMono: fontFamily });
saveFontMonoToStorage(fontFamily);
},
getEffectiveFontSans: (): string | null => {
const state = get();
const projectFont = state.currentProject?.fontFamilySans;
return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS);
},
getEffectiveFontMono: (): string | null => {
const state = get();
const projectFont = state.currentProject?.fontFamilyMono;
return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS);
},
// ============================================================================
// Board View Actions
// ============================================================================
setBoardViewMode: (mode: BoardViewMode) => set({ boardViewMode: mode }),
setBoardBackground: (projectPath: string, imagePath: string | null) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
imagePath,
imageVersion: Date.now(), // Bust cache on image change
},
},
})),
setCardOpacity: (projectPath: string, opacity: number) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardOpacity: opacity,
},
},
})),
setColumnOpacity: (projectPath: string, opacity: number) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
columnOpacity: opacity,
},
},
})),
setColumnBorderEnabled: (projectPath: string, enabled: boolean) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
columnBorderEnabled: enabled,
},
},
})),
setCardGlassmorphism: (projectPath: string, enabled: boolean) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardGlassmorphism: enabled,
},
},
})),
setCardBorderEnabled: (projectPath: string, enabled: boolean) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardBorderEnabled: enabled,
},
},
})),
setCardBorderOpacity: (projectPath: string, opacity: number) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
cardBorderOpacity: opacity,
},
},
})),
setHideScrollbar: (projectPath: string, hide: boolean) =>
set((state) => ({
boardBackgroundByProject: {
...state.boardBackgroundByProject,
[projectPath]: {
...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings),
hideScrollbar: hide,
},
},
})),
getBoardBackground: (projectPath: string): BackgroundSettings =>
get().boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings,
clearBoardBackground: (projectPath: string) =>
set((state) => {
const newBackgrounds = { ...state.boardBackgroundByProject };
delete newBackgrounds[projectPath];
return { boardBackgroundByProject: newBackgrounds };
}),
// ============================================================================
// Settings UI Actions
// ============================================================================
setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, [key]: value },
})),
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, ...shortcuts },
})),
resetKeyboardShortcuts: () => set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }),
setMuteDoneSound: (muted: boolean) => set({ muteDoneSound: muted }),
setDisableSplashScreen: (disabled: boolean) => set({ disableSplashScreen: disabled }),
setShowQueryDevtools: (show: boolean) => set({ showQueryDevtools: show }),
setChatHistoryOpen: (open: boolean) => set({ chatHistoryOpen: open }),
toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })),
// ============================================================================
// Panel Visibility Actions
// ============================================================================
setWorktreePanelCollapsed: (collapsed: boolean) => set({ worktreePanelCollapsed: collapsed }),
setWorktreePanelVisible: (projectPath: string, visible: boolean) =>
set((state) => ({
worktreePanelVisibleByProject: {
...state.worktreePanelVisibleByProject,
[projectPath]: visible,
},
})),
getWorktreePanelVisible: (projectPath: string): boolean =>
get().worktreePanelVisibleByProject[projectPath] ?? true,
setShowInitScriptIndicator: (projectPath: string, visible: boolean) =>
set((state) => ({
showInitScriptIndicatorByProject: {
...state.showInitScriptIndicatorByProject,
[projectPath]: visible,
},
})),
getShowInitScriptIndicator: (projectPath: string): boolean =>
get().showInitScriptIndicatorByProject[projectPath] ?? true,
setAutoDismissInitScriptIndicator: (projectPath: string, autoDismiss: boolean) =>
set((state) => ({
autoDismissInitScriptIndicatorByProject: {
...state.autoDismissInitScriptIndicatorByProject,
[projectPath]: autoDismiss,
},
})),
getAutoDismissInitScriptIndicator: (projectPath: string): boolean =>
get().autoDismissInitScriptIndicatorByProject[projectPath] ?? true,
// ============================================================================
// File Picker UI Actions
// ============================================================================
setLastProjectDir: (dir: string) => set({ lastProjectDir: dir }),
setRecentFolders: (folders: string[]) => set({ recentFolders: folders }),
addRecentFolder: (folder: string) =>
set((state) => {
const filtered = state.recentFolders.filter((f) => f !== folder);
return { recentFolders: [folder, ...filtered].slice(0, 10) };
}),
});

View File

@@ -117,3 +117,112 @@ export interface KeyboardShortcuts {
closeTerminal: string; closeTerminal: string;
newTerminalTab: string; newTerminalTab: string;
} }
// Import SidebarStyle from @automaker/types for UI slice
import type { SidebarStyle } from '@automaker/types';
/**
* UI Slice State
* Contains all UI-related state that is extracted into the UI slice.
*/
export interface UISliceState {
// Core UI State
currentView: ViewMode;
sidebarOpen: boolean;
sidebarStyle: SidebarStyle;
collapsedNavSections: Record<string, boolean>;
mobileSidebarHidden: boolean;
// Theme State
theme: ThemeMode;
previewTheme: ThemeMode | null;
// Font State
fontFamilySans: string | null;
fontFamilyMono: string | null;
// Board UI State
boardViewMode: BoardViewMode;
boardBackgroundByProject: Record<string, BackgroundSettings>;
// Settings UI State
keyboardShortcuts: KeyboardShortcuts;
muteDoneSound: boolean;
disableSplashScreen: boolean;
showQueryDevtools: boolean;
chatHistoryOpen: boolean;
// Panel Visibility State
worktreePanelCollapsed: boolean;
worktreePanelVisibleByProject: Record<string, boolean>;
showInitScriptIndicatorByProject: Record<string, boolean>;
autoDismissInitScriptIndicatorByProject: Record<string, boolean>;
// File Picker UI State
lastProjectDir: string;
recentFolders: string[];
}
/**
* UI Slice Actions
* Contains all UI-related actions that are extracted into the UI slice.
*/
export interface UISliceActions {
// View Actions
setCurrentView: (view: ViewMode) => void;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setSidebarStyle: (style: SidebarStyle) => void;
setCollapsedNavSections: (sections: Record<string, boolean>) => void;
toggleNavSection: (sectionLabel: string) => void;
toggleMobileSidebarHidden: () => void;
setMobileSidebarHidden: (hidden: boolean) => void;
// Theme Actions (Pure UI only - project theme actions stay in main store)
setTheme: (theme: ThemeMode) => void;
getEffectiveTheme: () => ThemeMode;
setPreviewTheme: (theme: ThemeMode | null) => void;
// Font Actions (Pure UI only - project font actions stay in main store)
setFontSans: (fontFamily: string | null) => void;
setFontMono: (fontFamily: string | null) => void;
getEffectiveFontSans: () => string | null;
getEffectiveFontMono: () => string | null;
// Board View Actions
setBoardViewMode: (mode: BoardViewMode) => void;
setBoardBackground: (projectPath: string, imagePath: string | null) => void;
setCardOpacity: (projectPath: string, opacity: number) => void;
setColumnOpacity: (projectPath: string, opacity: number) => void;
setColumnBorderEnabled: (projectPath: string, enabled: boolean) => void;
setCardGlassmorphism: (projectPath: string, enabled: boolean) => void;
setCardBorderEnabled: (projectPath: string, enabled: boolean) => void;
setCardBorderOpacity: (projectPath: string, opacity: number) => void;
setHideScrollbar: (projectPath: string, hide: boolean) => void;
getBoardBackground: (projectPath: string) => BackgroundSettings;
clearBoardBackground: (projectPath: string) => void;
// Settings UI Actions
setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void;
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) => void;
resetKeyboardShortcuts: () => void;
setMuteDoneSound: (muted: boolean) => void;
setDisableSplashScreen: (disabled: boolean) => void;
setShowQueryDevtools: (show: boolean) => void;
setChatHistoryOpen: (open: boolean) => void;
toggleChatHistory: () => void;
// Panel Visibility Actions
setWorktreePanelCollapsed: (collapsed: boolean) => void;
setWorktreePanelVisible: (projectPath: string, visible: boolean) => void;
getWorktreePanelVisible: (projectPath: string) => boolean;
setShowInitScriptIndicator: (projectPath: string, visible: boolean) => void;
getShowInitScriptIndicator: (projectPath: string) => boolean;
setAutoDismissInitScriptIndicator: (projectPath: string, autoDismiss: boolean) => void;
getAutoDismissInitScriptIndicator: (projectPath: string) => boolean;
// File Picker UI Actions
setLastProjectDir: (dir: string) => void;
setRecentFolders: (folders: string[]) => void;
addRecentFolder: (folder: string) => void;
}

View File

@@ -965,8 +965,20 @@ export const DEFAULT_PLAN_REVISION_TEMPLATE = `The user has requested revisions
## Instructions ## Instructions
Please regenerate the specification incorporating the user's feedback. Please regenerate the specification incorporating the user's feedback.
Keep the same format with the \`\`\`tasks block for task definitions. **Current planning mode: {{planningMode}}**
After generating the revised spec, output:
**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."`; "[SPEC_GENERATED] Please review the revised specification above."`;
export const DEFAULT_CONTINUATION_AFTER_APPROVAL_TEMPLATE = `The plan/specification has been approved. Now implement it. export const DEFAULT_CONTINUATION_AFTER_APPROVAL_TEMPLATE = `The plan/specification has been approved. Now implement it.