mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'v0.11.0rc' into claude/issue-469-20260113-1744
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||
import type { ModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||
|
||||
export interface UseModelOverrideOptions {
|
||||
@@ -14,7 +14,7 @@ export interface UseModelOverrideResult {
|
||||
/** The effective model entry (override or global default) */
|
||||
effectiveModelEntry: PhaseModelEntry;
|
||||
/** The effective model string (for backward compatibility with APIs that only accept strings) */
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
effectiveModel: ModelId;
|
||||
/** Whether the model is currently overridden */
|
||||
isOverridden: boolean;
|
||||
/** Set a model override */
|
||||
@@ -32,7 +32,7 @@ export interface UseModelOverrideResult {
|
||||
*/
|
||||
function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||
if (typeof entry === 'string') {
|
||||
return { model: entry as ModelAlias | CursorModelId };
|
||||
return { model: entry as ModelId };
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
@@ -40,9 +40,9 @@ function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string
|
||||
*/
|
||||
function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId {
|
||||
function extractModel(entry: PhaseModelEntry | string): ModelId {
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
return entry as ModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
@@ -422,6 +422,31 @@ export function BoardView() {
|
||||
const selectedWorktreeBranch =
|
||||
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
|
||||
|
||||
// Helper function to add and select a worktree
|
||||
const addAndSelectWorktree = useCallback(
|
||||
(worktreeResult: { path: string; branch: string }) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const currentWorktrees = getWorktrees(currentProject.path);
|
||||
const existingWorktree = currentWorktrees.find((w) => w.branch === worktreeResult.branch);
|
||||
|
||||
// Only add if it doesn't already exist (to avoid duplicates)
|
||||
if (!existingWorktree) {
|
||||
const newWorktreeInfo = {
|
||||
path: worktreeResult.path,
|
||||
branch: worktreeResult.branch,
|
||||
isMain: false,
|
||||
isCurrent: false,
|
||||
hasWorktree: true,
|
||||
};
|
||||
setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]);
|
||||
}
|
||||
// Select the worktree (whether it existed or was just added)
|
||||
setCurrentWorktree(currentProject.path, worktreeResult.path, worktreeResult.branch);
|
||||
},
|
||||
[currentProject, getWorktrees, setWorktrees, setCurrentWorktree]
|
||||
);
|
||||
|
||||
// Extract all action handlers into a hook
|
||||
const {
|
||||
handleAddFeature,
|
||||
@@ -467,43 +492,90 @@ export function BoardView() {
|
||||
outputFeature,
|
||||
projectPath: currentProject?.path || null,
|
||||
onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1),
|
||||
onWorktreeAutoSelect: (newWorktree) => {
|
||||
if (!currentProject) return;
|
||||
// Check if worktree already exists in the store (by branch name)
|
||||
const currentWorktrees = getWorktrees(currentProject.path);
|
||||
const existingWorktree = currentWorktrees.find((w) => w.branch === newWorktree.branch);
|
||||
|
||||
// Only add if it doesn't already exist (to avoid duplicates)
|
||||
if (!existingWorktree) {
|
||||
const newWorktreeInfo = {
|
||||
path: newWorktree.path,
|
||||
branch: newWorktree.branch,
|
||||
isMain: false,
|
||||
isCurrent: false,
|
||||
hasWorktree: true,
|
||||
};
|
||||
setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]);
|
||||
}
|
||||
// Select the worktree (whether it existed or was just added)
|
||||
setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch);
|
||||
},
|
||||
onWorktreeAutoSelect: addAndSelectWorktree,
|
||||
currentWorktreeBranch,
|
||||
});
|
||||
|
||||
// Handler for bulk updating multiple features
|
||||
const handleBulkUpdate = useCallback(
|
||||
async (updates: Partial<Feature>) => {
|
||||
async (updates: Partial<Feature>, workMode: 'current' | 'auto' | 'custom') => {
|
||||
if (!currentProject || selectedFeatureIds.size === 0) return;
|
||||
|
||||
try {
|
||||
// Determine final branch name based on work mode:
|
||||
// - 'current': Empty string to clear branch assignment (work on main/current branch)
|
||||
// - 'auto': Auto-generate branch name based on current branch
|
||||
// - 'custom': Use the provided branch name
|
||||
let finalBranchName: string | undefined;
|
||||
|
||||
if (workMode === 'current') {
|
||||
// Empty string clears the branch assignment, moving features to main/current branch
|
||||
finalBranchName = '';
|
||||
} else if (workMode === 'auto') {
|
||||
// Auto-generate a branch name based on current branch and timestamp
|
||||
const baseBranch =
|
||||
currentWorktreeBranch || getPrimaryWorktreeBranch(currentProject.path) || 'main';
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||
} else {
|
||||
// Custom mode - use provided branch name
|
||||
finalBranchName = updates.branchName || undefined;
|
||||
}
|
||||
|
||||
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName) {
|
||||
try {
|
||||
const electronApi = getElectronAPI();
|
||||
if (electronApi?.worktree?.create) {
|
||||
const result = await electronApi.worktree.create(
|
||||
currentProject.path,
|
||||
finalBranchName
|
||||
);
|
||||
if (result.success && result.worktree) {
|
||||
logger.info(
|
||||
`Worktree for branch "${finalBranchName}" ${
|
||||
result.worktree?.isNew ? 'created' : 'already exists'
|
||||
}`
|
||||
);
|
||||
// Auto-select the worktree when creating/using it for bulk update
|
||||
addAndSelectWorktree(result.worktree);
|
||||
// Refresh worktree list in UI
|
||||
setWorktreeRefreshKey((k) => k + 1);
|
||||
} else if (!result.success) {
|
||||
logger.error(
|
||||
`Failed to create worktree for branch "${finalBranchName}":`,
|
||||
result.error
|
||||
);
|
||||
toast.error('Failed to create worktree', {
|
||||
description: result.error || 'An error occurred',
|
||||
});
|
||||
return; // Don't proceed with update if worktree creation failed
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error creating worktree:', error);
|
||||
toast.error('Failed to create worktree', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return; // Don't proceed with update if worktree creation failed
|
||||
}
|
||||
}
|
||||
|
||||
// Use the final branch name in updates
|
||||
const finalUpdates = {
|
||||
...updates,
|
||||
branchName: finalBranchName,
|
||||
};
|
||||
|
||||
const api = getHttpApiClient();
|
||||
const featureIds = Array.from(selectedFeatureIds);
|
||||
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
|
||||
const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates);
|
||||
|
||||
if (result.success) {
|
||||
// Update local state
|
||||
featureIds.forEach((featureId) => {
|
||||
updateFeature(featureId, updates);
|
||||
updateFeature(featureId, finalUpdates);
|
||||
});
|
||||
toast.success(`Updated ${result.updatedCount} features`);
|
||||
exitSelectionMode();
|
||||
@@ -517,7 +589,16 @@ export function BoardView() {
|
||||
toast.error('Failed to update features');
|
||||
}
|
||||
},
|
||||
[currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]
|
||||
[
|
||||
currentProject,
|
||||
selectedFeatureIds,
|
||||
updateFeature,
|
||||
exitSelectionMode,
|
||||
currentWorktreeBranch,
|
||||
getPrimaryWorktreeBranch,
|
||||
addAndSelectWorktree,
|
||||
setWorktreeRefreshKey,
|
||||
]
|
||||
);
|
||||
|
||||
// Handler for bulk deleting multiple features
|
||||
@@ -1325,6 +1406,9 @@ export function BoardView() {
|
||||
onClose={() => setShowMassEditDialog(false)}
|
||||
selectedFeatures={selectedFeatures}
|
||||
onApply={handleBulkUpdate}
|
||||
branchSuggestions={branchSuggestions}
|
||||
branchCardCounts={branchCardCounts}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
/>
|
||||
|
||||
{/* Board Background Modal */}
|
||||
|
||||
@@ -138,7 +138,7 @@ export function BoardHeader({
|
||||
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
||||
Worktrees
|
||||
Worktree Bar
|
||||
</Label>
|
||||
<Switch
|
||||
id="worktrees-toggle"
|
||||
|
||||
@@ -117,73 +117,90 @@ export function CardActions({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === 'in_progress' && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask &&
|
||||
(feature.status === 'in_progress' ||
|
||||
(typeof feature.status === 'string' && feature.status.startsWith('pipeline_'))) && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === 'verified' && (
|
||||
<>
|
||||
{/* Logs button */}
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { Play, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { Play, Cpu, FolderKanban, Settings2 } from 'lucide-react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
PlanningMode,
|
||||
Feature,
|
||||
} from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry } from '@automaker/types';
|
||||
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
|
||||
import { supportsReasoningEffort, isClaudeModel } from '@automaker/types';
|
||||
import {
|
||||
TestingTabContent,
|
||||
@@ -122,7 +123,7 @@ interface AddFeatureDialogProps {
|
||||
selectedNonMainWorktreeBranch?: string;
|
||||
/**
|
||||
* When true, forces the dialog to default to 'current' work mode (work on current branch).
|
||||
* This is used when the "Use selected worktree branch" setting is disabled.
|
||||
* This is used when the "Default to worktree mode" setting is disabled.
|
||||
*/
|
||||
forceCurrentBranchMode?: boolean;
|
||||
}
|
||||
@@ -152,6 +153,7 @@ export function AddFeatureDialog({
|
||||
forceCurrentBranchMode,
|
||||
}: AddFeatureDialogProps) {
|
||||
const isSpawnMode = !!parentFeature;
|
||||
const navigate = useNavigate();
|
||||
const [workMode, setWorkMode] = useState<WorkMode>('current');
|
||||
|
||||
// Form state
|
||||
@@ -187,7 +189,8 @@ export function AddFeatureDialog({
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore();
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
||||
useAppStore();
|
||||
|
||||
// Track previous open state to detect when dialog opens
|
||||
const wasOpenRef = useRef(false);
|
||||
@@ -207,7 +210,7 @@ export function AddFeatureDialog({
|
||||
);
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setModelEntry({ model: 'opus' });
|
||||
setModelEntry(defaultFeatureModel);
|
||||
|
||||
// Initialize description history (empty for new feature)
|
||||
setDescriptionHistory([]);
|
||||
@@ -228,6 +231,7 @@ export function AddFeatureDialog({
|
||||
defaultBranch,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultFeatureModel,
|
||||
useWorktrees,
|
||||
selectedNonMainWorktreeBranch,
|
||||
forceCurrentBranchMode,
|
||||
@@ -318,7 +322,7 @@ export function AddFeatureDialog({
|
||||
// When a non-main worktree is selected, use its branch name for custom mode
|
||||
setBranchName(selectedNonMainWorktreeBranch || '');
|
||||
setPriority(2);
|
||||
setModelEntry({ model: 'opus' });
|
||||
setModelEntry(defaultFeatureModel);
|
||||
setWorkMode(
|
||||
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
||||
);
|
||||
@@ -473,9 +477,31 @@ export function AddFeatureDialog({
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
<div className={cardClass}>
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
<span>Edit Defaults</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Change default model and planning settings for new features</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -419,7 +419,7 @@ export function BacklogPlanDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">{renderContent()}</div>
|
||||
<div className="py-4 overflow-y-auto">{renderContent()}</div>
|
||||
|
||||
<DialogFooter>
|
||||
{mode === 'input' && (
|
||||
|
||||
@@ -117,7 +117,7 @@ export function CreatePRDialog({
|
||||
description: `PR already exists for ${result.result.branch}`,
|
||||
action: {
|
||||
label: 'View PR',
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank'),
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -125,7 +125,7 @@ export function CreatePRDialog({
|
||||
description: `PR created from ${result.result.branch}`,
|
||||
action: {
|
||||
label: 'View PR',
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank'),
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -251,7 +251,10 @@ export function CreatePRDialog({
|
||||
<p className="text-sm text-muted-foreground mt-1">Your PR is ready for review</p>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button onClick={() => window.open(prUrl, '_blank')} className="gap-2">
|
||||
<Button
|
||||
onClick={() => window.open(prUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Pull Request
|
||||
</Button>
|
||||
@@ -277,7 +280,7 @@ export function CreatePRDialog({
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (browserUrl) {
|
||||
window.open(browserUrl, '_blank');
|
||||
window.open(browserUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
className="gap-2 w-full"
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { GitBranch, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { cn, modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
|
||||
@@ -86,6 +87,7 @@ export function EditFeatureDialog({
|
||||
isMaximized,
|
||||
allFeatures,
|
||||
}: EditFeatureDialogProps) {
|
||||
const navigate = useNavigate();
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
// Derive initial workMode from feature's branchName
|
||||
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||
@@ -363,9 +365,31 @@ export function EditFeatureDialog({
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
<div className={cardClass}>
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
<span>Edit Defaults</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Change default model and planning settings for new features</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -23,7 +24,10 @@ interface MassEditDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedFeatures: Feature[];
|
||||
onApply: (updates: Partial<Feature>) => Promise<void>;
|
||||
onApply: (updates: Partial<Feature>, workMode: WorkMode) => Promise<void>;
|
||||
branchSuggestions: string[];
|
||||
branchCardCounts?: Record<string, number>;
|
||||
currentBranch?: string;
|
||||
}
|
||||
|
||||
interface ApplyState {
|
||||
@@ -33,6 +37,7 @@ interface ApplyState {
|
||||
requirePlanApproval: boolean;
|
||||
priority: boolean;
|
||||
skipTests: boolean;
|
||||
branchName: boolean;
|
||||
}
|
||||
|
||||
function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
@@ -47,6 +52,7 @@ function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
),
|
||||
priority: !features.every((f) => f.priority === first.priority),
|
||||
skipTests: !features.every((f) => f.skipTests === first.skipTests),
|
||||
branchName: !features.every((f) => f.branchName === first.branchName),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,7 +103,15 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi
|
||||
);
|
||||
}
|
||||
|
||||
export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) {
|
||||
export function MassEditDialog({
|
||||
open,
|
||||
onClose,
|
||||
selectedFeatures,
|
||||
onApply,
|
||||
branchSuggestions,
|
||||
branchCardCounts,
|
||||
currentBranch,
|
||||
}: MassEditDialogProps) {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
// Track which fields to apply
|
||||
@@ -108,6 +122,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
requirePlanApproval: false,
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
});
|
||||
|
||||
// Field values
|
||||
@@ -118,6 +133,18 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
const [priority, setPriority] = useState(2);
|
||||
const [skipTests, setSkipTests] = useState(false);
|
||||
|
||||
// Work mode and branch name state
|
||||
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||
// Derive initial work mode from first selected feature's branchName
|
||||
if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) {
|
||||
return 'custom';
|
||||
}
|
||||
return 'current';
|
||||
});
|
||||
const [branchName, setBranchName] = useState(() => {
|
||||
return getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
});
|
||||
|
||||
// Calculate mixed values
|
||||
const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]);
|
||||
|
||||
@@ -131,6 +158,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
requirePlanApproval: false,
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
});
|
||||
setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias);
|
||||
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
|
||||
@@ -138,6 +166,10 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false));
|
||||
setPriority(getInitialValue(selectedFeatures, 'priority', 2));
|
||||
setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false));
|
||||
// Reset work mode and branch name
|
||||
const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
setBranchName(initialBranchName);
|
||||
setWorkMode(initialBranchName ? 'custom' : 'current');
|
||||
}
|
||||
}, [open, selectedFeatures]);
|
||||
|
||||
@@ -150,6 +182,12 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval;
|
||||
if (applyState.priority) updates.priority = priority;
|
||||
if (applyState.skipTests) updates.skipTests = skipTests;
|
||||
if (applyState.branchName) {
|
||||
// For 'current' mode, use empty string (work on current branch)
|
||||
// For 'auto' mode, use empty string (will be auto-generated)
|
||||
// For 'custom' mode, use the specified branch name
|
||||
updates.branchName = workMode === 'custom' ? branchName : '';
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
onClose();
|
||||
@@ -158,7 +196,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
await onApply(updates);
|
||||
await onApply(updates, workMode);
|
||||
onClose();
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
@@ -293,6 +331,25 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
testIdPrefix="mass-edit"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
|
||||
{/* Branch / Work Mode */}
|
||||
<FieldWrapper
|
||||
label="Branch / Work Mode"
|
||||
isMixed={mixedValues.branchName}
|
||||
willApply={applyState.branchName}
|
||||
onApplyChange={(apply) => setApplyState((prev) => ({ ...prev, branchName: apply }))}
|
||||
>
|
||||
<WorkModeSelector
|
||||
workMode={workMode}
|
||||
onWorkModeChange={setWorkMode}
|
||||
branchName={branchName}
|
||||
onBranchNameChange={setBranchName}
|
||||
branchSuggestions={branchSuggestions}
|
||||
branchCardCounts={branchCardCounts}
|
||||
currentBranch={currentBranch}
|
||||
testIdPrefix="mass-edit-work-mode"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function PlanSettingsDialog({
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Use selected worktree branch
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="plan-worktree-branch-toggle"
|
||||
@@ -55,8 +55,8 @@ export function PlanSettingsDialog({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
When enabled, features created via the Plan dialog will be assigned to the currently
|
||||
selected worktree branch. When disabled, features will be added to the main branch.
|
||||
Planned features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function WorktreeSettingsDialog({
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Use selected worktree branch
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="worktree-branch-toggle"
|
||||
@@ -55,8 +55,8 @@ export function WorktreeSettingsDialog({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
When enabled, the Add Feature dialog will default to custom branch mode with the
|
||||
currently selected worktree branch pre-filled.
|
||||
New features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,10 @@ export function useBoardEffects({
|
||||
const checkAllContexts = async () => {
|
||||
const featuresWithPotentialContext = features.filter(
|
||||
(f) =>
|
||||
f.status === 'in_progress' || f.status === 'waiting_approval' || f.status === 'verified'
|
||||
f.status === 'in_progress' ||
|
||||
f.status === 'waiting_approval' ||
|
||||
f.status === 'verified' ||
|
||||
(typeof f.status === 'string' && f.status.startsWith('pipeline_'))
|
||||
);
|
||||
const contextChecks = await Promise.all(
|
||||
featuresWithPotentialContext.map(async (f) => ({
|
||||
|
||||
@@ -143,8 +143,12 @@ export function WorktreeActionsDropdown({
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
Dev Server Running (:{devServerInfo?.port})
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onOpenDevServerUrl(worktree)} className="text-xs">
|
||||
<Globe className="w-3.5 h-3.5 mr-2" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
className="text-xs"
|
||||
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 mr-2" aria-hidden="true" />
|
||||
Open in Browser
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -320,7 +324,7 @@ export function WorktreeActionsDropdown({
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(worktree.pr!.url, '_blank');
|
||||
window.open(worktree.pr!.url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
|
||||
@@ -298,20 +298,29 @@ export function WorktreeTab({
|
||||
)}
|
||||
|
||||
{isDevServerRunning && (
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 rounded-none border-r-0',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
||||
'text-green-500'
|
||||
)}
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
title={`Open dev server (port ${devServerInfo?.port})`}
|
||||
>
|
||||
<Globe className="w-3 h-3" />
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 rounded-none border-r-0',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
||||
'text-green-500'
|
||||
)}
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
||||
>
|
||||
<Globe className="w-3 h-3" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open dev server (:{devServerInfo?.port})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<WorktreeActionsDropdown
|
||||
|
||||
@@ -118,8 +118,37 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
|
||||
const handleOpenDevServerUrl = useCallback(
|
||||
(worktree: WorktreeInfo) => {
|
||||
const serverInfo = runningDevServers.get(getWorktreeKey(worktree));
|
||||
if (serverInfo) {
|
||||
window.open(serverInfo.url, '_blank');
|
||||
if (!serverInfo) {
|
||||
logger.warn('No dev server info found for worktree:', getWorktreeKey(worktree));
|
||||
toast.error('Dev server not found', {
|
||||
description: 'The dev server may have stopped. Try starting it again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Rewrite URL hostname to match the current browser's hostname.
|
||||
// This ensures dev server URLs work when accessing Automaker from
|
||||
// remote machines (e.g., 192.168.x.x or hostname.local instead of localhost).
|
||||
const devServerUrl = new URL(serverInfo.url);
|
||||
|
||||
// Security: Only allow http/https protocols to prevent potential attacks
|
||||
// via data:, javascript:, file:, or other dangerous URL schemes
|
||||
if (devServerUrl.protocol !== 'http:' && devServerUrl.protocol !== 'https:') {
|
||||
logger.error('Invalid dev server URL protocol:', devServerUrl.protocol);
|
||||
toast.error('Invalid dev server URL', {
|
||||
description: 'The server returned an unsupported URL protocol.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
devServerUrl.hostname = window.location.hostname;
|
||||
window.open(devServerUrl.toString(), '_blank', 'noopener,noreferrer');
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse dev server URL:', error);
|
||||
toast.error('Failed to open dev server', {
|
||||
description: 'The server URL could not be processed. Please try again.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[runningDevServers, getWorktreeKey]
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
} from '@/lib/electron';
|
||||
import type { LinkedPRInfo, PhaseModelEntry, ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import type { LinkedPRInfo, PhaseModelEntry, ModelId } from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { isValidationStale } from '../utils';
|
||||
@@ -19,12 +19,10 @@ const logger = createLogger('IssueValidation');
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string (handles both formats)
|
||||
*/
|
||||
function extractModel(
|
||||
entry: PhaseModelEntry | string | undefined
|
||||
): ModelAlias | CursorModelId | undefined {
|
||||
function extractModel(entry: PhaseModelEntry | string | undefined): ModelId | undefined {
|
||||
if (!entry) return undefined;
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
return entry as ModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
@@ -228,8 +226,8 @@ export function useIssueValidation({
|
||||
issue: GitHubIssue,
|
||||
options: {
|
||||
forceRevalidate?: boolean;
|
||||
model?: string | PhaseModelEntry; // Accept either string (backward compat) or PhaseModelEntry
|
||||
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking level
|
||||
model?: ModelId | PhaseModelEntry; // Accept either model ID (backward compat) or PhaseModelEntry
|
||||
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking/reasoning
|
||||
comments?: GitHubComment[];
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
} = {}
|
||||
@@ -267,15 +265,16 @@ export function useIssueValidation({
|
||||
? modelEntry
|
||||
: model
|
||||
? typeof model === 'string'
|
||||
? { model: model as ModelAlias | CursorModelId }
|
||||
? { model: model as ModelId }
|
||||
: model
|
||||
: phaseModels.validationModel;
|
||||
const normalizedEntry =
|
||||
typeof effectiveModelEntry === 'string'
|
||||
? { model: effectiveModelEntry as ModelAlias | CursorModelId }
|
||||
? { model: effectiveModelEntry as ModelId }
|
||||
: effectiveModelEntry;
|
||||
const modelToUse = normalizedEntry.model;
|
||||
const thinkingLevelToUse = normalizedEntry.thinkingLevel;
|
||||
const reasoningEffortToUse = normalizedEntry.reasoningEffort;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -292,7 +291,8 @@ export function useIssueValidation({
|
||||
currentProject.path,
|
||||
validationInput,
|
||||
modelToUse,
|
||||
thinkingLevelToUse
|
||||
thinkingLevelToUse,
|
||||
reasoningEffortToUse
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GitHubIssue, StoredValidation, GitHubComment } from '@/lib/electron';
|
||||
import type { ModelAlias, CursorModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
|
||||
import type { ModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
|
||||
|
||||
export interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
@@ -37,7 +37,7 @@ export interface IssueDetailPanelProps {
|
||||
/** Model override state */
|
||||
modelOverride: {
|
||||
effectiveModelEntry: PhaseModelEntry;
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
effectiveModel: ModelId;
|
||||
isOverridden: boolean;
|
||||
setOverride: (entry: PhaseModelEntry | null) => void;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useSearch } from '@tanstack/react-router';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
|
||||
import { useSettingsView, type SettingsViewId } from './settings-view/hooks';
|
||||
import { NAV_ITEMS } from './settings-view/config/navigation';
|
||||
@@ -51,6 +51,8 @@ export function SettingsView() {
|
||||
setDefaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
setDefaultRequirePlanApproval,
|
||||
defaultFeatureModel,
|
||||
setDefaultFeatureModel,
|
||||
autoLoadClaudeMd,
|
||||
setAutoLoadClaudeMd,
|
||||
promptCustomization,
|
||||
@@ -88,8 +90,11 @@ export function SettingsView() {
|
||||
}
|
||||
};
|
||||
|
||||
// Get initial view from URL search params
|
||||
const { view: initialView } = useSearch({ from: '/settings' });
|
||||
|
||||
// Use settings view navigation hook
|
||||
const { activeView, navigateTo } = useSettingsView();
|
||||
const { activeView, navigateTo } = useSettingsView({ initialView });
|
||||
|
||||
// Handle navigation - if navigating to 'providers', default to 'claude-provider'
|
||||
const handleNavigate = (viewId: SettingsViewId) => {
|
||||
@@ -154,11 +159,13 @@ export function SettingsView() {
|
||||
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
||||
defaultPlanningMode={defaultPlanningMode}
|
||||
defaultRequirePlanApproval={defaultRequirePlanApproval}
|
||||
defaultFeatureModel={defaultFeatureModel}
|
||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||
onEnableDependencyBlockingChange={setEnableDependencyBlocking}
|
||||
onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode}
|
||||
onDefaultPlanningModeChange={setDefaultPlanningMode}
|
||||
onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval}
|
||||
onDefaultFeatureModelChange={setDefaultFeatureModel}
|
||||
/>
|
||||
);
|
||||
case 'worktrees':
|
||||
|
||||
@@ -37,8 +37,8 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
||||
{
|
||||
label: 'Model & Prompts',
|
||||
items: [
|
||||
{ id: 'model-defaults', label: 'Model Defaults', icon: Workflow },
|
||||
{ id: 'defaults', label: 'Feature Defaults', icon: FlaskConical },
|
||||
{ id: 'model-defaults', label: 'Model Defaults', icon: Workflow },
|
||||
{ id: 'worktrees', label: 'Worktrees', icon: GitBranch },
|
||||
{ id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText },
|
||||
{ id: 'api-keys', label: 'API Keys', icon: Key },
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ScrollText,
|
||||
ShieldCheck,
|
||||
FastForward,
|
||||
Cpu,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@@ -19,6 +20,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { PhaseModelEntry } from '@automaker/types';
|
||||
import { PhaseModelSelector } from '../model-defaults/phase-model-selector';
|
||||
|
||||
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
|
||||
@@ -28,11 +31,13 @@ interface FeatureDefaultsSectionProps {
|
||||
skipVerificationInAutoMode: boolean;
|
||||
defaultPlanningMode: PlanningMode;
|
||||
defaultRequirePlanApproval: boolean;
|
||||
defaultFeatureModel: PhaseModelEntry;
|
||||
onDefaultSkipTestsChange: (value: boolean) => void;
|
||||
onEnableDependencyBlockingChange: (value: boolean) => void;
|
||||
onSkipVerificationInAutoModeChange: (value: boolean) => void;
|
||||
onDefaultPlanningModeChange: (value: PlanningMode) => void;
|
||||
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
|
||||
onDefaultFeatureModelChange: (value: PhaseModelEntry) => void;
|
||||
}
|
||||
|
||||
export function FeatureDefaultsSection({
|
||||
@@ -41,11 +46,13 @@ export function FeatureDefaultsSection({
|
||||
skipVerificationInAutoMode,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultFeatureModel,
|
||||
onDefaultSkipTestsChange,
|
||||
onEnableDependencyBlockingChange,
|
||||
onSkipVerificationInAutoModeChange,
|
||||
onDefaultPlanningModeChange,
|
||||
onDefaultRequirePlanApprovalChange,
|
||||
onDefaultFeatureModelChange,
|
||||
}: FeatureDefaultsSectionProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -68,6 +75,30 @@ export function FeatureDefaultsSection({
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-5">
|
||||
{/* Default Feature Model Setting */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<div className="w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0 bg-brand-500/10">
|
||||
<Cpu className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">Default Model</Label>
|
||||
<PhaseModelSelector
|
||||
value={defaultFeatureModel}
|
||||
onChange={onDefaultFeatureModelChange}
|
||||
compact
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
The default AI model and thinking level used when creating new feature cards.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Planning Mode Default */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<div
|
||||
@@ -165,12 +196,11 @@ export function FeatureDefaultsSection({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-border/30" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{defaultPlanningMode === 'skip' && <div className="border-t border-border/30" />}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Automated Testing Setting */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
|
||||
@@ -48,7 +48,7 @@ export function AddEditServerDialog({
|
||||
Configure an MCP server to extend agent capabilities with custom tools.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 py-4 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-name">Name</Label>
|
||||
<Input
|
||||
|
||||
@@ -50,7 +50,7 @@ export function CreateSpecDialog({
|
||||
<DialogDescription className="text-muted-foreground">{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 py-4 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Project Overview</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -51,7 +51,7 @@ export function RegenerateSpecDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 py-4 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Describe your project</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -564,6 +564,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
||||
useWorktrees: settings.useWorktrees ?? true,
|
||||
defaultPlanningMode: settings.defaultPlanningMode ?? 'skip',
|
||||
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
||||
defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' },
|
||||
muteDoneSound: settings.muteDoneSound ?? false,
|
||||
enhancementModel: settings.enhancementModel ?? 'sonnet',
|
||||
validationModel: settings.validationModel ?? 'opus',
|
||||
|
||||
@@ -42,6 +42,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
||||
'useWorktrees',
|
||||
'defaultPlanningMode',
|
||||
'defaultRequirePlanApproval',
|
||||
'defaultFeatureModel',
|
||||
'muteDoneSound',
|
||||
'enhancementModel',
|
||||
'validationModel',
|
||||
@@ -466,6 +467,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
useWorktrees: serverSettings.useWorktrees,
|
||||
defaultPlanningMode: serverSettings.defaultPlanningMode,
|
||||
defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval,
|
||||
defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' },
|
||||
muteDoneSound: serverSettings.muteDoneSound,
|
||||
enhancementModel: serverSettings.enhancementModel,
|
||||
validationModel: serverSettings.validationModel,
|
||||
|
||||
@@ -10,7 +10,9 @@ import type {
|
||||
IssueValidationResponse,
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
AgentModel,
|
||||
ModelId,
|
||||
ThinkingLevel,
|
||||
ReasoningEffort,
|
||||
GitHubComment,
|
||||
IssueCommentsResult,
|
||||
Idea,
|
||||
@@ -314,7 +316,9 @@ export interface GitHubAPI {
|
||||
validateIssue: (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput,
|
||||
model?: AgentModel
|
||||
model?: ModelId,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
reasoningEffort?: ReasoningEffort
|
||||
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
|
||||
/** Check validation status for an issue or all issues */
|
||||
getValidationStatus: (
|
||||
@@ -1294,6 +1298,7 @@ interface SetupAPI {
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
hasOpenaiKey: boolean;
|
||||
}>;
|
||||
deleteApiKey: (
|
||||
provider: string
|
||||
@@ -1377,6 +1382,7 @@ function createMockSetupAPI(): SetupAPI {
|
||||
success: true,
|
||||
hasAnthropicKey: false,
|
||||
hasGoogleKey: false,
|
||||
hasOpenaiKey: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3008,8 +3014,20 @@ function createMockGitHubAPI(): GitHubAPI {
|
||||
mergedPRs: [],
|
||||
};
|
||||
},
|
||||
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: AgentModel) => {
|
||||
console.log('[Mock] Starting async validation:', { projectPath, issue, model });
|
||||
validateIssue: async (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput,
|
||||
model?: ModelId,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
reasoningEffort?: ReasoningEffort
|
||||
) => {
|
||||
console.log('[Mock] Starting async validation:', {
|
||||
projectPath,
|
||||
issue,
|
||||
model,
|
||||
thinkingLevel,
|
||||
reasoningEffort,
|
||||
});
|
||||
|
||||
// Simulate async validation in background
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
import type { Message, SessionListItem } from '@/types/electron';
|
||||
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
|
||||
import type { ModelId, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
||||
import { getGlobalFileBrowser } from '@/contexts/file-browser-context';
|
||||
|
||||
const logger = createLogger('HttpClient');
|
||||
@@ -1173,6 +1174,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
hasOpenaiKey: boolean;
|
||||
}> => this.get('/api/setup/api-keys'),
|
||||
|
||||
getPlatform: (): Promise<{
|
||||
@@ -1838,9 +1840,17 @@ export class HttpApiClient implements ElectronAPI {
|
||||
validateIssue: (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput,
|
||||
model?: string,
|
||||
thinkingLevel?: string
|
||||
) => this.post('/api/github/validate-issue', { projectPath, ...issue, model, thinkingLevel }),
|
||||
model?: ModelId,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
reasoningEffort?: ReasoningEffort
|
||||
) =>
|
||||
this.post('/api/github/validate-issue', {
|
||||
projectPath,
|
||||
...issue,
|
||||
model,
|
||||
thinkingLevel,
|
||||
reasoningEffort,
|
||||
}),
|
||||
getValidationStatus: (projectPath: string, issueNumber?: number) =>
|
||||
this.post('/api/github/validation-status', { projectPath, issueNumber }),
|
||||
stopValidation: (projectPath: string, issueNumber: number) =>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { SettingsView } from '@/components/views/settings-view';
|
||||
import type { SettingsViewId } from '@/components/views/settings-view/hooks';
|
||||
|
||||
interface SettingsSearchParams {
|
||||
view?: SettingsViewId;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
component: SettingsView,
|
||||
validateSearch: (search: Record<string, unknown>): SettingsSearchParams => {
|
||||
return {
|
||||
view: search.view as SettingsViewId | undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -657,6 +657,7 @@ export interface AppState {
|
||||
|
||||
defaultPlanningMode: PlanningMode;
|
||||
defaultRequirePlanApproval: boolean;
|
||||
defaultFeatureModel: PhaseModelEntry;
|
||||
|
||||
// Plan Approval State
|
||||
// When a plan requires user approval, this holds the pending approval details
|
||||
@@ -689,6 +690,7 @@ export interface AppState {
|
||||
codexModelsLoading: boolean;
|
||||
codexModelsError: string | null;
|
||||
codexModelsLastFetched: number | null;
|
||||
codexModelsLastFailedAt: number | null;
|
||||
|
||||
// Pipeline Configuration (per-project, keyed by project path)
|
||||
pipelineConfigByProject: Record<string, PipelineConfig>;
|
||||
@@ -1106,6 +1108,7 @@ export interface AppActions {
|
||||
|
||||
setDefaultPlanningMode: (mode: PlanningMode) => void;
|
||||
setDefaultRequirePlanApproval: (require: boolean) => void;
|
||||
setDefaultFeatureModel: (entry: PhaseModelEntry) => void;
|
||||
|
||||
// Plan Approval actions
|
||||
setPendingPlanApproval: (
|
||||
@@ -1279,6 +1282,7 @@ const initialState: AppState = {
|
||||
specCreatingForProject: null,
|
||||
defaultPlanningMode: 'skip' as PlanningMode,
|
||||
defaultRequirePlanApproval: false,
|
||||
defaultFeatureModel: { model: 'opus' } as PhaseModelEntry,
|
||||
pendingPlanApproval: null,
|
||||
claudeRefreshInterval: 60,
|
||||
claudeUsage: null,
|
||||
@@ -1289,6 +1293,7 @@ const initialState: AppState = {
|
||||
codexModelsLoading: false,
|
||||
codexModelsError: null,
|
||||
codexModelsLastFetched: null,
|
||||
codexModelsLastFailedAt: null,
|
||||
pipelineConfigByProject: {},
|
||||
worktreePanelVisibleByProject: {},
|
||||
showInitScriptIndicatorByProject: {},
|
||||
@@ -3145,6 +3150,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
|
||||
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }),
|
||||
setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }),
|
||||
setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }),
|
||||
|
||||
// Plan Approval actions
|
||||
setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }),
|
||||
@@ -3167,13 +3173,29 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
|
||||
// Codex Models actions
|
||||
fetchCodexModels: async (forceRefresh = false) => {
|
||||
const { codexModelsLastFetched, codexModelsLoading } = get();
|
||||
const FAILURE_COOLDOWN_MS = 30 * 1000; // 30 seconds
|
||||
const SUCCESS_CACHE_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
const { codexModelsLastFetched, codexModelsLoading, codexModelsLastFailedAt } = get();
|
||||
|
||||
// Skip if already loading
|
||||
if (codexModelsLoading) return;
|
||||
|
||||
// Skip if recently fetched (< 5 minutes ago) and not forcing refresh
|
||||
if (!forceRefresh && codexModelsLastFetched && Date.now() - codexModelsLastFetched < 300000) {
|
||||
// Skip if recently failed and not forcing refresh
|
||||
if (
|
||||
!forceRefresh &&
|
||||
codexModelsLastFailedAt &&
|
||||
Date.now() - codexModelsLastFailedAt < FAILURE_COOLDOWN_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if recently fetched successfully and not forcing refresh
|
||||
if (
|
||||
!forceRefresh &&
|
||||
codexModelsLastFetched &&
|
||||
Date.now() - codexModelsLastFetched < SUCCESS_CACHE_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3196,12 +3218,14 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
codexModelsLastFetched: Date.now(),
|
||||
codexModelsLoading: false,
|
||||
codexModelsError: null,
|
||||
codexModelsLastFailedAt: null, // Clear failure on success
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
set({
|
||||
codexModelsError: errorMessage,
|
||||
codexModelsLoading: false,
|
||||
codexModelsLastFailedAt: Date.now(), // Record failure time for cooldown
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user