Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ Thumbs.db

src/main/appConfig.json
.pi/extensions/emdash-hook.ts
.opencode/plugins/emdash-notifications.js
22 changes: 14 additions & 8 deletions src/main/core/conversations/impl/local-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import type { IExecutionContext } from '@main/core/execution-context/types';
import { LocalFileSystem } from '@main/core/fs/impl/local-fs';
import { spawnLocalPty } from '@main/core/pty/local-pty';
import type { Pty } from '@main/core/pty/pty';
import { buildAgentEnv } from '@main/core/pty/pty-env';
import { buildAgentEnv, withThemeColorFgBg } from '@main/core/pty/pty-env';
import { ptySessionRegistry } from '@main/core/pty/pty-session-registry';
import { logLocalPtySpawnWarnings, resolveLocalPtySpawn } from '@main/core/pty/pty-spawn-platform';
import { killTmuxSession, makeTmuxSessionName } from '@main/core/pty/tmux-session-name';
import { resolveEffectiveTheme } from '@main/core/settings/effective-theme';
import { providerOverrideSettings } from '@main/core/settings/provider-settings-service';
import { appSettingsService } from '@main/core/settings/settings-service';
import { events } from '@main/lib/events';
Expand Down Expand Up @@ -124,18 +125,23 @@ export class LocalConversationProvider implements ConversationProvider {
const ptyId = makePtyId(conversation.providerId, conversation.id);
const port = agentHookService.getPort();
const token = agentHookService.getToken();
const theme = await resolveEffectiveTheme();
const pty = spawnLocalPty({
id: sessionId,
command: resolved.command,
args: resolved.args,
cwd: resolved.cwd,
env: {
...buildAgentEnv({
hook: port > 0 ? { port, ptyId, token } : undefined,
providerVars: providerEnv,
}),
...this.taskEnvVars,
},
env: withThemeColorFgBg(
{
...buildAgentEnv({
hook: port > 0 ? { port, ptyId, token } : undefined,
providerVars: providerEnv,
theme,
}),
...this.taskEnvVars,
},
theme
),
cols: initialSize.cols,
rows: initialSize.rows,
});
Expand Down
5 changes: 4 additions & 1 deletion src/main/core/conversations/impl/ssh-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import type { ConversationProvider } from '@main/core/conversations/types';
import type { IExecutionContext } from '@main/core/execution-context/types';
import { SshFileSystem } from '@main/core/fs/impl/ssh-fs';
import type { Pty } from '@main/core/pty/pty';
import { withThemeColorFgBg } from '@main/core/pty/pty-env';
import { ptySessionRegistry } from '@main/core/pty/pty-session-registry';
import { resolveSshCommand } from '@main/core/pty/spawn-utils';
import { openSsh2Pty } from '@main/core/pty/ssh2-pty';
import { killTmuxSession, makeTmuxSessionName } from '@main/core/pty/tmux-session-name';
import { resolveEffectiveTheme } from '@main/core/settings/effective-theme';
import { providerOverrideSettings } from '@main/core/settings/provider-settings-service';
import type { SshClientProxy } from '@main/core/ssh/ssh-client-proxy';
import { events } from '@main/lib/events';
Expand Down Expand Up @@ -115,10 +117,11 @@ export class SshConversationProvider implements ConversationProvider {
};

const profile = await this.proxy.getRemoteShellProfile();
const theme = await resolveEffectiveTheme();
const sshCommand = resolveSshCommand(
'agent',
cfg,
{ ...providerEnv, ...this.taskEnvVars },
withThemeColorFgBg({ ...providerEnv, ...this.taskEnvVars }, theme),
profile
);

Expand Down
30 changes: 30 additions & 0 deletions src/main/core/pty/pty-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,36 @@ describe('pty env Windows shell handling', () => {
expect(env.SHELL).toBe('/bin/bash');
});

it('sets COLORFGBG from theme so termenv TUIs detect light/dark', async () => {
setPlatform('linux');
const { buildAgentEnv, buildTerminalEnv } = await loadPtyEnv();

const lightAgent = buildAgentEnv({ agentApiVars: false, theme: 'emlight' });
const darkAgent = buildAgentEnv({ agentApiVars: false, theme: 'emdark' });
const lightTerm = buildTerminalEnv({ theme: 'emlight' });
const darkTerm = buildTerminalEnv({ theme: 'emdark' });
const noTheme = buildAgentEnv({ agentApiVars: false });

expect(lightAgent.COLORFGBG).toBe('0;15');
expect(darkAgent.COLORFGBG).toBe('15;0');
expect(lightTerm.COLORFGBG).toBe('0;15');
expect(darkTerm.COLORFGBG).toBe('15;0');
expect(noTheme.COLORFGBG).toBeUndefined();
});

it('keeps theme COLORFGBG authoritative over provider env vars', async () => {
setPlatform('linux');
const { buildAgentEnv } = await loadPtyEnv();

const env = buildAgentEnv({
agentApiVars: false,
theme: 'emlight',
providerVars: { COLORFGBG: '15;0' },
});

expect(env.COLORFGBG).toBe('0;15');
});

it('adds provider vars while keeping hook variables authoritative', async () => {
const { buildAgentEnv } = await loadPtyEnv();
const env = buildAgentEnv({
Expand Down
31 changes: 29 additions & 2 deletions src/main/core/pty/pty-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import os from 'node:os';
import { detectSshAuthSock } from '@main/utils/shellEnv';
import { getWindowsEnvValue } from '@main/utils/windows-env';

export type EffectiveTheme = 'emlight' | 'emdark';

// COLORFGBG values use xterm 16-color indices (fg;bg). termenv treats
// bg <= 6 || bg == 8 as dark, so 15;0 → dark and 0;15 → light. Used by
// opencode, charm/crush, and other termenv/lipgloss-based TUIs.
export function colorFgBgFor(theme: EffectiveTheme): string {
return theme === 'emdark' ? '15;0' : '0;15';
}

export function withThemeColorFgBg(
env: Record<string, string>,
theme: EffectiveTheme
): Record<string, string> {
return { ...env, COLORFGBG: colorFgBgFor(theme) };
}

export const AGENT_ENV_VARS = [
'AMP_API_KEY',
'ANTHROPIC_API_KEY',
Expand Down Expand Up @@ -119,6 +135,13 @@ export interface AgentEnvOptions {
* Per-provider variables configured in custom execution settings.
*/
providerVars?: Record<string, string>;

/**
* Effective UI theme. Sets COLORFGBG so termenv-based TUIs (opencode,
* charm/crush, ...) pick the correct colorscheme on launch instead of
* defaulting to dark.
*/
theme?: EffectiveTheme;
}

/**
Expand All @@ -134,7 +157,7 @@ export interface AgentEnvOptions {
* SSH_AUTH_SOCK is injected via the same cached detector used for agents,
* since GUI-launched apps often don't inherit it from the user's login shell.
*/
export function buildTerminalEnv(): Record<string, string> {
export function buildTerminalEnv(options: { theme?: EffectiveTheme } = {}): Record<string, string> {
// Inherit the full process environment, stripping undefined values.
const env: Record<string, string> = {};
for (const [key, val] of Object.entries(process.env)) {
Expand All @@ -146,6 +169,8 @@ export function buildTerminalEnv(): Record<string, string> {
env.COLORTERM = 'truecolor';
env.TERM_PROGRAM = 'emdash';

if (options.theme) env.COLORFGBG = colorFgBgFor(options.theme);

// Ensure SHELL reflects the user's configured shell on POSIX. Native Windows
// shells are selected via ComSpec by the spawn resolver, not SHELL.
if (process.platform !== 'win32') {
Expand Down Expand Up @@ -174,7 +199,7 @@ export function buildTerminalEnv(): Record<string, string> {
* find its own dependencies.
*/
export function buildAgentEnv(options: AgentEnvOptions = {}): Record<string, string> {
const { agentApiVars = true, includeShellVar = false, hook, providerVars } = options;
const { agentApiVars = true, includeShellVar = false, hook, providerVars, theme } = options;

// process.env.PATH is enriched at startup by resolveUserEnv() so it already
// contains the full login-shell PATH (Homebrew, nvm, npm globals, etc.).
Expand Down Expand Up @@ -215,6 +240,8 @@ export function buildAgentEnv(options: AgentEnvOptions = {}): Record<string, str
Object.assign(env, providerVars);
}

if (theme) env.COLORFGBG = colorFgBgFor(theme);

if (hook && hook.port > 0) {
env.EMDASH_HOOK_PORT = String(hook.port);
env.EMDASH_PTY_ID = hook.ptyId;
Expand Down
9 changes: 9 additions & 0 deletions src/main/core/settings/effective-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { nativeTheme } from 'electron';
import type { EffectiveTheme } from '@main/core/pty/pty-env';
import { appSettingsService } from './settings-service';

export async function resolveEffectiveTheme(): Promise<EffectiveTheme> {
const theme = await appSettingsService.get('theme');
if (theme === 'emlight' || theme === 'emdark') return theme;
return nativeTheme.shouldUseDarkColors ? 'emdark' : 'emlight';
}
6 changes: 4 additions & 2 deletions src/main/core/terminals/impl/local-terminal-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Terminal } from '@shared/terminals';
import type { IExecutionContext } from '@main/core/execution-context/types';
import { spawnLocalPty } from '@main/core/pty/local-pty';
import type { Pty } from '@main/core/pty/pty';
import { buildTerminalEnv } from '@main/core/pty/pty-env';
import { buildTerminalEnv, withThemeColorFgBg } from '@main/core/pty/pty-env';
import { ptySessionRegistry } from '@main/core/pty/pty-session-registry';
import {
logLocalPtySpawnWarnings,
Expand All @@ -12,6 +12,7 @@ import {
type PtySpawnIntent,
} from '@main/core/pty/pty-spawn-platform';
import { killTmuxSession, makeTmuxSessionName } from '@main/core/pty/tmux-session-name';
import { resolveEffectiveTheme } from '@main/core/settings/effective-theme';
import { log } from '@main/lib/logger';
import { wireTerminalDevServerWatcher } from '../dev-server-watcher';
import { type LifecycleScriptSpawnRequest, type TerminalProvider } from '../terminal-provider';
Expand Down Expand Up @@ -136,12 +137,13 @@ export class LocalTerminalProvider implements TerminalProvider {
sessionId,
});

const theme = await resolveEffectiveTheme();
const pty = spawnLocalPty({
id: sessionId,
command: resolved.command,
args: resolved.args,
cwd: resolved.cwd,
env: { ...buildTerminalEnv(), ...this.taskEnvVars },
env: withThemeColorFgBg({ ...buildTerminalEnv(), ...this.taskEnvVars }, theme),
cols: initialSize.cols,
rows: initialSize.rows,
});
Expand Down
10 changes: 9 additions & 1 deletion src/main/core/terminals/impl/ssh-terminal-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { makePtySessionId } from '@shared/ptySessionId';
import type { Terminal } from '@shared/terminals';
import type { IExecutionContext } from '@main/core/execution-context/types';
import type { Pty } from '@main/core/pty/pty';
import { withThemeColorFgBg } from '@main/core/pty/pty-env';
import { ptySessionRegistry } from '@main/core/pty/pty-session-registry';
import { resolveSshCommand } from '@main/core/pty/spawn-utils';
import { openSsh2Pty } from '@main/core/pty/ssh2-pty';
import { killTmuxSession, makeTmuxSessionName } from '@main/core/pty/tmux-session-name';
import { resolveEffectiveTheme } from '@main/core/settings/effective-theme';
import type { SshClientProxy } from '@main/core/ssh/ssh-client-proxy';
import {
sshConnectionManager,
Expand Down Expand Up @@ -147,7 +149,13 @@ export class SshTerminalProvider implements TerminalProvider {
};

const profile = await this.proxy.getRemoteShellProfile();
const sshCommand = resolveSshCommand('general', cfg, this.taskEnvVars, profile);
const theme = await resolveEffectiveTheme();
const sshCommand = resolveSshCommand(
'general',
cfg,
withThemeColorFgBg(this.taskEnvVars, theme),
profile
);

const result = await openSsh2Pty(this.proxy.client, {
id: sessionId,
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
--monaco-inactive-selection-bg: color-mix(in srgb, var(--selection) 15.3%, transparent);

/* xterm terminals */
--xterm-bg: var(--background);
--xterm-bg: #fcfcfc;
--xterm-fg: #1f2937;
--xterm-cursor: #1e293b;
--xterm-cursor-accent: #ffffff;
Expand Down Expand Up @@ -302,8 +302,8 @@
--monaco-inactive-selection-bg: color(display-p3 1 1 1 / 0.133);

/* xterm terminals */
--xterm-bg: var(--background-1);
--xterm-fg: var(--foreground);
--xterm-bg: #191919;
--xterm-fg: #eeeeee;
--xterm-cursor: #1e293b;
--xterm-cursor-accent: color(display-p3 0.484 0.484 0.484);
--xterm-selection-bg: color(display-p3 0.033 0.197 0.37);
Expand Down
Loading