diff --git a/web/src/client/components/session-view.ts b/web/src/client/components/session-view.ts
index 586e8243..c545cc9d 100644
--- a/web/src/client/components/session-view.ts
+++ b/web/src/client/components/session-view.ts
@@ -32,6 +32,7 @@ import {
COMMON_TERMINAL_WIDTHS,
TerminalPreferencesManager,
} from '../utils/terminal-preferences.js';
+import type { TerminalThemeId } from '../utils/terminal-themes.js';
import { ConnectionManager } from './session-view/connection-manager.js';
import {
type DirectKeyboardCallbacks,
@@ -87,6 +88,7 @@ export class SessionView extends LitElement {
@state() private showImagePicker = false;
@state() private isDragOver = false;
@state() private terminalFontSize = 14;
+ @state() private terminalTheme: TerminalThemeId = 'auto';
@state() private terminalContainerHeight = '100%';
@state() private isLandscape = false;
@@ -369,8 +371,10 @@ export class SessionView extends LitElement {
// Load terminal preferences
this.terminalMaxCols = this.preferencesManager.getMaxCols();
this.terminalFontSize = this.preferencesManager.getFontSize();
+ this.terminalTheme = this.preferencesManager.getTheme();
this.terminalLifecycleManager.setTerminalFontSize(this.terminalFontSize);
this.terminalLifecycleManager.setTerminalMaxCols(this.terminalMaxCols);
+ this.terminalLifecycleManager.setTerminalTheme(this.terminalTheme);
// Initialize lifecycle event manager
this.lifecycleEventManager = new LifecycleEventManager();
@@ -869,6 +873,18 @@ export class SessionView extends LitElement {
}
}
+ private handleThemeChange(newTheme: TerminalThemeId) {
+ this.terminalTheme = newTheme;
+ this.preferencesManager.setTheme(newTheme);
+ this.terminalLifecycleManager.setTerminalTheme(newTheme);
+
+ const terminal = this.querySelector('vibe-terminal') as Terminal;
+ if (terminal) {
+ terminal.theme = newTheme;
+ terminal.requestUpdate();
+ }
+ }
+
private handleOpenFileBrowser() {
this.showFileBrowser = true;
}
@@ -1285,6 +1301,7 @@ export class SessionView extends LitElement {
.fontSize=${this.terminalFontSize}
.fitHorizontally=${false}
.maxCols=${this.terminalMaxCols}
+ .theme=${this.terminalTheme}
.initialCols=${this.session?.initialCols || 0}
.initialRows=${this.session?.initialRows || 0}
.disableClick=${this.isMobile && this.useDirectKeyboard}
@@ -1478,10 +1495,12 @@ export class SessionView extends LitElement {
.visible=${this.showWidthSelector}
.terminalMaxCols=${this.terminalMaxCols}
.terminalFontSize=${this.terminalFontSize}
+ .terminalTheme=${this.terminalTheme}
.customWidth=${this.customWidth}
.isMobile=${this.isMobile}
.onWidthSelect=${(width: number) => this.handleWidthSelect(width)}
.onFontSizeChange=${(size: number) => this.handleFontSizeChange(size)}
+ .onThemeChange=${(theme: TerminalThemeId) => this.handleThemeChange(theme)}
.onClose=${() => {
this.showWidthSelector = false;
this.customWidth = '';
diff --git a/web/src/client/components/session-view/terminal-lifecycle-manager.ts b/web/src/client/components/session-view/terminal-lifecycle-manager.ts
index 2d264576..b9f6f008 100644
--- a/web/src/client/components/session-view/terminal-lifecycle-manager.ts
+++ b/web/src/client/components/session-view/terminal-lifecycle-manager.ts
@@ -7,6 +7,7 @@
import { authClient } from '../../services/auth-client.js';
import { createLogger } from '../../utils/logger.js';
+import type { TerminalThemeId } from '../../utils/terminal-themes.js';
import type { Session } from '../session-list.js';
import type { Terminal } from '../terminal.js';
import type { ConnectionManager } from './connection-manager.js';
@@ -32,6 +33,7 @@ export class TerminalLifecycleManager {
private connected = false;
private terminalFontSize = 14;
private terminalMaxCols = 0;
+ private terminalTheme: TerminalThemeId = 'auto';
private resizeTimeout: number | null = null;
private lastResizeWidth = 0;
private lastResizeHeight = 0;
@@ -67,6 +69,10 @@ export class TerminalLifecycleManager {
this.terminalMaxCols = maxCols;
}
+ setTerminalTheme(theme: TerminalThemeId) {
+ this.terminalTheme = theme;
+ }
+
getTerminal(): Terminal | null {
return this.terminal;
}
@@ -114,6 +120,7 @@ export class TerminalLifecycleManager {
this.terminal.fontSize = this.terminalFontSize; // Apply saved font size preference
this.terminal.fitHorizontally = false; // Allow natural terminal sizing
this.terminal.maxCols = this.terminalMaxCols; // Apply saved max width preference
+ this.terminal.theme = this.terminalTheme;
if (this.eventHandlers) {
// Listen for session exit events
diff --git a/web/src/client/components/session-view/width-selector.ts b/web/src/client/components/session-view/width-selector.ts
index 646c08c6..deb1580f 100644
--- a/web/src/client/components/session-view/width-selector.ts
+++ b/web/src/client/components/session-view/width-selector.ts
@@ -8,6 +8,7 @@ import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Z_INDEX } from '../../utils/constants.js';
import { COMMON_TERMINAL_WIDTHS } from '../../utils/terminal-preferences.js';
+import { TERMINAL_THEMES, type TerminalThemeId } from '../../utils/terminal-themes.js';
@customElement('width-selector')
export class WidthSelector extends LitElement {
@@ -19,9 +20,11 @@ export class WidthSelector extends LitElement {
@property({ type: Boolean }) visible = false;
@property({ type: Number }) terminalMaxCols = 0;
@property({ type: Number }) terminalFontSize = 14;
+ @property({ type: String }) terminalTheme: TerminalThemeId = 'auto';
@property({ type: String }) customWidth = '';
@property({ type: Function }) onWidthSelect?: (width: number) => void;
@property({ type: Function }) onFontSizeChange?: (size: number) => void;
+ @property({ type: Function }) onThemeChange?: (theme: TerminalThemeId) => void;
@property({ type: Function }) onClose?: () => void;
@property({ type: Boolean }) isMobile = false;
@@ -128,8 +131,8 @@ export class WidthSelector extends LitElement {
-
Font Size
-
+
Font Size
+
`;
diff --git a/web/src/client/components/terminal.ts b/web/src/client/components/terminal.ts
index 5b06c036..59487078 100644
--- a/web/src/client/components/terminal.ts
+++ b/web/src/client/components/terminal.ts
@@ -16,6 +16,8 @@ import { customElement, property, state } from 'lit/decorators.js';
import { processKeyboardShortcuts } from '../utils/keyboard-shortcut-highlighter.js';
import { createLogger } from '../utils/logger.js';
import { detectMobile } from '../utils/mobile-utils.js';
+import { TerminalPreferencesManager } from '../utils/terminal-preferences.js';
+import { TERMINAL_THEMES, type TerminalThemeId } from '../utils/terminal-themes.js';
import { UrlHighlighter } from '../utils/url-highlighter';
const logger = createLogger('terminal');
@@ -34,6 +36,7 @@ export class Terminal extends LitElement {
@property({ type: Number }) fontSize = 14;
@property({ type: Boolean }) fitHorizontally = false;
@property({ type: Number }) maxCols = 0; // 0 means no limit
+ @property({ type: String }) theme: TerminalThemeId = 'auto';
@property({ type: Boolean }) disableClick = false; // Disable click handling (for mobile direct keyboard)
@property({ type: Boolean }) hideScrollButton = false; // Hide scroll-to-bottom button
@property({ type: Number }) initialCols = 0; // Initial terminal width from session creation
@@ -138,6 +141,8 @@ export class Terminal extends LitElement {
private themeObserver?: MutationObserver;
connectedCallback() {
+ const prefs = TerminalPreferencesManager.getInstance();
+ this.theme = prefs.getTheme();
super.connectedCallback();
// Check for debug mode
@@ -217,6 +222,12 @@ export class Terminal extends LitElement {
this.requestResize('property-change');
}
}
+
+ if (changedProperties.has('theme')) {
+ if (this.terminal) {
+ this.terminal.options.theme = this.getTerminalTheme();
+ }
+ }
}
disconnectedCallback() {
@@ -353,64 +364,14 @@ export class Terminal extends LitElement {
}
private getTerminalTheme() {
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ let themeId = this.theme;
- if (isDark) {
- // Dark theme (original colors)
- return {
- background: '#1e1e1e',
- foreground: '#d4d4d4',
- cursor: 'rgb(var(--color-primary))',
- cursorAccent: '#1e1e1e',
- // Standard 16 colors (0-15) - using proper xterm colors
- black: '#000000',
- red: '#cd0000',
- green: '#00cd00',
- yellow: '#cdcd00',
- blue: '#0000ee',
- magenta: '#cd00cd',
- cyan: '#00cdcd',
- white: '#e5e5e5',
- brightBlack: '#7f7f7f',
- brightRed: '#ff0000',
- brightGreen: '#00ff00',
- brightYellow: '#ffff00',
- brightBlue: '#5c5cff',
- brightMagenta: '#ff00ff',
- brightCyan: '#00ffff',
- brightWhite: '#ffffff',
- };
- } else {
- // Light theme - optimized for readability with softer contrast
- return {
- background: '#f8f9fa', // Slightly off-white for less eye strain
- foreground: '#1f2328', // Dark gray for better readability than pure black
- cursor: 'rgb(var(--color-primary))',
- cursorAccent: '#f8f9fa',
- // Standard 16 colors optimized for light backgrounds
- // Based on GitHub light theme for proven readability
- black: '#24292f',
- red: '#cf222e',
- green: '#1a7f37',
- yellow: '#9a6700',
- blue: '#0969da',
- magenta: '#8250df',
- cyan: '#1b7c83',
- white: '#6e7781',
- brightBlack: '#57606a',
- brightRed: '#da3633',
- brightGreen: '#2da44e',
- brightYellow: '#bf8700',
- brightBlue: '#218bff',
- brightMagenta: '#a475f9',
- brightCyan: '#3192aa',
- brightWhite: '#8c959f',
- // Selection colors for better visibility
- selectionBackground: '#0969da',
- selectionForeground: '#ffffff',
- selectionInactiveBackground: '#e1e4e8',
- };
+ if (themeId === 'auto') {
+ themeId = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
}
+
+ const preset = TERMINAL_THEMES.find((t) => t.id === themeId) || TERMINAL_THEMES[0];
+ return { ...preset.colors };
}
private async initializeTerminal() {
diff --git a/web/src/client/utils/terminal-preferences.ts b/web/src/client/utils/terminal-preferences.ts
index 5222b64f..46ffdd94 100644
--- a/web/src/client/utils/terminal-preferences.ts
+++ b/web/src/client/utils/terminal-preferences.ts
@@ -5,6 +5,7 @@
import { createLogger } from './logger.js';
import { detectMobile } from './mobile-utils.js';
+import type { TerminalThemeId } from './terminal-themes.js';
const logger = createLogger('terminal-preferences');
@@ -12,6 +13,7 @@ export interface TerminalPreferences {
maxCols: number; // 0 means no limit, positive numbers set max width
fontSize: number;
fitHorizontally: boolean;
+ theme: TerminalThemeId;
}
// Common terminal widths
@@ -28,6 +30,7 @@ const DEFAULT_PREFERENCES: TerminalPreferences = {
maxCols: 0, // No limit by default - take as much as possible
fontSize: detectMobile() ? 12 : 14, // 12px on mobile, 14px on desktop
fitHorizontally: false,
+ theme: 'auto',
};
const STORAGE_KEY_TERMINAL_PREFS = 'vibetunnel_terminal_preferences';
@@ -96,6 +99,15 @@ export class TerminalPreferencesManager {
this.savePreferences();
}
+ getTheme(): TerminalThemeId {
+ return this.preferences.theme;
+ }
+
+ setTheme(theme: TerminalThemeId) {
+ this.preferences.theme = theme;
+ this.savePreferences();
+ }
+
getPreferences(): TerminalPreferences {
return { ...this.preferences };
}
diff --git a/web/src/client/utils/terminal-themes.ts b/web/src/client/utils/terminal-themes.ts
new file mode 100644
index 00000000..350d820a
--- /dev/null
+++ b/web/src/client/utils/terminal-themes.ts
@@ -0,0 +1,149 @@
+export type TerminalThemeId = 'auto' | 'light' | 'dark' | 'vscode-dark' | 'dracula' | 'nord';
+
+export interface TerminalTheme {
+ id: TerminalThemeId;
+ name: string;
+ description: string;
+ colors: Record;
+}
+
+export const TERMINAL_THEMES: TerminalTheme[] = [
+ {
+ id: 'dark',
+ name: 'Dark',
+ description: 'VibeTunnel default dark',
+ colors: {
+ background: '#1e1e1e',
+ foreground: '#d4d4d4',
+ cursor: 'rgb(var(--color-primary))',
+ cursorAccent: '#1e1e1e',
+ black: '#000000',
+ red: '#cd0000',
+ green: '#00cd00',
+ yellow: '#cdcd00',
+ blue: '#0000ee',
+ magenta: '#cd00cd',
+ cyan: '#00cdcd',
+ white: '#e5e5e5',
+ brightBlack: '#7f7f7f',
+ brightRed: '#ff0000',
+ brightGreen: '#00ff00',
+ brightYellow: '#ffff00',
+ brightBlue: '#5c5cff',
+ brightMagenta: '#ff00ff',
+ brightCyan: '#00ffff',
+ brightWhite: '#ffffff',
+ },
+ },
+ {
+ id: 'light',
+ name: 'Light',
+ description: 'Soft light theme',
+ colors: {
+ background: '#f8f9fa',
+ foreground: '#1f2328',
+ cursor: 'rgb(var(--color-primary))',
+ cursorAccent: '#f8f9fa',
+ black: '#24292f',
+ red: '#cf222e',
+ green: '#1a7f37',
+ yellow: '#9a6700',
+ blue: '#0969da',
+ magenta: '#8250df',
+ cyan: '#1b7c83',
+ white: '#6e7781',
+ brightBlack: '#57606a',
+ brightRed: '#da3633',
+ brightGreen: '#2da44e',
+ brightYellow: '#bf8700',
+ brightBlue: '#218bff',
+ brightMagenta: '#a475f9',
+ brightCyan: '#3192aa',
+ brightWhite: '#8c959f',
+ selectionBackground: '#0969da',
+ selectionForeground: '#ffffff',
+ selectionInactiveBackground: '#e1e4e8',
+ },
+ },
+ {
+ id: 'vscode-dark',
+ name: 'VS Code Dark',
+ description: 'Popular theme from Visual Studio Code',
+ colors: {
+ background: '#1E1E1E',
+ foreground: '#D4D4D4',
+ cursor: '#AEAFAD',
+ cursorAccent: '#1E1E1E',
+ black: '#000000',
+ red: '#CD3131',
+ green: '#0DBC79',
+ yellow: '#E5E510',
+ blue: '#2472C8',
+ magenta: '#BC3FBC',
+ cyan: '#11A8CD',
+ white: '#E5E5E5',
+ brightBlack: '#666666',
+ brightRed: '#F14C4C',
+ brightGreen: '#23D18B',
+ brightYellow: '#F5F543',
+ brightBlue: '#3B8EEA',
+ brightMagenta: '#D670D6',
+ brightCyan: '#29B8DB',
+ brightWhite: '#FFFFFF',
+ },
+ },
+ {
+ id: 'dracula',
+ name: 'Dracula',
+ description: 'Classic dark theme',
+ colors: {
+ background: '#282A36',
+ foreground: '#F8F8F2',
+ cursor: '#F8F8F2',
+ cursorAccent: '#282A36',
+ black: '#21222C',
+ red: '#FF5555',
+ green: '#50FA7B',
+ yellow: '#F1FA8C',
+ blue: '#BD93F9',
+ magenta: '#FF79C6',
+ cyan: '#8BE9FD',
+ white: '#F8F8F2',
+ brightBlack: '#6272A4',
+ brightRed: '#FF6E6E',
+ brightGreen: '#69FF94',
+ brightYellow: '#FFFFA5',
+ brightBlue: '#D6ACFF',
+ brightMagenta: '#FF92DF',
+ brightCyan: '#A4FFFF',
+ brightWhite: '#FFFFFF',
+ },
+ },
+ {
+ id: 'nord',
+ name: 'Nord',
+ description: 'Arctic north-bluish palette',
+ colors: {
+ background: '#2E3440',
+ foreground: '#D8DEE9',
+ cursor: '#D8DEE9',
+ cursorAccent: '#2E3440',
+ black: '#3B4252',
+ red: '#BF616A',
+ green: '#A3BE8C',
+ yellow: '#EBCB8B',
+ blue: '#81A1C1',
+ magenta: '#B48EAD',
+ cyan: '#88C0D0',
+ white: '#E5E9F0',
+ brightBlack: '#4C566A',
+ brightRed: '#BF616A',
+ brightGreen: '#A3BE8C',
+ brightYellow: '#EBCB8B',
+ brightBlue: '#81A1C1',
+ brightMagenta: '#B48EAD',
+ brightCyan: '#8FBCBB',
+ brightWhite: '#ECEFF4',
+ },
+ },
+];