Files
automaker/apps/ui/src/components/views/settings-view/terminal/prompt-preview.tsx
Dhanush Santosh 88864ad6bc feature/custom terminal configs (#717)
* 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>
2026-02-03 20:34:33 +05:30

284 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
);
}