mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 22:13:08 +00:00
Merge remote-tracking branch 'origin/v0.15.0rc' into feature/bug-startup-warning-ignores-claude-oauth-credenti-fuzx
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'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 alias gs='git status' alias ll='ls -la' 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user