mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
* feat(terminal): Add core infrastructure for custom terminal configurations - Add TerminalConfig types to settings schema (global & project-specific) - Create RC generator with hex-to-xterm-256 color mapping - Create RC file manager for .automaker/terminal/ directory - Add terminal theme color data (40 themes) to platform package - Integrate terminal config injection into TerminalService - Support bash, zsh, and sh with proper env var injection (BASH_ENV, ZDOTDIR, ENV) - Add onThemeChange hook for theme synchronization Part of custom terminal configurations feature implementation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(terminal): Wire terminal service with settings service - Pass SettingsService to TerminalService constructor - Initialize terminal service with settings service dependency - Enable terminal config injection to work with actual settings This completes Steps 1-4 of the terminal configuration plan: - RC Generator (color mapping, prompt formats) - RC File Manager (file I/O, atomic writes) - Settings Schema (GlobalSettings + ProjectSettings) - Terminal Service Integration (env var injection) Next steps: Settings UI and theme change hooks. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(terminal): Add Settings UI and theme change synchronization Complete Steps 5 & 6 of terminal configuration implementation: Settings UI Components: - Add PromptPreview component with live theme-aware rendering - Add TerminalConfigSection with comprehensive controls: * Enable/disable toggle with confirmation dialog * Custom prompt toggle * Prompt format selector (4 formats) * Git branch/status toggles * Custom aliases textarea * Custom env vars key-value editor with validation * Info box explaining behavior - Integrate into existing TerminalSection Theme Change Hook: - Add theme detection in update-global settings route - Regenerate RC files for all projects when theme changes - Skip projects with terminal config disabled - Error handling with per-project logging - Inject terminal service with settings service dependency This completes the full terminal configuration feature: ✓ RC Generator (color mapping, prompts) ✓ RC File Manager (file I/O, versioning) ✓ Settings Schema (types, defaults) ✓ Terminal Service Integration (env vars, PTY spawn) ✓ Settings UI (comprehensive controls, preview) ✓ Theme Synchronization (automatic RC regeneration) New terminals will use custom prompts matching app theme. Existing terminals unaffected. User RC files preserved. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Add error handling and explicit field mapping for terminal config - Add try-catch block to handleToggleEnabled - Explicitly set all required terminalConfig fields - Add console logging for debugging - Show error toast if update fails - Include rcFileVersion: 1 in config object This should fix the issue where the toggle doesn't enable after clicking OK in the confirmation dialog. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Use React Query mutation hook for settings updates The issue was that `updateGlobalSettings` doesn't exist in the app store. The correct pattern is to use the `useUpdateGlobalSettings` hook from use-settings-mutations.ts, which is a React Query mutation. Changes: - Import useUpdateGlobalSettings from mutations hook - Use mutation.mutate() instead of direct function call - Add proper onSuccess/onError callbacks - Remove async/await pattern (React Query handles this) This fixes the toggle not enabling after clicking OK in the confirmation dialog. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Use React Query hook for globalSettings instead of store The root cause: Component was reading globalSettings from the app store, which doesn't update reactively when the mutation completes. Solution: Use useGlobalSettings() React Query hook which: - Automatically refetches when the mutation invalidates the cache - Triggers re-render with updated data - Makes the toggle reflect the new state Now the flow is: 1. User clicks toggle → confirmation dialog 2. Click OK → mutation.mutate() called 3. Mutation succeeds → invalidates queryKeys.settings.global() 4. Query refetches → component re-renders with new globalSettings 5. Toggle shows enabled state ✓ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * debug(terminal): Add detailed logging for terminal config application Add logging to track: - When terminal config check happens - CWD being used - Global and project enabled states - Effective enabled state This will help diagnose why RC files aren't being generated when opening terminals in Automaker. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix terminal rc updates and bash rcfile loading * feat(terminal): add banner on shell start * feat(terminal): colorize banner per theme * chore(terminal): bump rc version for banner colors * feat(terminal): match banner colors to launcher * feat(terminal): add prompt customization controls * feat: integrate oh-my-posh prompt themes * fix: resolve oh-my-posh theme path * fix: correct oh-my-posh config invocation * docs: add terminal theme screenshot * fix: address review feedback and stabilize e2e test * ui: split terminal config into separate card * fix: enable cross-platform Warp terminal detection - Remove macOS-only platform restriction for Warp - Add Linux CLI alias 'warp-terminal' (primary on Linux) - Add CLI launch handler using --cwd flag - Fixes issue where Warp was not detected on Linux systems Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
284 lines
9.1 KiB
TypeScript
284 lines
9.1 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
}
|