mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge remote-tracking branch 'origin/v0.14.0rc' into feature/bug-complete-fix-for-the-plan-mode-system-inside-sbyt
This commit is contained in:
@@ -95,12 +95,20 @@ export function useWorktrees({
|
||||
);
|
||||
|
||||
// fetchWorktrees for backward compatibility - now just triggers a refetch
|
||||
const fetchWorktrees = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.worktrees.all(projectPath),
|
||||
});
|
||||
return refetch();
|
||||
}, [projectPath, queryClient, refetch]);
|
||||
// The silent option is accepted but not used (React Query handles loading states)
|
||||
// Returns removed worktrees array if any were detected, undefined otherwise
|
||||
const fetchWorktrees = useCallback(
|
||||
async (_options?: {
|
||||
silent?: boolean;
|
||||
}): Promise<Array<{ path: string; branch: string }> | undefined> => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.worktrees.all(projectPath),
|
||||
});
|
||||
const result = await refetch();
|
||||
return result.data?.removedWorktrees;
|
||||
},
|
||||
[projectPath, queryClient, refetch]
|
||||
);
|
||||
|
||||
const currentWorktreePath = currentWorktree?.path ?? null;
|
||||
const selectedWorktree = currentWorktreePath
|
||||
|
||||
@@ -383,13 +383,13 @@ export function WorktreePanel({
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
||||
// Periodic interval check (30 seconds) to detect branch changes on disk
|
||||
// Reduced polling to lessen repeated worktree list calls while keeping UI reasonably fresh
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
useEffect(() => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchWorktrees({ silent: true });
|
||||
}, 5000);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* IdeationSettingsPopover - Configure context sources for idea generation
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Settings2, FileText, Brain, LayoutGrid, Lightbulb, ScrollText } from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useIdeationStore } from '@/store/ideation-store';
|
||||
import { DEFAULT_IDEATION_CONTEXT_SOURCES, type IdeationContextSources } from '@automaker/types';
|
||||
|
||||
interface IdeationSettingsPopoverProps {
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
const IDEATION_CONTEXT_OPTIONS: Array<{
|
||||
key: keyof IdeationContextSources;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: typeof FileText;
|
||||
}> = [
|
||||
{
|
||||
key: 'useAppSpec',
|
||||
label: 'App Specification',
|
||||
description: 'Overview, capabilities, features',
|
||||
icon: ScrollText,
|
||||
},
|
||||
{
|
||||
key: 'useContextFiles',
|
||||
label: 'Context Files',
|
||||
description: '.automaker/context/*.md|.txt',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
key: 'useMemoryFiles',
|
||||
label: 'Memory Files',
|
||||
description: '.automaker/memory/*.md',
|
||||
icon: Brain,
|
||||
},
|
||||
{
|
||||
key: 'useExistingFeatures',
|
||||
label: 'Existing Features',
|
||||
description: 'Board features list',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
key: 'useExistingIdeas',
|
||||
label: 'Existing Ideas',
|
||||
description: 'Ideation ideas list',
|
||||
icon: Lightbulb,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Renders a settings popover to toggle per-project ideation context sources.
|
||||
* Merges defaults with stored overrides and persists changes via the ideation store.
|
||||
*/
|
||||
export function IdeationSettingsPopover({ projectPath }: IdeationSettingsPopoverProps) {
|
||||
const { projectOverrides, setContextSource } = useIdeationStore(
|
||||
useShallow((state) => ({
|
||||
projectOverrides: state.contextSourcesByProject[projectPath],
|
||||
setContextSource: state.setContextSource,
|
||||
}))
|
||||
);
|
||||
const contextSources = useMemo(
|
||||
() => ({ ...DEFAULT_IDEATION_CONTEXT_SOURCES, ...projectOverrides }),
|
||||
[projectOverrides]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 border rounded hover:bg-accent/50 transition-colors"
|
||||
title="Generation Settings"
|
||||
aria-label="Generation settings"
|
||||
data-testid="ideation-context-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="end" sideOffset={8}>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Generation Settings</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure which context sources are included when generating ideas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{IDEATION_CONTEXT_OPTIONS.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-3 p-2 rounded-md bg-secondary/50"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Icon className="w-4 h-4 text-brand-500 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<Label
|
||||
htmlFor={`ideation-context-toggle-${option.key}`}
|
||||
className="text-xs font-medium cursor-pointer block"
|
||||
>
|
||||
{option.label}
|
||||
</Label>
|
||||
<span className="text-[10px] text-muted-foreground truncate block">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id={`ideation-context-toggle-${option.key}`}
|
||||
checked={contextSources[option.key]}
|
||||
onCheckedChange={(checked) =>
|
||||
setContextSource(projectPath, option.key, checked)
|
||||
}
|
||||
data-testid={`ideation-context-toggle-${option.key}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
Disable sources to generate more focused ideas or reduce context size.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Trash2 } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { IdeationSettingsPopover } from './components/ideation-settings-popover';
|
||||
import type { IdeaCategory } from '@automaker/types';
|
||||
import type { IdeationMode } from '@/store/ideation-store';
|
||||
|
||||
@@ -61,7 +62,10 @@ function IdeationBreadcrumbs({
|
||||
);
|
||||
}
|
||||
|
||||
// Header shown on all pages - matches other view headers
|
||||
/**
|
||||
* Header component for the ideation view with navigation, bulk actions, and settings.
|
||||
* Displays breadcrumbs, accept/discard all buttons, and the generate ideas button with settings popover.
|
||||
*/
|
||||
function IdeationHeader({
|
||||
currentMode,
|
||||
selectedCategory,
|
||||
@@ -75,6 +79,7 @@ function IdeationHeader({
|
||||
discardAllReady,
|
||||
discardAllCount,
|
||||
onDiscardAll,
|
||||
projectPath,
|
||||
}: {
|
||||
currentMode: IdeationMode;
|
||||
selectedCategory: IdeaCategory | null;
|
||||
@@ -88,6 +93,7 @@ function IdeationHeader({
|
||||
discardAllReady: boolean;
|
||||
discardAllCount: number;
|
||||
onDiscardAll: () => void;
|
||||
projectPath: string;
|
||||
}) {
|
||||
const { getCategoryById } = useGuidedPrompts();
|
||||
const showBackButton = currentMode === 'prompts';
|
||||
@@ -157,15 +163,23 @@ function IdeationHeader({
|
||||
Accept All ({acceptAllCount})
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onGenerateIdeas} className="gap-2">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
Generate Ideas
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={onGenerateIdeas} className="gap-2">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
Generate Ideas
|
||||
</Button>
|
||||
<IdeationSettingsPopover projectPath={projectPath} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main view for brainstorming and idea management.
|
||||
* Provides a dashboard for reviewing generated ideas and a prompt selection flow
|
||||
* for generating new ideas using AI-powered suggestions.
|
||||
*/
|
||||
export function IdeationView() {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { currentMode, selectedCategory, setMode, setCategory } = useIdeationStore();
|
||||
@@ -282,6 +296,7 @@ export function IdeationView() {
|
||||
discardAllReady={discardAllReady}
|
||||
discardAllCount={discardAllCount}
|
||||
onDiscardAll={handleDiscardAll}
|
||||
projectPath={currentProject.path}
|
||||
/>
|
||||
|
||||
{/* Dashboard - main view */}
|
||||
|
||||
@@ -104,7 +104,10 @@ function FeatureDefaultModelOverrideSection({ project }: { project: Project }) {
|
||||
const hasOverride = !!projectOverride;
|
||||
const effectiveValue = projectOverride || globalValue;
|
||||
|
||||
// Get display name for a model
|
||||
/**
|
||||
* Formats a user-friendly model label using provider metadata when available,
|
||||
* falling back to known Claude aliases or the raw model id.
|
||||
*/
|
||||
const getModelDisplayName = (entry: PhaseModelEntry): string => {
|
||||
if (entry.providerId) {
|
||||
const provider = (claudeCompatibleProviders || []).find((p) => p.id === entry.providerId);
|
||||
@@ -127,10 +130,16 @@ function FeatureDefaultModelOverrideSection({ project }: { project: Project }) {
|
||||
return modelMap[entry.model] || entry.model;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the project-level model override for this scope.
|
||||
*/
|
||||
const handleClearOverride = () => {
|
||||
setProjectDefaultFeatureModel(project.id, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the project-level model override for this scope.
|
||||
*/
|
||||
const handleSetOverride = (entry: PhaseModelEntry) => {
|
||||
setProjectDefaultFeatureModel(project.id, entry);
|
||||
};
|
||||
@@ -209,6 +218,10 @@ function FeatureDefaultModelOverrideSection({ project }: { project: Project }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single phase override row, showing the effective model
|
||||
* (project override or global default) and wiring selector/reset actions.
|
||||
*/
|
||||
function PhaseOverrideItem({
|
||||
phase,
|
||||
project,
|
||||
@@ -225,7 +238,10 @@ function PhaseOverrideItem({
|
||||
const hasOverride = !!projectOverride;
|
||||
const effectiveValue = projectOverride || globalValue;
|
||||
|
||||
// Get display name for a model
|
||||
/**
|
||||
* Formats a user-friendly model label using provider metadata when available,
|
||||
* falling back to known Claude aliases or the raw model id.
|
||||
*/
|
||||
const getModelDisplayName = (entry: PhaseModelEntry): string => {
|
||||
if (entry.providerId) {
|
||||
const provider = (claudeCompatibleProviders || []).find((p) => p.id === entry.providerId);
|
||||
@@ -248,10 +264,16 @@ function PhaseOverrideItem({
|
||||
return modelMap[entry.model] || entry.model;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the project-level model override for this scope.
|
||||
*/
|
||||
const handleClearOverride = () => {
|
||||
setProjectPhaseModelOverride(project.id, phase.key, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the project-level model override for this scope.
|
||||
*/
|
||||
const handleSetOverride = (entry: PhaseModelEntry) => {
|
||||
setProjectPhaseModelOverride(project.id, phase.key, entry);
|
||||
};
|
||||
@@ -315,6 +337,10 @@ function PhaseOverrideItem({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a titled group of phase override rows and resolves each phase's
|
||||
* global default model with a fallback to DEFAULT_PHASE_MODELS.
|
||||
*/
|
||||
function PhaseGroup({
|
||||
title,
|
||||
subtitle,
|
||||
@@ -350,9 +376,11 @@ function PhaseGroup({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the per-project model overrides UI for all phase models.
|
||||
*/
|
||||
export function ProjectModelsSection({ project }: ProjectModelsSectionProps) {
|
||||
const { clearAllProjectPhaseModelOverrides, disabledProviders, claudeCompatibleProviders } =
|
||||
useAppStore();
|
||||
const { clearAllProjectPhaseModelOverrides, claudeCompatibleProviders } = useAppStore();
|
||||
const [showBulkReplace, setShowBulkReplace] = useState(false);
|
||||
|
||||
// Count how many overrides are set (including defaultFeatureModel)
|
||||
@@ -360,25 +388,13 @@ export function ProjectModelsSection({ project }: ProjectModelsSectionProps) {
|
||||
const hasDefaultFeatureModelOverride = !!project.defaultFeatureModel;
|
||||
const overrideCount = phaseOverrideCount + (hasDefaultFeatureModelOverride ? 1 : 0);
|
||||
|
||||
// Check if Claude is available
|
||||
const isClaudeDisabled = disabledProviders.includes('claude');
|
||||
|
||||
// Check if there are any enabled ClaudeCompatibleProviders
|
||||
const hasEnabledProviders =
|
||||
claudeCompatibleProviders && claudeCompatibleProviders.some((p) => p.enabled !== false);
|
||||
|
||||
if (isClaudeDisabled) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Workflow className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">Claude not configured</p>
|
||||
<p className="text-xs mt-1">
|
||||
Enable Claude in global settings to configure per-project model overrides.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all project-level phase model overrides for this project.
|
||||
*/
|
||||
const handleClearAll = () => {
|
||||
clearAllProjectPhaseModelOverrides(project.id);
|
||||
};
|
||||
|
||||
@@ -68,7 +68,16 @@ export function useGenerateIdeationSuggestions(projectPath: string) {
|
||||
throw new Error('Ideation API not available');
|
||||
}
|
||||
|
||||
const result = await api.ideation.generateSuggestions(projectPath, promptId, category);
|
||||
// Get context sources from store
|
||||
const contextSources = useIdeationStore.getState().getContextSources(projectPath);
|
||||
|
||||
const result = await api.ideation.generateSuggestions(
|
||||
projectPath,
|
||||
promptId,
|
||||
category,
|
||||
undefined, // count - use default
|
||||
contextSources
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to generate suggestions');
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
ConvertToFeatureOptions,
|
||||
IdeationContextSources,
|
||||
} from '@automaker/types';
|
||||
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
|
||||
import { getJSON, setJSON, removeItem } from './storage';
|
||||
@@ -114,7 +115,8 @@ export interface IdeationAPI {
|
||||
projectPath: string,
|
||||
promptId: string,
|
||||
category: IdeaCategory,
|
||||
count?: number
|
||||
count?: number,
|
||||
contextSources?: IdeationContextSources
|
||||
) => Promise<{ success: boolean; suggestions?: AnalysisSuggestion[]; error?: string }>;
|
||||
|
||||
// Convert to feature
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
NotificationsAPI,
|
||||
EventHistoryAPI,
|
||||
} from './electron';
|
||||
import type { IdeationContextSources } from '@automaker/types';
|
||||
import type { EventHistoryFilter } from '@automaker/types';
|
||||
import type { Message, SessionListItem } from '@/types/electron';
|
||||
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||
@@ -2739,9 +2740,16 @@ export class HttpApiClient implements ElectronAPI {
|
||||
projectPath: string,
|
||||
promptId: string,
|
||||
category: IdeaCategory,
|
||||
count?: number
|
||||
count?: number,
|
||||
contextSources?: IdeationContextSources
|
||||
) =>
|
||||
this.post('/api/ideation/suggestions/generate', { projectPath, promptId, category, count }),
|
||||
this.post('/api/ideation/suggestions/generate', {
|
||||
projectPath,
|
||||
promptId,
|
||||
category,
|
||||
count,
|
||||
contextSources,
|
||||
}),
|
||||
|
||||
convertToFeature: (projectPath: string, ideaId: string, options?: ConvertToFeatureOptions) =>
|
||||
this.post('/api/ideation/convert', { projectPath, ideaId, ...options }),
|
||||
|
||||
@@ -11,7 +11,9 @@ import type {
|
||||
IdeationPrompt,
|
||||
AnalysisSuggestion,
|
||||
ProjectAnalysisResult,
|
||||
IdeationContextSources,
|
||||
} from '@automaker/types';
|
||||
import { DEFAULT_IDEATION_CONTEXT_SOURCES } from '@automaker/types';
|
||||
|
||||
// ============================================================================
|
||||
// Generation Job Types
|
||||
@@ -61,6 +63,9 @@ interface IdeationState {
|
||||
currentMode: IdeationMode;
|
||||
selectedCategory: IdeaCategory | null;
|
||||
filterStatus: IdeaStatus | 'all';
|
||||
|
||||
// Context sources per project
|
||||
contextSourcesByProject: Record<string, Partial<IdeationContextSources>>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -110,6 +115,21 @@ interface IdeationActions {
|
||||
setCategory: (category: IdeaCategory | null) => void;
|
||||
setFilterStatus: (status: IdeaStatus | 'all') => void;
|
||||
|
||||
// Context sources
|
||||
/**
|
||||
* Returns the effective context-source settings for a project,
|
||||
* merging defaults with any stored overrides.
|
||||
*/
|
||||
getContextSources: (projectPath: string) => IdeationContextSources;
|
||||
/**
|
||||
* Updates a single context-source flag for a project.
|
||||
*/
|
||||
setContextSource: (
|
||||
projectPath: string,
|
||||
key: keyof IdeationContextSources,
|
||||
value: boolean
|
||||
) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
resetSuggestions: () => void;
|
||||
@@ -135,6 +155,7 @@ const initialState: IdeationState = {
|
||||
currentMode: 'dashboard',
|
||||
selectedCategory: null,
|
||||
filterStatus: 'all',
|
||||
contextSourcesByProject: {},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -300,6 +321,24 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
|
||||
setFilterStatus: (status) => set({ filterStatus: status }),
|
||||
|
||||
// Context sources
|
||||
getContextSources: (projectPath) => {
|
||||
const state = get();
|
||||
const projectOverrides = state.contextSourcesByProject[projectPath] ?? {};
|
||||
return { ...DEFAULT_IDEATION_CONTEXT_SOURCES, ...projectOverrides };
|
||||
},
|
||||
|
||||
setContextSource: (projectPath, key, value) =>
|
||||
set((state) => ({
|
||||
contextSourcesByProject: {
|
||||
...state.contextSourcesByProject,
|
||||
[projectPath]: {
|
||||
...state.contextSourcesByProject[projectPath],
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState),
|
||||
|
||||
@@ -313,13 +352,14 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
}),
|
||||
{
|
||||
name: 'automaker-ideation-store',
|
||||
version: 4,
|
||||
version: 5,
|
||||
partialize: (state) => ({
|
||||
// Only persist these fields
|
||||
ideas: state.ideas,
|
||||
generationJobs: state.generationJobs,
|
||||
analysisResult: state.analysisResult,
|
||||
filterStatus: state.filterStatus,
|
||||
contextSourcesByProject: state.contextSourcesByProject,
|
||||
}),
|
||||
migrate: (persistedState: unknown, version: number) => {
|
||||
const state = persistedState as Record<string, unknown>;
|
||||
@@ -331,6 +371,13 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
generationJobs: jobs.filter((job) => job.projectPath !== undefined),
|
||||
};
|
||||
}
|
||||
if (version < 5) {
|
||||
// Initialize contextSourcesByProject if not present
|
||||
return {
|
||||
...state,
|
||||
contextSourcesByProject: state.contextSourcesByProject ?? {},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user