-
Notifications
You must be signed in to change notification settings - Fork 513
feature/custom terminal configs #717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v0.14.0rc
Are you sure you want to change the base?
Changes from 12 commits
7e90328
cea405f
13457de
4f28286
691c181
8d2d6d8
f07fbc0
efad219
7d31a05
92cbb42
a44d425
b1387d2
ca5a16f
01d59d5
b3829f6
942e6ba
b818a45
a204bc1
02659b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,12 @@ 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'; | ||
|
|
||
| // Maximum scrollback buffer size (characters) | ||
| const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal | ||
|
|
||
|
|
@@ -42,6 +56,25 @@ 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; | ||
| } | ||
|
|
||
| export interface TerminalSession { | ||
| id: string; | ||
| pty: pty.IPty; | ||
|
|
@@ -77,6 +110,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. | ||
|
|
@@ -116,11 +155,11 @@ export class TerminalService extends EventEmitter { | |
| return []; | ||
| } | ||
| // sh doesn't support --login in all implementations | ||
| if (shellName === 'sh') { | ||
| if (shellName === SHELL_NAME_SH) { | ||
| return []; | ||
| } | ||
| // bash, zsh, and other POSIX shells support --login | ||
| return ['--login']; | ||
| return [BASH_LOGIN_ARG]; | ||
| }; | ||
|
|
||
| // Check if running in WSL - prefer user's shell or bash with --login | ||
|
|
@@ -313,8 +352,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 = [...detectedShellArgs]; | ||
|
|
||
| // Validate and resolve working directory | ||
| // Uses secureFs internally to enforce ALLOWED_ROOT_DIRECTORY | ||
|
|
@@ -332,6 +372,101 @@ 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; | ||
|
|
||
| // Merge global and project terminal configs | ||
| const globalTerminalConfig = globalSettings?.terminalConfig; | ||
| const projectTerminalConfig = projectSettings?.terminalConfig; | ||
|
|
||
| logger.info( | ||
| `[createSession] Terminal config: global.enabled=${globalTerminalConfig?.enabled}, project.enabled=${projectTerminalConfig?.enabled}` | ||
| ); | ||
|
|
||
| // Determine if terminal config is enabled | ||
| const enabled = | ||
| projectTerminalConfig?.enabled !== undefined | ||
| ? projectTerminalConfig.enabled | ||
| : globalTerminalConfig?.enabled || false; | ||
|
|
||
| logger.info(`[createSession] Terminal config effective enabled: ${enabled}`); | ||
|
|
||
| if (enabled && globalTerminalConfig) { | ||
| const currentTheme = globalSettings?.theme || 'dark'; | ||
| const themeColors = getTerminalThemeColors(currentTheme); | ||
| const allThemes = getAllTerminalThemes(); | ||
|
|
||
| // Full config object (global + project overrides) | ||
| const effectiveConfig: TerminalConfig = { | ||
| enabled: true, | ||
| customPrompt: globalTerminalConfig.customPrompt, | ||
| promptFormat: globalTerminalConfig.promptFormat, | ||
| showGitBranch: globalTerminalConfig.showGitBranch, | ||
| showGitStatus: globalTerminalConfig.showGitStatus, | ||
| customAliases: | ||
| projectTerminalConfig?.customAliases || globalTerminalConfig.customAliases, | ||
| customEnvVars: { | ||
| ...globalTerminalConfig.customEnvVars, | ||
| ...projectTerminalConfig?.customEnvVars, | ||
| }, | ||
| rcFileVersion: globalTerminalConfig.rcFileVersion, | ||
| }; | ||
|
|
||
| // Ensure RC files are up to date | ||
| await ensureRcFilesUpToDate( | ||
| options.cwd || cwd, | ||
| currentTheme, | ||
| effectiveConfig, | ||
| themeColors, | ||
| allThemes | ||
| ); | ||
|
|
||
| // Set shell-specific env vars | ||
| const shellName = path.basename(shell).toLowerCase(); | ||
|
|
||
| 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 +476,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 +788,52 @@ 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; | ||
|
|
||
| if (terminalConfig?.enabled) { | ||
| const themeColors = getTerminalThemeColors( | ||
| newTheme as import('@automaker/types').ThemeMode | ||
| ); | ||
| const allThemes = getAllTerminalThemes(); | ||
|
|
||
| // Regenerate RC files with new theme | ||
| const effectiveConfig: TerminalConfig = { | ||
| enabled: true, | ||
| customPrompt: terminalConfig.customPrompt, | ||
| promptFormat: terminalConfig.promptFormat, | ||
| showGitBranch: terminalConfig.showGitBranch, | ||
| showGitStatus: terminalConfig.showGitStatus, | ||
| customAliases: terminalConfig.customAliases, | ||
| customEnvVars: terminalConfig.customEnvVars, | ||
| rcFileVersion: terminalConfig.rcFileVersion, | ||
| }; | ||
|
||
|
|
||
| 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 +858,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; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.