mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
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>
This commit is contained in:
@@ -390,7 +390,7 @@ const server = createServer(app);
|
||||
// WebSocket servers using noServer mode for proper multi-path support
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const terminalWss = new WebSocketServer({ noServer: true });
|
||||
const terminalService = getTerminalService();
|
||||
const terminalService = getTerminalService(settingsService);
|
||||
|
||||
/**
|
||||
* Authenticate WebSocket upgrade requests
|
||||
|
||||
25
apps/server/src/lib/terminal-themes-data.ts
Normal file
25
apps/server/src/lib/terminal-themes-data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Terminal Theme Data - Re-export terminal themes from platform package
|
||||
*
|
||||
* This module re-exports terminal theme data for use in the server.
|
||||
*/
|
||||
|
||||
import { terminalThemeColors, getTerminalThemeColors as getThemeColors } from '@automaker/platform';
|
||||
import type { ThemeMode } from '@automaker/types';
|
||||
import type { TerminalTheme } from '@automaker/platform';
|
||||
|
||||
/**
|
||||
* Get terminal theme colors for a given theme mode
|
||||
*/
|
||||
export function getTerminalThemeColors(theme: ThemeMode): TerminalTheme {
|
||||
return getThemeColors(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all terminal themes
|
||||
*/
|
||||
export function getAllTerminalThemes(): Record<ThemeMode, TerminalTheme> {
|
||||
return terminalThemeColors;
|
||||
}
|
||||
|
||||
export default terminalThemeColors;
|
||||
@@ -14,6 +14,7 @@ import type { GlobalSettings } from '../../../types/settings.js';
|
||||
import { getErrorMessage, logError, logger } from '../common.js';
|
||||
import { setLogLevel, LogLevel } from '@automaker/utils';
|
||||
import { setRequestLoggingEnabled } from '../../../index.js';
|
||||
import { getTerminalService } from '../../../services/terminal-service.js';
|
||||
|
||||
/**
|
||||
* Map server log level string to LogLevel enum
|
||||
@@ -57,6 +58,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
||||
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
|
||||
);
|
||||
|
||||
// Get old settings to detect theme changes
|
||||
const oldSettings = await settingsService.getGlobalSettings();
|
||||
const oldTheme = oldSettings?.theme;
|
||||
|
||||
logger.info('[SERVER_SETTINGS_UPDATE] Calling updateGlobalSettings...');
|
||||
const settings = await settingsService.updateGlobalSettings(updates);
|
||||
logger.info(
|
||||
@@ -64,6 +69,37 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
||||
settings.projects?.length ?? 0
|
||||
);
|
||||
|
||||
// Handle theme change - regenerate terminal RC files for all projects
|
||||
if ('theme' in updates && updates.theme && updates.theme !== oldTheme) {
|
||||
const terminalService = getTerminalService(settingsService);
|
||||
const newTheme = updates.theme;
|
||||
|
||||
logger.info(
|
||||
`[TERMINAL_CONFIG] Theme changed from ${oldTheme} to ${newTheme}, regenerating RC files`
|
||||
);
|
||||
|
||||
// Regenerate RC files for all projects with terminal config enabled
|
||||
const projects = settings.projects || [];
|
||||
for (const project of projects) {
|
||||
try {
|
||||
const projectSettings = await settingsService.getProjectSettings(project.path);
|
||||
// Check if terminal config is enabled (global or project-specific)
|
||||
const terminalConfigEnabled =
|
||||
projectSettings.terminalConfig?.enabled !== false &&
|
||||
settings.terminalConfig?.enabled === true;
|
||||
|
||||
if (terminalConfigEnabled) {
|
||||
await terminalService.onThemeChange(project.path, newTheme);
|
||||
logger.info(`[TERMINAL_CONFIG] Regenerated RC files for project: ${project.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[TERMINAL_CONFIG] Failed to regenerate RC files for project ${project.name}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply server log level if it was updated
|
||||
if ('serverLogLevel' in updates && updates.serverLogLevel) {
|
||||
const level = LOG_LEVEL_MAP[updates.serverLogLevel];
|
||||
|
||||
@@ -13,6 +13,14 @@ import * as path from 'path';
|
||||
// to enforce ALLOWED_ROOT_DIRECTORY security boundary
|
||||
import * as secureFs from '../lib/secure-fs.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import type { SettingsService } from './settings-service.js';
|
||||
import { getTerminalThemeColors, getAllTerminalThemes } from '../lib/terminal-themes-data.js';
|
||||
import {
|
||||
getRcFilePath,
|
||||
getTerminalDir,
|
||||
ensureRcFilesUpToDate,
|
||||
type TerminalConfig,
|
||||
} from '@automaker/platform';
|
||||
|
||||
const logger = createLogger('Terminal');
|
||||
// System paths module handles shell binary checks and WSL detection
|
||||
@@ -24,6 +32,27 @@ import {
|
||||
getShellPaths,
|
||||
} from '@automaker/platform';
|
||||
|
||||
const BASH_LOGIN_ARG = '--login';
|
||||
const BASH_RCFILE_ARG = '--rcfile';
|
||||
const SHELL_NAME_BASH = 'bash';
|
||||
const SHELL_NAME_ZSH = 'zsh';
|
||||
const SHELL_NAME_SH = 'sh';
|
||||
const DEFAULT_SHOW_USER_HOST = true;
|
||||
const DEFAULT_SHOW_PATH = true;
|
||||
const DEFAULT_SHOW_TIME = false;
|
||||
const DEFAULT_SHOW_EXIT_STATUS = false;
|
||||
const DEFAULT_PATH_DEPTH = 0;
|
||||
const DEFAULT_PATH_STYLE: TerminalConfig['pathStyle'] = 'full';
|
||||
const DEFAULT_CUSTOM_PROMPT = true;
|
||||
const DEFAULT_PROMPT_FORMAT: TerminalConfig['promptFormat'] = 'standard';
|
||||
const DEFAULT_SHOW_GIT_BRANCH = true;
|
||||
const DEFAULT_SHOW_GIT_STATUS = true;
|
||||
const DEFAULT_CUSTOM_ALIASES = '';
|
||||
const DEFAULT_CUSTOM_ENV_VARS: Record<string, string> = {};
|
||||
const PROMPT_THEME_CUSTOM = 'custom';
|
||||
const PROMPT_THEME_PREFIX = 'omp-';
|
||||
const OMP_THEME_ENV_VAR = 'AUTOMAKER_OMP_THEME';
|
||||
|
||||
// Maximum scrollback buffer size (characters)
|
||||
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
|
||||
|
||||
@@ -42,6 +71,114 @@ let maxSessions = parseInt(process.env.TERMINAL_MAX_SESSIONS || '1000', 10);
|
||||
const OUTPUT_THROTTLE_MS = 4; // ~250fps max update rate for responsive input
|
||||
const OUTPUT_BATCH_SIZE = 4096; // Smaller batches for lower latency
|
||||
|
||||
function applyBashRcFileArgs(args: string[], rcFilePath: string): string[] {
|
||||
const sanitizedArgs: string[] = [];
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === BASH_LOGIN_ARG) {
|
||||
continue;
|
||||
}
|
||||
if (arg === BASH_RCFILE_ARG) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
sanitizedArgs.push(arg);
|
||||
}
|
||||
|
||||
sanitizedArgs.push(BASH_RCFILE_ARG, rcFilePath);
|
||||
return sanitizedArgs;
|
||||
}
|
||||
|
||||
function normalizePathStyle(
|
||||
pathStyle: TerminalConfig['pathStyle'] | undefined
|
||||
): TerminalConfig['pathStyle'] {
|
||||
if (pathStyle === 'short' || pathStyle === 'basename') {
|
||||
return pathStyle;
|
||||
}
|
||||
return DEFAULT_PATH_STYLE;
|
||||
}
|
||||
|
||||
function normalizePathDepth(pathDepth: number | undefined): number {
|
||||
const depth =
|
||||
typeof pathDepth === 'number' && Number.isFinite(pathDepth) ? pathDepth : DEFAULT_PATH_DEPTH;
|
||||
return Math.max(DEFAULT_PATH_DEPTH, Math.floor(depth));
|
||||
}
|
||||
|
||||
function getShellBasename(shellPath: string): string {
|
||||
const lastSep = Math.max(shellPath.lastIndexOf('/'), shellPath.lastIndexOf('\\'));
|
||||
return lastSep >= 0 ? shellPath.slice(lastSep + 1) : shellPath;
|
||||
}
|
||||
|
||||
function getShellArgsForPath(shellPath: string): string[] {
|
||||
const shellName = getShellBasename(shellPath).toLowerCase().replace('.exe', '');
|
||||
if (shellName === 'powershell' || shellName === 'pwsh' || shellName === 'cmd') {
|
||||
return [];
|
||||
}
|
||||
if (shellName === SHELL_NAME_SH) {
|
||||
return [];
|
||||
}
|
||||
return [BASH_LOGIN_ARG];
|
||||
}
|
||||
|
||||
function resolveOmpThemeName(promptTheme: string | undefined): string | null {
|
||||
if (!promptTheme || promptTheme === PROMPT_THEME_CUSTOM) {
|
||||
return null;
|
||||
}
|
||||
if (promptTheme.startsWith(PROMPT_THEME_PREFIX)) {
|
||||
return promptTheme.slice(PROMPT_THEME_PREFIX.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildEffectiveTerminalConfig(
|
||||
globalTerminalConfig: TerminalConfig | undefined,
|
||||
projectTerminalConfig: Partial<TerminalConfig> | undefined
|
||||
): TerminalConfig {
|
||||
const mergedEnvVars = {
|
||||
...(globalTerminalConfig?.customEnvVars ?? DEFAULT_CUSTOM_ENV_VARS),
|
||||
...(projectTerminalConfig?.customEnvVars ?? DEFAULT_CUSTOM_ENV_VARS),
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: projectTerminalConfig?.enabled ?? globalTerminalConfig?.enabled ?? false,
|
||||
customPrompt: globalTerminalConfig?.customPrompt ?? DEFAULT_CUSTOM_PROMPT,
|
||||
promptFormat: globalTerminalConfig?.promptFormat ?? DEFAULT_PROMPT_FORMAT,
|
||||
showGitBranch:
|
||||
projectTerminalConfig?.showGitBranch ??
|
||||
globalTerminalConfig?.showGitBranch ??
|
||||
DEFAULT_SHOW_GIT_BRANCH,
|
||||
showGitStatus:
|
||||
projectTerminalConfig?.showGitStatus ??
|
||||
globalTerminalConfig?.showGitStatus ??
|
||||
DEFAULT_SHOW_GIT_STATUS,
|
||||
showUserHost:
|
||||
projectTerminalConfig?.showUserHost ??
|
||||
globalTerminalConfig?.showUserHost ??
|
||||
DEFAULT_SHOW_USER_HOST,
|
||||
showPath:
|
||||
projectTerminalConfig?.showPath ?? globalTerminalConfig?.showPath ?? DEFAULT_SHOW_PATH,
|
||||
pathStyle: normalizePathStyle(
|
||||
projectTerminalConfig?.pathStyle ?? globalTerminalConfig?.pathStyle
|
||||
),
|
||||
pathDepth: normalizePathDepth(
|
||||
projectTerminalConfig?.pathDepth ?? globalTerminalConfig?.pathDepth
|
||||
),
|
||||
showTime:
|
||||
projectTerminalConfig?.showTime ?? globalTerminalConfig?.showTime ?? DEFAULT_SHOW_TIME,
|
||||
showExitStatus:
|
||||
projectTerminalConfig?.showExitStatus ??
|
||||
globalTerminalConfig?.showExitStatus ??
|
||||
DEFAULT_SHOW_EXIT_STATUS,
|
||||
customAliases:
|
||||
projectTerminalConfig?.customAliases ??
|
||||
globalTerminalConfig?.customAliases ??
|
||||
DEFAULT_CUSTOM_ALIASES,
|
||||
customEnvVars: mergedEnvVars,
|
||||
rcFileVersion: globalTerminalConfig?.rcFileVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string;
|
||||
pty: pty.IPty;
|
||||
@@ -77,6 +214,12 @@ export class TerminalService extends EventEmitter {
|
||||
!!(process.versions && (process.versions as Record<string, string>).electron) ||
|
||||
!!process.env.ELECTRON_RUN_AS_NODE;
|
||||
private useConptyFallback = false; // Track if we need to use winpty fallback on Windows
|
||||
private settingsService: SettingsService | null = null;
|
||||
|
||||
constructor(settingsService?: SettingsService) {
|
||||
super();
|
||||
this.settingsService = settingsService || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a PTY process with platform-specific handling.
|
||||
@@ -102,37 +245,19 @@ export class TerminalService extends EventEmitter {
|
||||
const platform = os.platform();
|
||||
const shellPaths = getShellPaths();
|
||||
|
||||
// Helper to get basename handling both path separators
|
||||
const getBasename = (shellPath: string): string => {
|
||||
const lastSep = Math.max(shellPath.lastIndexOf('/'), shellPath.lastIndexOf('\\'));
|
||||
return lastSep >= 0 ? shellPath.slice(lastSep + 1) : shellPath;
|
||||
};
|
||||
|
||||
// Helper to get shell args based on shell name
|
||||
const getShellArgs = (shell: string): string[] => {
|
||||
const shellName = getBasename(shell).toLowerCase().replace('.exe', '');
|
||||
// PowerShell and cmd don't need --login
|
||||
if (shellName === 'powershell' || shellName === 'pwsh' || shellName === 'cmd') {
|
||||
return [];
|
||||
}
|
||||
// sh doesn't support --login in all implementations
|
||||
if (shellName === 'sh') {
|
||||
return [];
|
||||
}
|
||||
// bash, zsh, and other POSIX shells support --login
|
||||
return ['--login'];
|
||||
};
|
||||
|
||||
// Check if running in WSL - prefer user's shell or bash with --login
|
||||
if (platform === 'linux' && this.isWSL()) {
|
||||
const userShell = process.env.SHELL;
|
||||
if (userShell) {
|
||||
// Try to find userShell in allowed paths
|
||||
for (const allowedShell of shellPaths) {
|
||||
if (allowedShell === userShell || getBasename(allowedShell) === getBasename(userShell)) {
|
||||
if (
|
||||
allowedShell === userShell ||
|
||||
getShellBasename(allowedShell) === getShellBasename(userShell)
|
||||
) {
|
||||
try {
|
||||
if (systemPathExists(allowedShell)) {
|
||||
return { shell: allowedShell, args: getShellArgs(allowedShell) };
|
||||
return { shell: allowedShell, args: getShellArgsForPath(allowedShell) };
|
||||
}
|
||||
} catch {
|
||||
// Path not allowed, continue searching
|
||||
@@ -144,7 +269,7 @@ export class TerminalService extends EventEmitter {
|
||||
for (const shell of shellPaths) {
|
||||
try {
|
||||
if (systemPathExists(shell)) {
|
||||
return { shell, args: getShellArgs(shell) };
|
||||
return { shell, args: getShellArgsForPath(shell) };
|
||||
}
|
||||
} catch {
|
||||
// Path not allowed, continue
|
||||
@@ -158,10 +283,13 @@ export class TerminalService extends EventEmitter {
|
||||
if (userShell && platform !== 'win32') {
|
||||
// Try to find userShell in allowed paths
|
||||
for (const allowedShell of shellPaths) {
|
||||
if (allowedShell === userShell || getBasename(allowedShell) === getBasename(userShell)) {
|
||||
if (
|
||||
allowedShell === userShell ||
|
||||
getShellBasename(allowedShell) === getShellBasename(userShell)
|
||||
) {
|
||||
try {
|
||||
if (systemPathExists(allowedShell)) {
|
||||
return { shell: allowedShell, args: getShellArgs(allowedShell) };
|
||||
return { shell: allowedShell, args: getShellArgsForPath(allowedShell) };
|
||||
}
|
||||
} catch {
|
||||
// Path not allowed, continue searching
|
||||
@@ -174,7 +302,7 @@ export class TerminalService extends EventEmitter {
|
||||
for (const shell of shellPaths) {
|
||||
try {
|
||||
if (systemPathExists(shell)) {
|
||||
return { shell, args: getShellArgs(shell) };
|
||||
return { shell, args: getShellArgsForPath(shell) };
|
||||
}
|
||||
} catch {
|
||||
// Path not allowed or doesn't exist, continue to next
|
||||
@@ -313,8 +441,9 @@ export class TerminalService extends EventEmitter {
|
||||
|
||||
const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
const { shell: detectedShell, args: shellArgs } = this.detectShell();
|
||||
const { shell: detectedShell, args: detectedShellArgs } = this.detectShell();
|
||||
const shell = options.shell || detectedShell;
|
||||
let shellArgs = options.shell ? getShellArgsForPath(shell) : [...detectedShellArgs];
|
||||
|
||||
// Validate and resolve working directory
|
||||
// Uses secureFs internally to enforce ALLOWED_ROOT_DIRECTORY
|
||||
@@ -332,6 +461,89 @@ export class TerminalService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal config injection (custom prompts, themes)
|
||||
const terminalConfigEnv: Record<string, string> = {};
|
||||
if (this.settingsService) {
|
||||
try {
|
||||
logger.info(
|
||||
`[createSession] Checking terminal config for session ${id}, cwd: ${options.cwd || cwd}`
|
||||
);
|
||||
const globalSettings = await this.settingsService.getGlobalSettings();
|
||||
const projectSettings = options.cwd
|
||||
? await this.settingsService.getProjectSettings(options.cwd)
|
||||
: null;
|
||||
|
||||
const globalTerminalConfig = globalSettings?.terminalConfig;
|
||||
const projectTerminalConfig = projectSettings?.terminalConfig;
|
||||
const effectiveConfig = buildEffectiveTerminalConfig(
|
||||
globalTerminalConfig,
|
||||
projectTerminalConfig
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`[createSession] Terminal config: global.enabled=${globalTerminalConfig?.enabled}, project.enabled=${projectTerminalConfig?.enabled}`
|
||||
);
|
||||
logger.info(
|
||||
`[createSession] Terminal config effective enabled: ${effectiveConfig.enabled}`
|
||||
);
|
||||
|
||||
if (effectiveConfig.enabled && globalTerminalConfig) {
|
||||
const currentTheme = globalSettings?.theme || 'dark';
|
||||
const themeColors = getTerminalThemeColors(currentTheme);
|
||||
const allThemes = getAllTerminalThemes();
|
||||
const promptTheme =
|
||||
projectTerminalConfig?.promptTheme ?? globalTerminalConfig.promptTheme;
|
||||
const ompThemeName = resolveOmpThemeName(promptTheme);
|
||||
|
||||
// Ensure RC files are up to date
|
||||
await ensureRcFilesUpToDate(
|
||||
options.cwd || cwd,
|
||||
currentTheme,
|
||||
effectiveConfig,
|
||||
themeColors,
|
||||
allThemes
|
||||
);
|
||||
|
||||
// Set shell-specific env vars
|
||||
const shellName = getShellBasename(shell).toLowerCase();
|
||||
if (ompThemeName && effectiveConfig.customPrompt) {
|
||||
terminalConfigEnv[OMP_THEME_ENV_VAR] = ompThemeName;
|
||||
}
|
||||
|
||||
if (shellName.includes(SHELL_NAME_BASH)) {
|
||||
const bashRcFilePath = getRcFilePath(options.cwd || cwd, SHELL_NAME_BASH);
|
||||
terminalConfigEnv.BASH_ENV = bashRcFilePath;
|
||||
terminalConfigEnv.AUTOMAKER_CUSTOM_PROMPT = effectiveConfig.customPrompt
|
||||
? 'true'
|
||||
: 'false';
|
||||
terminalConfigEnv.AUTOMAKER_THEME = currentTheme;
|
||||
shellArgs = applyBashRcFileArgs(shellArgs, bashRcFilePath);
|
||||
} else if (shellName.includes(SHELL_NAME_ZSH)) {
|
||||
terminalConfigEnv.ZDOTDIR = getTerminalDir(options.cwd || cwd);
|
||||
terminalConfigEnv.AUTOMAKER_CUSTOM_PROMPT = effectiveConfig.customPrompt
|
||||
? 'true'
|
||||
: 'false';
|
||||
terminalConfigEnv.AUTOMAKER_THEME = currentTheme;
|
||||
} else if (shellName === SHELL_NAME_SH) {
|
||||
terminalConfigEnv.ENV = getRcFilePath(options.cwd || cwd, SHELL_NAME_SH);
|
||||
terminalConfigEnv.AUTOMAKER_CUSTOM_PROMPT = effectiveConfig.customPrompt
|
||||
? 'true'
|
||||
: 'false';
|
||||
terminalConfigEnv.AUTOMAKER_THEME = currentTheme;
|
||||
}
|
||||
|
||||
// Add custom env vars from config
|
||||
Object.assign(terminalConfigEnv, effectiveConfig.customEnvVars);
|
||||
|
||||
logger.info(
|
||||
`[createSession] Terminal config enabled for session ${id}, shell: ${shellName}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[createSession] Failed to apply terminal config: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {
|
||||
...cleanEnv,
|
||||
TERM: 'xterm-256color',
|
||||
@@ -341,6 +553,7 @@ export class TerminalService extends EventEmitter {
|
||||
LANG: process.env.LANG || 'en_US.UTF-8',
|
||||
LC_ALL: process.env.LC_ALL || process.env.LANG || 'en_US.UTF-8',
|
||||
...options.env,
|
||||
...terminalConfigEnv, // Apply terminal config env vars last (highest priority)
|
||||
};
|
||||
|
||||
logger.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
|
||||
@@ -652,6 +865,44 @@ export class TerminalService extends EventEmitter {
|
||||
return () => this.exitCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle theme change - regenerate RC files with new theme colors
|
||||
*/
|
||||
async onThemeChange(projectPath: string, newTheme: string): Promise<void> {
|
||||
if (!this.settingsService) {
|
||||
logger.warn('[onThemeChange] SettingsService not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const globalSettings = await this.settingsService.getGlobalSettings();
|
||||
const terminalConfig = globalSettings?.terminalConfig;
|
||||
const projectSettings = await this.settingsService.getProjectSettings(projectPath);
|
||||
const projectTerminalConfig = projectSettings?.terminalConfig;
|
||||
const effectiveConfig = buildEffectiveTerminalConfig(terminalConfig, projectTerminalConfig);
|
||||
|
||||
if (effectiveConfig.enabled && terminalConfig) {
|
||||
const themeColors = getTerminalThemeColors(
|
||||
newTheme as import('@automaker/types').ThemeMode
|
||||
);
|
||||
const allThemes = getAllTerminalThemes();
|
||||
|
||||
// Regenerate RC files with new theme
|
||||
await ensureRcFilesUpToDate(
|
||||
projectPath,
|
||||
newTheme as import('@automaker/types').ThemeMode,
|
||||
effectiveConfig,
|
||||
themeColors,
|
||||
allThemes
|
||||
);
|
||||
|
||||
logger.info(`[onThemeChange] Regenerated RC files for theme: ${newTheme}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[onThemeChange] Failed to regenerate RC files: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all sessions
|
||||
*/
|
||||
@@ -676,9 +927,9 @@ export class TerminalService extends EventEmitter {
|
||||
// Singleton instance
|
||||
let terminalService: TerminalService | null = null;
|
||||
|
||||
export function getTerminalService(): TerminalService {
|
||||
export function getTerminalService(settingsService?: SettingsService): TerminalService {
|
||||
if (!terminalService) {
|
||||
terminalService = new TerminalService();
|
||||
terminalService = new TerminalService(settingsService);
|
||||
}
|
||||
return terminalService;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user