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:
Shirone
2026-01-25 11:45:37 +01:00
18 changed files with 687 additions and 124 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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>
);
}

View File

@@ -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 */}

View File

@@ -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);
};