Merge remote-tracking branch 'origin/v0.15.0rc' into feature/bug-startup-warning-ignores-claude-oauth-credenti-fuzx

This commit is contained in:
Shirone
2026-02-15 17:37:17 +01:00
73 changed files with 6064 additions and 647 deletions

View File

@@ -405,9 +405,28 @@ export function Sidebar() {
</div>
{/* Scroll indicator - shows there's more content below */}
{canScrollDown && sidebarOpen && (
<div className="flex justify-center py-1 border-t border-border/30">
<ChevronDown className="w-4 h-4 text-muted-foreground/50 animate-bounce" />
{canScrollDown && (
<div
className={cn(
'relative flex justify-center py-2 border-t border-border/30',
'bg-gradient-to-t from-background via-background/95 to-transparent',
'-mt-8 pt-8',
'pointer-events-none'
)}
>
<div className="pointer-events-auto flex flex-col items-center gap-0.5">
<ChevronDown
className={cn(
'w-4 h-4 text-brand-500/70 animate-bounce',
sidebarOpen ? 'block' : 'w-3 h-3'
)}
/>
{sidebarOpen && (
<span className="text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wide">
Scroll
</span>
)}
</div>
</div>
)}

View File

@@ -437,6 +437,63 @@ export function BoardView() {
// Auto mode hook - pass current worktree to get worktree-specific state
// Must be after selectedWorktree is defined
const autoMode = useAutoMode(selectedWorktree);
const refreshBoardState = useCallback(async () => {
if (!currentProject) return;
const projectPath = currentProject.path;
const beforeFeatures = (
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
)?.length;
const beforeWorktrees = (
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
| { worktrees?: unknown[] }
| undefined
)?.worktrees?.length;
const beforeRunningAgents = (
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
)?.count;
const beforeAutoModeRunning = autoMode.isRunning;
try {
await Promise.all([
queryClient.refetchQueries({ queryKey: queryKeys.features.all(projectPath) }),
queryClient.refetchQueries({ queryKey: queryKeys.runningAgents.all() }),
queryClient.refetchQueries({ queryKey: queryKeys.worktrees.all(projectPath) }),
autoMode.refreshStatus(),
]);
const afterFeatures = (
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
)?.length;
const afterWorktrees = (
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
| { worktrees?: unknown[] }
| undefined
)?.worktrees?.length;
const afterRunningAgents = (
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
)?.count;
const afterAutoModeRunning = autoMode.isRunning;
if (
beforeFeatures !== afterFeatures ||
beforeWorktrees !== afterWorktrees ||
beforeRunningAgents !== afterRunningAgents ||
beforeAutoModeRunning !== afterAutoModeRunning
) {
logger.info('[Board] Refresh detected state mismatch', {
features: { before: beforeFeatures, after: afterFeatures },
worktrees: { before: beforeWorktrees, after: afterWorktrees },
runningAgents: { before: beforeRunningAgents, after: afterRunningAgents },
autoModeRunning: { before: beforeAutoModeRunning, after: afterAutoModeRunning },
});
}
} catch (error) {
logger.error('[Board] Failed to refresh board state:', error);
toast.error('Failed to refresh board state');
}
}, [autoMode, currentProject, queryClient]);
// Get runningTasks from the hook (scoped to current project/worktree)
const runningAutoTasks = autoMode.runningTasks;
// Get worktree-specific maxConcurrency from the hook
@@ -1321,6 +1378,7 @@ export function BoardView() {
isCreatingSpec={isCreatingSpec}
creatingSpecProjectPath={creatingSpecProjectPath}
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
onRefreshBoard={refreshBoardState}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>

View File

@@ -1,7 +1,9 @@
import { useCallback, useState } from 'react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Wand2, GitBranch, ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Wand2, GitBranch, ClipboardCheck, RefreshCw } from 'lucide-react';
import { UsagePopover } from '@/components/usage-popover';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
@@ -35,6 +37,7 @@ interface BoardHeaderProps {
creatingSpecProjectPath?: string;
// Board controls props
onShowBoardBackground: () => void;
onRefreshBoard: () => Promise<void>;
// View toggle props
viewMode: ViewMode;
onViewModeChange: (mode: ViewMode) => void;
@@ -60,6 +63,7 @@ export function BoardHeader({
isCreatingSpec,
creatingSpecProjectPath,
onShowBoardBackground,
onRefreshBoard,
viewMode,
onViewModeChange,
}: BoardHeaderProps) {
@@ -110,9 +114,20 @@ export function BoardHeader({
// State for mobile actions panel
const [showActionsPanel, setShowActionsPanel] = useState(false);
const [isRefreshingBoard, setIsRefreshingBoard] = useState(false);
const isTablet = useIsTablet();
const handleRefreshBoard = useCallback(async () => {
if (isRefreshingBoard) return;
setIsRefreshingBoard(true);
try {
await onRefreshBoard();
} finally {
setIsRefreshingBoard(false);
}
}, [isRefreshingBoard, onRefreshBoard]);
return (
<div className="flex items-center justify-between gap-5 p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-4">
@@ -127,6 +142,22 @@ export function BoardHeader({
<BoardControls isMounted={isMounted} onShowBoardBackground={onShowBoardBackground} />
</div>
<div className="flex gap-4 items-center">
{isMounted && !isTablet && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
size="icon-sm"
onClick={handleRefreshBoard}
disabled={isRefreshingBoard}
aria-label="Refresh board state from server"
>
<RefreshCw className={isRefreshingBoard ? 'w-4 h-4 animate-spin' : 'w-4 h-4'} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Refresh board state from server</TooltipContent>
</Tooltip>
)}
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
{isMounted && !isTablet && (showClaudeUsage || showCodexUsage) && <UsagePopover />}

View File

@@ -28,7 +28,7 @@ import { cn } from '@/lib/utils';
import { modelSupportsThinking } from '@/lib/utils';
import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
import { supportsReasoningEffort } from '@automaker/types';
import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types';
import {
PrioritySelector,
WorkModeSelector,
@@ -264,7 +264,20 @@ export function AddFeatureDialog({
}, [planningMode]);
const handleModelChange = (entry: PhaseModelEntry) => {
setModelEntry(entry);
// Normalize thinking level when switching between adaptive and non-adaptive models
const isNewModelAdaptive =
typeof entry.model === 'string' && isAdaptiveThinkingModel(entry.model);
const currentLevel = entry.thinkingLevel || 'none';
if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') {
// Switching TO Opus 4.6 with a manual level -> auto-switch to 'adaptive'
setModelEntry({ ...entry, thinkingLevel: 'adaptive' });
} else if (!isNewModelAdaptive && currentLevel === 'adaptive') {
// Switching FROM Opus 4.6 with adaptive -> auto-switch to 'high'
setModelEntry({ ...entry, thinkingLevel: 'high' });
} else {
setModelEntry(entry);
}
};
const buildFeatureData = (): FeatureData | null => {

View File

@@ -241,9 +241,9 @@ export function CreatePRDialog({
<GitPullRequest className="w-5 h-5" />
Create Pull Request
</DialogTitle>
<DialogDescription>
<DialogDescription className="break-words">
Push changes and create a pull request from{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>
<code className="font-mono bg-muted px-1 rounded break-all">{worktree.branch}</code>
</DialogDescription>
</DialogHeader>

View File

@@ -167,7 +167,14 @@ export const ALL_MODELS: ModelOption[] = [
...COPILOT_MODELS,
];
export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink'];
export const THINKING_LEVELS: ThinkingLevel[] = [
'none',
'low',
'medium',
'high',
'ultrathink',
'adaptive',
];
export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
none: 'None',
@@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
medium: 'Med',
high: 'High',
ultrathink: 'Ultra',
adaptive: 'Adaptive',
};
/**

View File

@@ -2,19 +2,25 @@ import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ThinkingLevel } from '@/store/app-store';
import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from './model-constants';
import { THINKING_LEVEL_LABELS } from './model-constants';
import { getThinkingLevelsForModel } from '@automaker/types';
interface ThinkingLevelSelectorProps {
selectedLevel: ThinkingLevel;
onLevelSelect: (level: ThinkingLevel) => void;
testIdPrefix?: string;
/** Optional model ID to filter available thinking levels (e.g., Opus 4.6 only shows None/Adaptive) */
model?: string;
}
export function ThinkingLevelSelector({
selectedLevel,
onLevelSelect,
testIdPrefix = 'thinking-level',
model,
}: ThinkingLevelSelectorProps) {
const levels = getThinkingLevelsForModel(model || '');
return (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
@@ -22,7 +28,7 @@ export function ThinkingLevelSelector({
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{THINKING_LEVELS.map((level) => (
{levels.map((level) => (
<button
key={level}
type="button"
@@ -40,7 +46,9 @@ export function ThinkingLevelSelector({
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems.
{levels.includes('adaptive')
? 'Adaptive thinking lets the model decide how much reasoning to use.'
: 'Higher levels give more time to reason through complex problems.'}
</p>
</div>
);

View File

@@ -21,6 +21,7 @@ import {
isGroupSelected,
getSelectedVariant,
codexModelHasThinking,
getThinkingLevelsForModel,
} from '@automaker/types';
import {
CLAUDE_MODELS,
@@ -28,7 +29,6 @@ import {
OPENCODE_MODELS,
GEMINI_MODELS,
COPILOT_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS,
@@ -1296,7 +1296,9 @@ export function PhaseModelSelector({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
<button
key={level}
onClick={() => {
@@ -1322,6 +1324,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
<button
key={level}
onClick={() => {
@@ -1428,6 +1433,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1564,7 +1570,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(model.id).map((level) => (
<button
key={level}
onClick={() => {
@@ -1589,6 +1595,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1685,7 +1692,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(model.id).map((level) => (
<button
key={level}
onClick={() => {
@@ -1710,6 +1717,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (

View File

@@ -9,7 +9,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import type { CodexModelId } from '@automaker/types';
import { supportsReasoningEffort, type CodexModelId } from '@automaker/types';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CodexModelConfigurationProps {
@@ -27,25 +27,30 @@ interface CodexModelInfo {
}
const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
'codex-gpt-5.3-codex': {
id: 'codex-gpt-5.3-codex',
label: 'GPT-5.3-Codex',
description: 'Latest frontier agentic coding model',
},
'codex-gpt-5.2-codex': {
id: 'codex-gpt-5.2-codex',
label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering',
description: 'Frontier agentic coding model',
},
'codex-gpt-5.1-codex-max': {
id: 'codex-gpt-5.1-codex-max',
label: 'GPT-5.1-Codex-Max',
description: 'Optimized for long-horizon, agentic coding tasks in Codex',
description: 'Codex-optimized flagship for deep and fast reasoning',
},
'codex-gpt-5.1-codex-mini': {
id: 'codex-gpt-5.1-codex-mini',
label: 'GPT-5.1-Codex-Mini',
description: 'Smaller, more cost-effective version for faster workflows',
description: 'Optimized for codex. Cheaper, faster, but less capable',
},
'codex-gpt-5.2': {
id: 'codex-gpt-5.2',
label: 'GPT-5.2',
description: 'Best general agentic model for tasks across industries and domains',
description: 'Latest frontier model with improvements across knowledge, reasoning and coding',
},
'codex-gpt-5.1': {
id: 'codex-gpt-5.1',
@@ -157,13 +162,3 @@ export function CodexModelConfiguration({
</div>
);
}
function supportsReasoningEffort(modelId: string): boolean {
const reasoningModels = [
'codex-gpt-5.2-codex',
'codex-gpt-5.1-codex-max',
'codex-gpt-5.2',
'codex-gpt-5.1',
];
return reasoningModels.includes(modelId);
}

View File

@@ -0,0 +1,283 @@
/**
* Prompt Preview - Shows a live preview of the custom terminal prompt
*/
import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import type { ThemeMode } from '@automaker/types';
import { getTerminalTheme } from '@/config/terminal-themes';
interface PromptPreviewProps {
format: 'standard' | 'minimal' | 'powerline' | 'starship';
theme: ThemeMode;
showGitBranch: boolean;
showGitStatus: boolean;
showUserHost: boolean;
showPath: boolean;
pathStyle: 'full' | 'short' | 'basename';
pathDepth: number;
showTime: boolean;
showExitStatus: boolean;
isOmpTheme?: boolean;
promptThemeLabel?: string;
className?: string;
}
export function PromptPreview({
format,
theme,
showGitBranch,
showGitStatus,
showUserHost,
showPath,
pathStyle,
pathDepth,
showTime,
showExitStatus,
isOmpTheme = false,
promptThemeLabel,
className,
}: PromptPreviewProps) {
const terminalTheme = getTerminalTheme(theme);
const formatPath = (inputPath: string) => {
let displayPath = inputPath;
let prefix = '';
if (displayPath.startsWith('~/')) {
prefix = '~/';
displayPath = displayPath.slice(2);
} else if (displayPath.startsWith('/')) {
prefix = '/';
displayPath = displayPath.slice(1);
}
const segments = displayPath.split('/').filter((segment) => segment.length > 0);
const depth = Math.max(0, pathDepth);
const trimmedSegments = depth > 0 ? segments.slice(-depth) : segments;
let formattedSegments = trimmedSegments;
if (pathStyle === 'basename' && trimmedSegments.length > 0) {
formattedSegments = [trimmedSegments[trimmedSegments.length - 1]];
} else if (pathStyle === 'short') {
formattedSegments = trimmedSegments.map((segment, index) => {
if (index < trimmedSegments.length - 1) {
return segment.slice(0, 1);
}
return segment;
});
}
const joined = formattedSegments.join('/');
if (prefix === '/' && joined.length === 0) {
return '/';
}
if (prefix === '~/' && joined.length === 0) {
return '~';
}
return `${prefix}${joined}`;
};
// Generate preview text based on format
const renderPrompt = () => {
if (isOmpTheme) {
return (
<div className="font-mono text-sm leading-relaxed space-y-2">
<div style={{ color: terminalTheme.magenta }}>
{promptThemeLabel ?? 'Oh My Posh theme'}
</div>
<div className="text-xs text-muted-foreground">
Rendered by the oh-my-posh CLI in the terminal.
</div>
<div className="text-xs text-muted-foreground">
Preview here stays generic to avoid misleading output.
</div>
</div>
);
}
const user = 'user';
const host = 'automaker';
const path = formatPath('~/projects/automaker');
const branch = showGitBranch ? 'main' : null;
const dirty = showGitStatus && showGitBranch ? '*' : '';
const time = showTime ? '[14:32]' : '';
const status = showExitStatus ? '✗ 1' : '';
const gitInfo = branch ? ` (${branch}${dirty})` : '';
switch (format) {
case 'minimal': {
return (
<div className="font-mono text-sm leading-relaxed">
{showTime && <span style={{ color: terminalTheme.magenta }}>{time} </span>}
{showUserHost && (
<span style={{ color: terminalTheme.cyan }}>
{user}
<span style={{ color: terminalTheme.foreground }}>@</span>
<span style={{ color: terminalTheme.blue }}>{host}</span>{' '}
</span>
)}
{showPath && <span style={{ color: terminalTheme.yellow }}>{path}</span>}
{gitInfo && <span style={{ color: terminalTheme.magenta }}>{gitInfo}</span>}
{showExitStatus && <span style={{ color: terminalTheme.red }}> {status}</span>}
<span style={{ color: terminalTheme.green }}> $</span>
<span className="ml-1 animate-pulse"></span>
</div>
);
}
case 'powerline': {
const powerlineSegments: ReactNode[] = [];
if (showUserHost) {
powerlineSegments.push(
<span key="user-host" style={{ color: terminalTheme.cyan }}>
[{user}
<span style={{ color: terminalTheme.foreground }}>@</span>
<span style={{ color: terminalTheme.blue }}>{host}</span>]
</span>
);
}
if (showPath) {
powerlineSegments.push(
<span key="path" style={{ color: terminalTheme.yellow }}>
[{path}]
</span>
);
}
const powerlineCore = powerlineSegments.flatMap((segment, index) =>
index === 0
? [segment]
: [
<span key={`sep-${index}`} style={{ color: terminalTheme.cyan }}>
</span>,
segment,
]
);
const powerlineExtras: ReactNode[] = [];
if (gitInfo) {
powerlineExtras.push(
<span key="git" style={{ color: terminalTheme.magenta }}>
{gitInfo}
</span>
);
}
if (showTime) {
powerlineExtras.push(
<span key="time" style={{ color: terminalTheme.magenta }}>
{time}
</span>
);
}
if (showExitStatus) {
powerlineExtras.push(
<span key="status" style={{ color: terminalTheme.red }}>
{status}
</span>
);
}
const powerlineLine: ReactNode[] = [...powerlineCore];
if (powerlineExtras.length > 0) {
if (powerlineLine.length > 0) {
powerlineLine.push(' ');
}
powerlineLine.push(...powerlineExtras);
}
return (
<div className="font-mono text-sm leading-relaxed space-y-1">
<div>
<span style={{ color: terminalTheme.cyan }}></span>
{powerlineLine}
</div>
<div>
<span style={{ color: terminalTheme.cyan }}></span>
<span style={{ color: terminalTheme.green }}>$</span>
<span className="ml-1 animate-pulse"></span>
</div>
</div>
);
}
case 'starship': {
return (
<div className="font-mono text-sm leading-relaxed space-y-1">
<div>
{showTime && <span style={{ color: terminalTheme.magenta }}>{time} </span>}
{showUserHost && (
<>
<span style={{ color: terminalTheme.cyan }}>{user}</span>
<span style={{ color: terminalTheme.foreground }}>@</span>
<span style={{ color: terminalTheme.blue }}>{host}</span>
</>
)}
{showPath && (
<>
<span style={{ color: terminalTheme.foreground }}> in </span>
<span style={{ color: terminalTheme.yellow }}>{path}</span>
</>
)}
{branch && (
<>
<span style={{ color: terminalTheme.foreground }}> on </span>
<span style={{ color: terminalTheme.magenta }}>
{branch}
{dirty}
</span>
</>
)}
{showExitStatus && <span style={{ color: terminalTheme.red }}> {status}</span>}
</div>
<div>
<span style={{ color: terminalTheme.green }}></span>
<span className="ml-1 animate-pulse"></span>
</div>
</div>
);
}
case 'standard':
default: {
return (
<div className="font-mono text-sm leading-relaxed">
{showTime && <span style={{ color: terminalTheme.magenta }}>{time} </span>}
{showUserHost && (
<>
<span style={{ color: terminalTheme.cyan }}>[{user}</span>
<span style={{ color: terminalTheme.foreground }}>@</span>
<span style={{ color: terminalTheme.blue }}>{host}</span>
<span style={{ color: terminalTheme.cyan }}>]</span>
</>
)}
{showPath && <span style={{ color: terminalTheme.yellow }}> {path}</span>}
{gitInfo && <span style={{ color: terminalTheme.magenta }}>{gitInfo}</span>}
{showExitStatus && <span style={{ color: terminalTheme.red }}> {status}</span>}
<span style={{ color: terminalTheme.green }}> $</span>
<span className="ml-1 animate-pulse"></span>
</div>
);
}
}
};
return (
<div
className={cn(
'rounded-lg border p-4',
'bg-[var(--terminal-bg)] text-[var(--terminal-fg)]',
'shadow-inner',
className
)}
style={
{
'--terminal-bg': terminalTheme.background,
'--terminal-fg': terminalTheme.foreground,
} as React.CSSProperties
}
>
<div className="mb-2 text-xs text-muted-foreground opacity-70">Preview</div>
{renderPrompt()}
</div>
);
}

View File

@@ -0,0 +1,253 @@
import type { TerminalPromptTheme } from '@automaker/types';
export const PROMPT_THEME_CUSTOM_ID: TerminalPromptTheme = 'custom';
export const OMP_THEME_NAMES = [
'1_shell',
'M365Princess',
'agnoster',
'agnoster.minimal',
'agnosterplus',
'aliens',
'amro',
'atomic',
'atomicBit',
'avit',
'blue-owl',
'blueish',
'bubbles',
'bubblesextra',
'bubblesline',
'capr4n',
'catppuccin',
'catppuccin_frappe',
'catppuccin_latte',
'catppuccin_macchiato',
'catppuccin_mocha',
'cert',
'chips',
'cinnamon',
'clean-detailed',
'cloud-context',
'cloud-native-azure',
'cobalt2',
'craver',
'darkblood',
'devious-diamonds',
'di4am0nd',
'dracula',
'easy-term',
'emodipt',
'emodipt-extend',
'fish',
'free-ukraine',
'froczh',
'gmay',
'glowsticks',
'grandpa-style',
'gruvbox',
'half-life',
'honukai',
'hotstick.minimal',
'hul10',
'hunk',
'huvix',
'if_tea',
'illusi0n',
'iterm2',
'jandedobbeleer',
'jblab_2021',
'jonnychipz',
'json',
'jtracey93',
'jv_sitecorian',
'kali',
'kushal',
'lambda',
'lambdageneration',
'larserikfinholt',
'lightgreen',
'marcduiker',
'markbull',
'material',
'microverse-power',
'mojada',
'montys',
'mt',
'multiverse-neon',
'negligible',
'neko',
'night-owl',
'nordtron',
'nu4a',
'onehalf.minimal',
'paradox',
'pararussel',
'patriksvensson',
'peru',
'pixelrobots',
'plague',
'poshmon',
'powerlevel10k_classic',
'powerlevel10k_lean',
'powerlevel10k_modern',
'powerlevel10k_rainbow',
'powerline',
'probua.minimal',
'pure',
'quick-term',
'remk',
'robbyrussell',
'rudolfs-dark',
'rudolfs-light',
'sim-web',
'slim',
'slimfat',
'smoothie',
'sonicboom_dark',
'sonicboom_light',
'sorin',
'space',
'spaceship',
'star',
'stelbent-compact.minimal',
'stelbent.minimal',
'takuya',
'the-unnamed',
'thecyberden',
'tiwahu',
'tokyo',
'tokyonight_storm',
'tonybaloney',
'uew',
'unicorn',
'velvet',
'wholespace',
'wopian',
'xtoys',
'ys',
'zash',
] as const;
type OmpThemeName = (typeof OMP_THEME_NAMES)[number];
type PromptFormat = 'standard' | 'minimal' | 'powerline' | 'starship';
type PathStyle = 'full' | 'short' | 'basename';
export interface PromptThemeConfig {
promptFormat: PromptFormat;
showGitBranch: boolean;
showGitStatus: boolean;
showUserHost: boolean;
showPath: boolean;
pathStyle: PathStyle;
pathDepth: number;
showTime: boolean;
showExitStatus: boolean;
}
export interface PromptThemePreset {
id: TerminalPromptTheme;
label: string;
description: string;
config: PromptThemeConfig;
}
const PATH_DEPTH_FULL = 0;
const PATH_DEPTH_TWO = 2;
const PATH_DEPTH_THREE = 3;
const POWERLINE_HINTS = ['powerline', 'powerlevel10k', 'agnoster', 'bubbles', 'smoothie'];
const MINIMAL_HINTS = ['minimal', 'pure', 'slim', 'negligible'];
const STARSHIP_HINTS = ['spaceship', 'star'];
const SHORT_PATH_HINTS = ['compact', 'lean', 'slim'];
const TIME_HINTS = ['time', 'clock'];
const EXIT_STATUS_HINTS = ['status', 'exit', 'fail', 'error'];
function toPromptThemeId(name: OmpThemeName): TerminalPromptTheme {
return `omp-${name}` as TerminalPromptTheme;
}
function formatLabel(name: string): string {
const cleaned = name.replace(/[._-]+/g, ' ').trim();
return cleaned
.split(' ')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
function buildPresetConfig(name: OmpThemeName): PromptThemeConfig {
const lower = name.toLowerCase();
const isPowerline = POWERLINE_HINTS.some((hint) => lower.includes(hint));
const isMinimal = MINIMAL_HINTS.some((hint) => lower.includes(hint));
const isStarship = STARSHIP_HINTS.some((hint) => lower.includes(hint));
let promptFormat: PromptFormat = 'standard';
if (isPowerline) {
promptFormat = 'powerline';
} else if (isMinimal) {
promptFormat = 'minimal';
} else if (isStarship) {
promptFormat = 'starship';
}
const showUserHost = !isMinimal;
const showPath = true;
const pathStyle: PathStyle = isMinimal ? 'short' : 'full';
let pathDepth = isMinimal ? PATH_DEPTH_THREE : PATH_DEPTH_FULL;
if (SHORT_PATH_HINTS.some((hint) => lower.includes(hint))) {
pathDepth = PATH_DEPTH_TWO;
}
if (lower.includes('powerlevel10k')) {
pathDepth = PATH_DEPTH_THREE;
}
const showTime = TIME_HINTS.some((hint) => lower.includes(hint));
const showExitStatus = EXIT_STATUS_HINTS.some((hint) => lower.includes(hint));
return {
promptFormat,
showGitBranch: true,
showGitStatus: true,
showUserHost,
showPath,
pathStyle,
pathDepth,
showTime,
showExitStatus,
};
}
export const PROMPT_THEME_PRESETS: PromptThemePreset[] = OMP_THEME_NAMES.map((name) => ({
id: toPromptThemeId(name),
label: `${formatLabel(name)} (OMP)`,
description: 'Oh My Posh theme preset',
config: buildPresetConfig(name),
}));
export function getPromptThemePreset(presetId: TerminalPromptTheme): PromptThemePreset | null {
return PROMPT_THEME_PRESETS.find((preset) => preset.id === presetId) ?? null;
}
export function getMatchingPromptThemeId(config: PromptThemeConfig): TerminalPromptTheme {
const match = PROMPT_THEME_PRESETS.find((preset) => {
const presetConfig = preset.config;
return (
presetConfig.promptFormat === config.promptFormat &&
presetConfig.showGitBranch === config.showGitBranch &&
presetConfig.showGitStatus === config.showGitStatus &&
presetConfig.showUserHost === config.showUserHost &&
presetConfig.showPath === config.showPath &&
presetConfig.pathStyle === config.pathStyle &&
presetConfig.pathDepth === config.pathDepth &&
presetConfig.showTime === config.showTime &&
presetConfig.showExitStatus === config.showExitStatus
);
});
return match?.id ?? PROMPT_THEME_CUSTOM_ID;
}

View File

@@ -0,0 +1,662 @@
/**
* Terminal Config Section - Custom terminal configurations with theme synchronization
*
* This component provides UI for enabling custom terminal prompts that automatically
* sync with Automaker's 40 themes. It's an opt-in feature that generates shell configs
* in .automaker/terminal/ without modifying user's existing RC files.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Wand2, GitBranch, Info, Plus, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { PromptPreview } from './prompt-preview';
import type { TerminalPromptTheme } from '@automaker/types';
import {
PROMPT_THEME_CUSTOM_ID,
PROMPT_THEME_PRESETS,
getMatchingPromptThemeId,
getPromptThemePreset,
type PromptThemeConfig,
} from './prompt-theme-presets';
import { useUpdateGlobalSettings } from '@/hooks/mutations/use-settings-mutations';
import { useGlobalSettings } from '@/hooks/queries/use-settings';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
export function TerminalConfigSection() {
const PATH_DEPTH_MIN = 0;
const PATH_DEPTH_MAX = 10;
const ENV_VAR_UPDATE_DEBOUNCE_MS = 400;
const ENV_VAR_ID_PREFIX = 'env';
const TERMINAL_RC_FILE_VERSION = 11;
const { theme } = useAppStore();
const { data: globalSettings } = useGlobalSettings();
const updateGlobalSettings = useUpdateGlobalSettings({ showSuccessToast: false });
const envVarIdRef = useRef(0);
const envVarUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const createEnvVarEntry = useCallback(
(key = '', value = '') => {
envVarIdRef.current += 1;
return {
id: `${ENV_VAR_ID_PREFIX}-${envVarIdRef.current}`,
key,
value,
};
},
[ENV_VAR_ID_PREFIX]
);
const [localEnvVars, setLocalEnvVars] = useState<
Array<{ id: string; key: string; value: string }>
>(() =>
Object.entries(globalSettings?.terminalConfig?.customEnvVars || {}).map(([key, value]) =>
createEnvVarEntry(key, value)
)
);
const [showEnableConfirm, setShowEnableConfirm] = useState(false);
const clampPathDepth = (value: number) =>
Math.min(PATH_DEPTH_MAX, Math.max(PATH_DEPTH_MIN, value));
const defaultTerminalConfig = {
enabled: false,
customPrompt: true,
promptFormat: 'standard' as const,
promptTheme: PROMPT_THEME_CUSTOM_ID,
showGitBranch: true,
showGitStatus: true,
showUserHost: true,
showPath: true,
pathStyle: 'full' as const,
pathDepth: PATH_DEPTH_MIN,
showTime: false,
showExitStatus: false,
customAliases: '',
customEnvVars: {},
};
const terminalConfig = {
...defaultTerminalConfig,
...globalSettings?.terminalConfig,
customAliases:
globalSettings?.terminalConfig?.customAliases ?? defaultTerminalConfig.customAliases,
customEnvVars:
globalSettings?.terminalConfig?.customEnvVars ?? defaultTerminalConfig.customEnvVars,
};
const promptThemeConfig: PromptThemeConfig = {
promptFormat: terminalConfig.promptFormat,
showGitBranch: terminalConfig.showGitBranch,
showGitStatus: terminalConfig.showGitStatus,
showUserHost: terminalConfig.showUserHost,
showPath: terminalConfig.showPath,
pathStyle: terminalConfig.pathStyle,
pathDepth: terminalConfig.pathDepth,
showTime: terminalConfig.showTime,
showExitStatus: terminalConfig.showExitStatus,
};
const storedPromptTheme = terminalConfig.promptTheme;
const activePromptThemeId =
storedPromptTheme === PROMPT_THEME_CUSTOM_ID
? PROMPT_THEME_CUSTOM_ID
: (storedPromptTheme ?? getMatchingPromptThemeId(promptThemeConfig));
const isOmpTheme =
storedPromptTheme !== undefined && storedPromptTheme !== PROMPT_THEME_CUSTOM_ID;
const promptThemePreset = isOmpTheme
? getPromptThemePreset(storedPromptTheme as TerminalPromptTheme)
: null;
const applyEnabledUpdate = (enabled: boolean) => {
// Ensure all required fields are present
const updatedConfig = {
enabled,
customPrompt: terminalConfig.customPrompt,
promptFormat: terminalConfig.promptFormat,
showGitBranch: terminalConfig.showGitBranch,
showGitStatus: terminalConfig.showGitStatus,
showUserHost: terminalConfig.showUserHost,
showPath: terminalConfig.showPath,
pathStyle: terminalConfig.pathStyle,
pathDepth: terminalConfig.pathDepth,
showTime: terminalConfig.showTime,
showExitStatus: terminalConfig.showExitStatus,
promptTheme: terminalConfig.promptTheme ?? PROMPT_THEME_CUSTOM_ID,
customAliases: terminalConfig.customAliases,
customEnvVars: terminalConfig.customEnvVars,
rcFileVersion: TERMINAL_RC_FILE_VERSION,
};
updateGlobalSettings.mutate(
{ terminalConfig: updatedConfig },
{
onSuccess: () => {
toast.success(
enabled ? 'Custom terminal configs enabled' : 'Custom terminal configs disabled',
{
description: enabled
? 'New terminals will use custom prompts'
: '.automaker/terminal/ will be cleaned up',
}
);
},
onError: (error) => {
console.error('[TerminalConfig] Failed to update settings:', error);
toast.error('Failed to update terminal config', {
description: error instanceof Error ? error.message : 'Unknown error',
});
},
}
);
};
useEffect(() => {
setLocalEnvVars(
Object.entries(globalSettings?.terminalConfig?.customEnvVars || {}).map(([key, value]) =>
createEnvVarEntry(key, value)
)
);
}, [createEnvVarEntry, globalSettings?.terminalConfig?.customEnvVars]);
useEffect(() => {
return () => {
if (envVarUpdateTimeoutRef.current) {
clearTimeout(envVarUpdateTimeoutRef.current);
}
};
}, []);
const handleToggleEnabled = async (enabled: boolean) => {
if (enabled) {
setShowEnableConfirm(true);
return;
}
applyEnabledUpdate(false);
};
const handleUpdateConfig = (updates: Partial<typeof terminalConfig>) => {
const nextPromptTheme = updates.promptTheme ?? PROMPT_THEME_CUSTOM_ID;
updateGlobalSettings.mutate(
{
terminalConfig: {
...terminalConfig,
...updates,
promptTheme: nextPromptTheme,
},
},
{
onError: (error) => {
console.error('[TerminalConfig] Failed to update settings:', error);
toast.error('Failed to update terminal config', {
description: error instanceof Error ? error.message : 'Unknown error',
});
},
}
);
};
const scheduleEnvVarsUpdate = (envVarsObject: Record<string, string>) => {
if (envVarUpdateTimeoutRef.current) {
clearTimeout(envVarUpdateTimeoutRef.current);
}
envVarUpdateTimeoutRef.current = setTimeout(() => {
handleUpdateConfig({ customEnvVars: envVarsObject });
}, ENV_VAR_UPDATE_DEBOUNCE_MS);
};
const handlePromptThemeChange = (themeId: string) => {
if (themeId === PROMPT_THEME_CUSTOM_ID) {
handleUpdateConfig({ promptTheme: PROMPT_THEME_CUSTOM_ID });
return;
}
const preset = getPromptThemePreset(themeId as TerminalPromptTheme);
if (!preset) {
handleUpdateConfig({ promptTheme: PROMPT_THEME_CUSTOM_ID });
return;
}
handleUpdateConfig({
...preset.config,
promptTheme: preset.id,
});
};
const addEnvVar = () => {
setLocalEnvVars([...localEnvVars, createEnvVarEntry()]);
};
const removeEnvVar = (id: string) => {
const newVars = localEnvVars.filter((envVar) => envVar.id !== id);
setLocalEnvVars(newVars);
// Update settings
const envVarsObject = newVars.reduce(
(acc, { key, value }) => {
if (key) acc[key] = value;
return acc;
},
{} as Record<string, string>
);
scheduleEnvVarsUpdate(envVarsObject);
};
const updateEnvVar = (id: string, field: 'key' | 'value', newValue: string) => {
const newVars = localEnvVars.map((envVar) =>
envVar.id === id ? { ...envVar, [field]: newValue } : envVar
);
setLocalEnvVars(newVars);
// Validate and update settings (only if key is valid)
const envVarsObject = newVars.reduce(
(acc, { key, value }) => {
// Only include vars with valid keys (alphanumeric + underscore)
if (key && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
scheduleEnvVarsUpdate(envVarsObject);
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-purple-500/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 flex items-center justify-center border border-purple-500/20">
<Wand2 className="w-5 h-5 text-purple-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Custom Terminal Configurations
</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Generate custom shell prompts that automatically sync with your app theme. Opt-in feature
that creates configs in .automaker/terminal/ without modifying your existing RC files.
</p>
</div>
<div className="p-6 space-y-6">
{/* Enable Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-foreground font-medium">Enable Custom Configurations</Label>
<p className="text-xs text-muted-foreground">
Create theme-synced shell configs in .automaker/terminal/
</p>
</div>
<Switch checked={terminalConfig.enabled} onCheckedChange={handleToggleEnabled} />
</div>
{terminalConfig.enabled && (
<>
{/* Info Box */}
<div className="rounded-lg border border-purple-500/20 bg-purple-500/5 p-3 flex gap-2">
<Info className="h-4 w-4 text-purple-500 flex-shrink-0 mt-0.5" />
<div className="text-xs text-foreground/80">
<strong>How it works:</strong> Custom configs are applied to new terminals only.
Your ~/.bashrc and ~/.zshrc are still loaded first. Close and reopen terminals to
see changes.
</div>
</div>
{/* Custom Prompt Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-foreground font-medium">Custom Prompt</Label>
<p className="text-xs text-muted-foreground">
Override default shell prompt with themed version
</p>
</div>
<Switch
checked={terminalConfig.customPrompt}
onCheckedChange={(checked) => handleUpdateConfig({ customPrompt: checked })}
/>
</div>
{terminalConfig.customPrompt && (
<>
{/* Prompt Format */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Prompt Theme (Oh My Posh)</Label>
<Select value={activePromptThemeId} onValueChange={handlePromptThemeChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={PROMPT_THEME_CUSTOM_ID}>
<div className="space-y-0.5">
<div>Custom</div>
<div className="text-xs text-muted-foreground">
Hand-tuned configuration
</div>
</div>
</SelectItem>
{PROMPT_THEME_PRESETS.map((preset) => (
<SelectItem key={preset.id} value={preset.id}>
<div className="space-y-0.5">
<div>{preset.label}</div>
<div className="text-xs text-muted-foreground">
{preset.description}
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{isOmpTheme && (
<div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-3 flex gap-2">
<Info className="h-4 w-4 text-emerald-500 flex-shrink-0 mt-0.5" />
<div className="text-xs text-foreground/80">
<strong>{promptThemePreset?.label ?? 'Oh My Posh theme'}</strong> uses the
oh-my-posh CLI for rendering. Ensure it&apos;s installed for the full theme.
Prompt format and segment toggles are ignored while an OMP theme is selected.
</div>
</div>
)}
<div className="space-y-3">
<Label className="text-foreground font-medium">Prompt Format</Label>
<Select
value={terminalConfig.promptFormat}
onValueChange={(value: 'standard' | 'minimal' | 'powerline' | 'starship') =>
handleUpdateConfig({ promptFormat: value })
}
disabled={isOmpTheme}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="standard">
<div className="space-y-0.5">
<div>Standard</div>
<div className="text-xs text-muted-foreground">
[user@host] ~/path (main*) $
</div>
</div>
</SelectItem>
<SelectItem value="minimal">
<div className="space-y-0.5">
<div>Minimal</div>
<div className="text-xs text-muted-foreground">~/path (main*) $</div>
</div>
</SelectItem>
<SelectItem value="powerline">
<div className="space-y-0.5">
<div>Powerline</div>
<div className="text-xs text-muted-foreground">
[user@host][~/path][main*]
</div>
</div>
</SelectItem>
<SelectItem value="starship">
<div className="space-y-0.5">
<div>Starship-Inspired</div>
<div className="text-xs text-muted-foreground">
user@host in ~/path on main*
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Git Info Toggles */}
<div className="space-y-4 pl-4 border-l-2 border-border/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GitBranch className="w-4 h-4 text-muted-foreground" />
<Label className="text-sm">Show Git Branch</Label>
</div>
<Switch
checked={terminalConfig.showGitBranch}
onCheckedChange={(checked) => handleUpdateConfig({ showGitBranch: checked })}
disabled={isOmpTheme}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">*</span>
<Label className="text-sm">Show Git Status (dirty indicator)</Label>
</div>
<Switch
checked={terminalConfig.showGitStatus}
onCheckedChange={(checked) => handleUpdateConfig({ showGitStatus: checked })}
disabled={!terminalConfig.showGitBranch || isOmpTheme}
/>
</div>
</div>
{/* Prompt Segments */}
<div className="space-y-4 pl-4 border-l-2 border-border/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Wand2 className="w-4 h-4 text-muted-foreground" />
<Label className="text-sm">Show User & Host</Label>
</div>
<Switch
checked={terminalConfig.showUserHost}
onCheckedChange={(checked) => handleUpdateConfig({ showUserHost: checked })}
disabled={isOmpTheme}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">~/</span>
<Label className="text-sm">Show Path</Label>
</div>
<Switch
checked={terminalConfig.showPath}
onCheckedChange={(checked) => handleUpdateConfig({ showPath: checked })}
disabled={isOmpTheme}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"></span>
<Label className="text-sm">Show Time</Label>
</div>
<Switch
checked={terminalConfig.showTime}
onCheckedChange={(checked) => handleUpdateConfig({ showTime: checked })}
disabled={isOmpTheme}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"></span>
<Label className="text-sm">Show Exit Status</Label>
</div>
<Switch
checked={terminalConfig.showExitStatus}
onCheckedChange={(checked) => handleUpdateConfig({ showExitStatus: checked })}
disabled={isOmpTheme}
/>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Path Style</Label>
<Select
value={terminalConfig.pathStyle}
onValueChange={(value: 'full' | 'short' | 'basename') =>
handleUpdateConfig({ pathStyle: value })
}
disabled={!terminalConfig.showPath || isOmpTheme}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">Full</SelectItem>
<SelectItem value="short">Short</SelectItem>
<SelectItem value="basename">Basename</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Path Depth</Label>
<Input
type="number"
min={PATH_DEPTH_MIN}
max={PATH_DEPTH_MAX}
value={terminalConfig.pathDepth}
onChange={(event) =>
handleUpdateConfig({
pathDepth: clampPathDepth(Number(event.target.value) || 0),
})
}
disabled={!terminalConfig.showPath || isOmpTheme}
/>
</div>
</div>
</div>
{/* Live Preview */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Preview</Label>
<PromptPreview
format={terminalConfig.promptFormat}
theme={theme}
showGitBranch={terminalConfig.showGitBranch}
showGitStatus={terminalConfig.showGitStatus}
showUserHost={terminalConfig.showUserHost}
showPath={terminalConfig.showPath}
pathStyle={terminalConfig.pathStyle}
pathDepth={terminalConfig.pathDepth}
showTime={terminalConfig.showTime}
showExitStatus={terminalConfig.showExitStatus}
isOmpTheme={isOmpTheme}
promptThemeLabel={promptThemePreset?.label}
/>
</div>
</>
)}
{/* Custom Aliases */}
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-foreground font-medium">Custom Aliases</Label>
<p className="text-xs text-muted-foreground">
Add shell aliases (one per line, e.g., alias ll='ls -la')
</p>
</div>
<Textarea
value={terminalConfig.customAliases}
onChange={(e) => handleUpdateConfig({ customAliases: e.target.value })}
placeholder="# Custom aliases&#10;alias gs='git status'&#10;alias ll='ls -la'&#10;alias ..='cd ..'"
className="font-mono text-sm h-32"
/>
</div>
{/* Custom Environment Variables */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-foreground font-medium">
Custom Environment Variables
</Label>
<p className="text-xs text-muted-foreground">
Add custom env vars (alphanumeric + underscore only)
</p>
</div>
<Button variant="outline" size="sm" onClick={addEnvVar} className="h-8 gap-1.5">
<Plus className="w-3.5 h-3.5" />
Add
</Button>
</div>
{localEnvVars.length > 0 && (
<div className="space-y-2">
{localEnvVars.map((envVar) => (
<div key={envVar.id} className="flex gap-2 items-start">
<Input
value={envVar.key}
onChange={(e) => updateEnvVar(envVar.id, 'key', e.target.value)}
placeholder="VAR_NAME"
className={cn(
'font-mono text-sm flex-1',
envVar.key &&
!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(envVar.key) &&
'border-destructive'
)}
/>
<Input
value={envVar.value}
onChange={(e) => updateEnvVar(envVar.id, 'value', e.target.value)}
placeholder="value"
className="font-mono text-sm flex-[2]"
/>
<Button
variant="ghost"
size="sm"
onClick={() => removeEnvVar(envVar.id)}
className="h-9 w-9 p-0 text-muted-foreground hover:text-destructive"
>
<X className="w-4 h-4" />
</Button>
</div>
))}
</div>
)}
</div>
</>
)}
</div>
<ConfirmDialog
open={showEnableConfirm}
onOpenChange={setShowEnableConfirm}
title="Enable custom terminal configurations"
description="Automaker will generate per-project shell configuration files for your terminal."
icon={Info}
confirmText="Enable"
onConfirm={() => applyEnabledUpdate(true)}
>
<div className="space-y-3 text-sm text-muted-foreground">
<ul className="list-disc space-y-1 pl-5">
<li>Creates shell config files in `.automaker/terminal/`</li>
<li>Applies prompts and colors that match your app theme</li>
<li>Leaves your existing `~/.bashrc` and `~/.zshrc` untouched</li>
</ul>
<p className="text-xs text-muted-foreground">
New terminal sessions will use the custom prompt; existing sessions are unchanged.
</p>
</div>
</ConfirmDialog>
</div>
);
}

View File

@@ -24,6 +24,7 @@ import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
import { useAvailableTerminals } from '@/components/views/board-view/worktree-panel/hooks/use-available-terminals';
import { getTerminalIcon } from '@/components/icons/terminal-icons';
import { TerminalConfigSection } from './terminal-config-section';
export function TerminalSection() {
const {
@@ -53,253 +54,258 @@ export function TerminalSection() {
const { terminals, isRefreshing, refresh } = useAvailableTerminals();
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/10 flex items-center justify-center border border-green-500/20">
<SquareTerminal className="w-5 h-5 text-green-500" />
<div className="space-y-6">
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/10 flex items-center justify-center border border-green-500/20">
<SquareTerminal className="w-5 h-5 text-green-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Terminal</h2>
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Terminal</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Customize terminal appearance and behavior. Theme follows your app theme in Appearance
settings.
</p>
</div>
<div className="p-6 space-y-6">
{/* Default External Terminal */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Default External Terminal</Label>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={refresh}
disabled={isRefreshing}
title="Refresh available terminals"
aria-label="Refresh available terminals"
>
<RefreshCw className={cn('w-3.5 h-3.5', isRefreshing && 'animate-spin')} />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Terminal to use when selecting "Open in Terminal" from the worktree menu
<p className="text-sm text-muted-foreground/80 ml-12">
Customize terminal appearance and behavior. Theme follows your app theme in Appearance
settings.
</p>
<Select
value={defaultTerminalId ?? 'integrated'}
onValueChange={(value) => {
setDefaultTerminalId(value === 'integrated' ? null : value);
toast.success(
value === 'integrated'
? 'Integrated terminal set as default'
: 'Default terminal changed'
);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a terminal" />
</SelectTrigger>
<SelectContent>
<SelectItem value="integrated">
<span className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Integrated Terminal
</span>
</SelectItem>
{terminals.map((terminal) => {
const TerminalIcon = getTerminalIcon(terminal.id);
return (
<SelectItem key={terminal.id} value={terminal.id}>
<span className="flex items-center gap-2">
<TerminalIcon className="w-4 h-4" />
{terminal.name}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{terminals.length === 0 && !isRefreshing && (
<p className="text-xs text-muted-foreground italic">
No external terminals detected. Click refresh to re-scan.
</div>
<div className="p-6 space-y-6">
{/* Default External Terminal */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Default External Terminal</Label>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={refresh}
disabled={isRefreshing}
title="Refresh available terminals"
aria-label="Refresh available terminals"
>
<RefreshCw className={cn('w-3.5 h-3.5', isRefreshing && 'animate-spin')} />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Terminal to use when selecting "Open in Terminal" from the worktree menu
</p>
)}
</div>
{/* Default Open Mode */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Default Open Mode</Label>
<p className="text-xs text-muted-foreground">
How to open the integrated terminal when using "Open in Terminal" from the worktree menu
</p>
<Select
value={openTerminalMode}
onValueChange={(value: 'newTab' | 'split') => {
setOpenTerminalMode(value);
toast.success(
value === 'newTab'
? 'New terminals will open in new tabs'
: 'New terminals will split the current tab'
);
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="newTab">
<span className="flex items-center gap-2">
<SquarePlus className="w-4 h-4" />
New Tab
</span>
</SelectItem>
<SelectItem value="split">
<span className="flex items-center gap-2">
<SplitSquareHorizontal className="w-4 h-4" />
Split Current Tab
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Font Family */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Font Family</Label>
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily: option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
<Select
value={defaultTerminalId ?? 'integrated'}
onValueChange={(value) => {
setDefaultTerminalId(value === 'integrated' ? null : value);
toast.success(
value === 'integrated'
? 'Integrated terminal set as default'
: 'Default terminal changed'
);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a terminal" />
</SelectTrigger>
<SelectContent>
<SelectItem value="integrated">
<span className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Integrated Terminal
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Default Font Size */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Default Font Size</Label>
<span className="text-sm text-muted-foreground">{defaultFontSize}px</span>
{terminals.map((terminal) => {
const TerminalIcon = getTerminalIcon(terminal.id);
return (
<SelectItem key={terminal.id} value={terminal.id}>
<span className="flex items-center gap-2">
<TerminalIcon className="w-4 h-4" />
{terminal.name}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{terminals.length === 0 && !isRefreshing && (
<p className="text-xs text-muted-foreground italic">
No external terminals detected. Click refresh to re-scan.
</p>
)}
</div>
<Slider
value={[defaultFontSize]}
min={8}
max={32}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
className="flex-1"
/>
</div>
{/* Line Height */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Line Height</Label>
<span className="text-sm text-muted-foreground">{lineHeight.toFixed(1)}</span>
</div>
<Slider
value={[lineHeight]}
min={1.0}
max={2.0}
step={0.1}
onValueChange={([value]) => {
setTerminalLineHeight(value);
}}
onValueCommit={() => {
toast.info('Line height changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
/>
</div>
{/* Scrollback Lines */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Scrollback Buffer</Label>
<span className="text-sm text-muted-foreground">
{(scrollbackLines / 1000).toFixed(0)}k lines
</span>
</div>
<Slider
value={[scrollbackLines]}
min={1000}
max={100000}
step={1000}
onValueChange={([value]) => setTerminalScrollbackLines(value)}
onValueCommit={() => {
toast.info('Scrollback changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
/>
</div>
{/* Default Run Script */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Default Run Script</Label>
<p className="text-xs text-muted-foreground">
Command to run automatically when opening a new terminal (e.g., "claude", "codex")
</p>
<Input
value={defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, codex, npm run dev"
className="bg-accent/30 border-border/50"
/>
</div>
{/* Screen Reader Mode */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-foreground font-medium">Screen Reader Mode</Label>
{/* Default Open Mode */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Default Open Mode</Label>
<p className="text-xs text-muted-foreground">
Enable accessibility mode for screen readers
How to open the integrated terminal when using "Open in Terminal" from the worktree
menu
</p>
<Select
value={openTerminalMode}
onValueChange={(value: 'newTab' | 'split') => {
setOpenTerminalMode(value);
toast.success(
value === 'newTab'
? 'New terminals will open in new tabs'
: 'New terminals will split the current tab'
);
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="newTab">
<span className="flex items-center gap-2">
<SquarePlus className="w-4 h-4" />
New Tab
</span>
</SelectItem>
<SelectItem value="split">
<span className="flex items-center gap-2">
<SplitSquareHorizontal className="w-4 h-4" />
Split Current Tab
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<Switch
checked={screenReaderMode}
onCheckedChange={(checked) => {
setTerminalScreenReaderMode(checked);
toast.success(
checked ? 'Screen reader mode enabled' : 'Screen reader mode disabled',
{
{/* Font Family */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Font Family</Label>
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
}
);
}}
/>
});
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily: option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Default Font Size */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Default Font Size</Label>
<span className="text-sm text-muted-foreground">{defaultFontSize}px</span>
</div>
<Slider
value={[defaultFontSize]}
min={8}
max={32}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
className="flex-1"
/>
</div>
{/* Line Height */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Line Height</Label>
<span className="text-sm text-muted-foreground">{lineHeight.toFixed(1)}</span>
</div>
<Slider
value={[lineHeight]}
min={1.0}
max={2.0}
step={0.1}
onValueChange={([value]) => {
setTerminalLineHeight(value);
}}
onValueCommit={() => {
toast.info('Line height changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
/>
</div>
{/* Scrollback Lines */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Scrollback Buffer</Label>
<span className="text-sm text-muted-foreground">
{(scrollbackLines / 1000).toFixed(0)}k lines
</span>
</div>
<Slider
value={[scrollbackLines]}
min={1000}
max={100000}
step={1000}
onValueChange={([value]) => setTerminalScrollbackLines(value)}
onValueCommit={() => {
toast.info('Scrollback changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
/>
</div>
{/* Default Run Script */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Default Run Script</Label>
<p className="text-xs text-muted-foreground">
Command to run automatically when opening a new terminal (e.g., "claude", "codex")
</p>
<Input
value={defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, codex, npm run dev"
className="bg-accent/30 border-border/50"
/>
</div>
{/* Screen Reader Mode */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-foreground font-medium">Screen Reader Mode</Label>
<p className="text-xs text-muted-foreground">
Enable accessibility mode for screen readers
</p>
</div>
<Switch
checked={screenReaderMode}
onCheckedChange={(checked) => {
setTerminalScreenReaderMode(checked);
toast.success(
checked ? 'Screen reader mode enabled' : 'Screen reader mode disabled',
{
description: 'Restart terminal for changes to take effect',
}
);
}}
/>
</div>
</div>
</div>
<TerminalConfigSection />
</div>
);
}

View File

@@ -10,11 +10,12 @@ import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import { getGlobalEventsRecent } from '@/hooks/use-event-recency';
import { createSmartPollingInterval, getGlobalEventsRecent } from '@/hooks/use-event-recency';
import type { Feature } from '@/store/app-store';
const FEATURES_REFETCH_ON_FOCUS = false;
const FEATURES_REFETCH_ON_RECONNECT = false;
const FEATURES_POLLING_INTERVAL = 30000;
/** Default polling interval for agent output when WebSocket is inactive */
const AGENT_OUTPUT_POLLING_INTERVAL = 5000;
@@ -43,6 +44,7 @@ export function useFeatures(projectPath: string | undefined) {
},
enabled: !!projectPath,
staleTime: STALE_TIMES.FEATURES,
refetchInterval: createSmartPollingInterval(FEATURES_POLLING_INTERVAL),
refetchOnWindowFocus: FEATURES_REFETCH_ON_FOCUS,
refetchOnReconnect: FEATURES_REFETCH_ON_RECONNECT,
});

View File

@@ -9,9 +9,11 @@ import { useQuery } from '@tanstack/react-query';
import { getElectronAPI, type RunningAgent } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import { createSmartPollingInterval } from '@/hooks/use-event-recency';
const RUNNING_AGENTS_REFETCH_ON_FOCUS = false;
const RUNNING_AGENTS_REFETCH_ON_RECONNECT = false;
const RUNNING_AGENTS_POLLING_INTERVAL = 30000;
interface RunningAgentsResult {
agents: RunningAgent[];
@@ -47,8 +49,7 @@ export function useRunningAgents() {
};
},
staleTime: STALE_TIMES.RUNNING_AGENTS,
// Note: Don't use refetchInterval here - rely on WebSocket invalidation
// for real-time updates instead of polling
refetchInterval: createSmartPollingInterval(RUNNING_AGENTS_POLLING_INTERVAL),
refetchOnWindowFocus: RUNNING_AGENTS_REFETCH_ON_FOCUS,
refetchOnReconnect: RUNNING_AGENTS_REFETCH_ON_RECONNECT,
});

View File

@@ -8,9 +8,11 @@ import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import { createSmartPollingInterval } from '@/hooks/use-event-recency';
const WORKTREE_REFETCH_ON_FOCUS = false;
const WORKTREE_REFETCH_ON_RECONNECT = false;
const WORKTREES_POLLING_INTERVAL = 30000;
interface WorktreeInfo {
path: string;
@@ -65,6 +67,7 @@ export function useWorktrees(projectPath: string | undefined, includeDetails = t
},
enabled: !!projectPath,
staleTime: STALE_TIMES.WORKTREES,
refetchInterval: createSmartPollingInterval(WORKTREES_POLLING_INTERVAL),
refetchOnWindowFocus: WORKTREE_REFETCH_ON_FOCUS,
refetchOnReconnect: WORKTREE_REFETCH_ON_RECONNECT,
});

View File

@@ -6,10 +6,12 @@ import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import type { AutoModeEvent } from '@/types/electron';
import type { WorktreeInfo } from '@/components/views/board-view/worktree-panel/types';
import { getGlobalEventsRecent } from '@/hooks/use-event-recency';
const logger = createLogger('AutoMode');
const AUTO_MODE_SESSION_KEY = 'automaker:autoModeRunningByWorktreeKey';
const AUTO_MODE_POLLING_INTERVAL = 30000;
/**
* Generate a worktree key for session storage
@@ -140,42 +142,54 @@ export function useAutoMode(worktree?: WorktreeInfo) {
// Check if we can start a new task based on concurrency limit
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
const refreshStatus = useCallback(async () => {
if (!currentProject) return;
try {
const api = getElectronAPI();
if (!api?.autoMode?.status) return;
const result = await api.autoMode.status(currentProject.path, branchName);
if (result.success && result.isAutoLoopRunning !== undefined) {
const backendIsRunning = result.isAutoLoopRunning;
if (backendIsRunning !== isAutoModeRunning) {
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
logger.info(
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
);
setAutoModeRunning(
currentProject.id,
branchName,
backendIsRunning,
result.maxConcurrency,
result.runningFeatures
);
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
}
}
} catch (error) {
logger.error('Error syncing auto mode state with backend:', error);
}
}, [branchName, currentProject, isAutoModeRunning, setAutoModeRunning]);
// On mount, query backend for current auto loop status and sync UI state.
// This handles cases where the backend is still running after a page refresh.
useEffect(() => {
void refreshStatus();
}, [refreshStatus]);
// Periodic polling fallback when WebSocket events are stale.
useEffect(() => {
if (!currentProject) return;
const syncWithBackend = async () => {
try {
const api = getElectronAPI();
if (!api?.autoMode?.status) return;
const interval = setInterval(() => {
if (getGlobalEventsRecent()) return;
void refreshStatus();
}, AUTO_MODE_POLLING_INTERVAL);
const result = await api.autoMode.status(currentProject.path, branchName);
if (result.success && result.isAutoLoopRunning !== undefined) {
const backendIsRunning = result.isAutoLoopRunning;
if (backendIsRunning !== isAutoModeRunning) {
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
logger.info(
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
);
setAutoModeRunning(
currentProject.id,
branchName,
backendIsRunning,
result.maxConcurrency,
result.runningFeatures
);
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
}
}
} catch (error) {
logger.error('Error syncing auto mode state with backend:', error);
}
};
syncWithBackend();
}, [currentProject, branchName, setAutoModeRunning]);
return () => clearInterval(interval);
}, [currentProject, refreshStatus]);
// Handle auto mode events - listen globally for all projects/worktrees
useEffect(() => {
@@ -672,5 +686,6 @@ export function useAutoMode(worktree?: WorktreeInfo) {
start,
stop,
stopFeature,
refreshStatus,
};
}

View File

@@ -27,18 +27,20 @@ export interface AgentTaskInfo {
/**
* Default model used by the feature executor
*/
export const DEFAULT_MODEL = 'claude-opus-4-5-20251101';
export const DEFAULT_MODEL = 'claude-opus-4-6';
/**
* Formats a model name for display
*/
export function formatModelName(model: string): string {
// Claude models
if (model.includes('opus-4-6')) return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';
// Codex/GPT models - specific formatting
if (model === 'codex-gpt-5.3-codex') return 'GPT-5.3 Codex';
if (model === 'codex-gpt-5.2-codex') return 'GPT-5.2 Codex';
if (model === 'codex-gpt-5.2') return 'GPT-5.2';
if (model === 'codex-gpt-5.1-codex-max') return 'GPT-5.1 Max';