Skip to content
Open
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
22 changes: 14 additions & 8 deletions frontend/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NotificationSettings } from './NotificationSettings';
import { useNotifications } from '../hooks/useNotifications';
import { API } from '../utils/api';
import { optIn, capture, captureAndOptOut } from '../services/posthog';
import type { AppConfig, TerminalShortcut } from '../types/config';
import type { PreferredShell, TerminalShortcut } from '../types/config';
import type { WorktreeFileSyncEntry } from '../../../shared/types/worktreeFileSync';
import { DEFAULT_WORKTREE_FILE_SYNC_ENTRIES } from '../../../shared/types/worktreeFileSync';
import { useConfigStore } from '../stores/configStore';
Expand Down Expand Up @@ -46,8 +46,13 @@ interface SettingsProps {
initialSection?: string;
}

type AvailableShell = {
id: PreferredShell;
name: string;
path: string;
};

export function Settings({ isOpen, onClose, initialSection }: SettingsProps) {
const [_config, setConfig] = useState<AppConfig | null>(null);
const [verbose, setVerbose] = useState(false);
const [claudeExecutablePath, setClaudeExecutablePath] = useState('');
const [autoCheckUpdates, setAutoCheckUpdates] = useState(true);
Expand Down Expand Up @@ -75,8 +80,8 @@ export function Settings({ isOpen, onClose, initialSection }: SettingsProps) {
const [activeTab, setActiveTab] = useState<'general' | 'notifications' | 'shortcuts'>('general');
const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
const [previousAnalyticsEnabled, setPreviousAnalyticsEnabled] = useState(true);
const [preferredShell, setPreferredShell] = useState<string>('auto');
const [availableShells, setAvailableShells] = useState<Array<{id: string; name: string; path: string}>>([]);
const [preferredShell, setPreferredShell] = useState<PreferredShell>('auto');
const [availableShells, setAvailableShells] = useState<AvailableShell[]>([]);
const [terminalShortcuts, setTerminalShortcuts] = useState<TerminalShortcut[]>([]);
const [worktreeFileSync, setWorktreeFileSync] = useState<WorktreeFileSyncEntry[]>([]);
const { updateSettings } = useNotifications();
Expand Down Expand Up @@ -138,9 +143,10 @@ export function Settings({ isOpen, onClose, initialSection }: SettingsProps) {
const fetchConfig = async (currentPlatform?: string) => {
try {
const response = await API.config.get();
if (!response.success) throw new Error(response.error || 'Failed to fetch config');
if (!response.success || !response.data) {
throw new Error(response.error || 'Failed to fetch config');
}
const data = response.data;
setConfig(data);
setVerbose(data.verbose || false);
setAutoCheckUpdates(data.autoCheckUpdates !== false); // Default to true
setDevMode(data.devMode || false);
Expand Down Expand Up @@ -174,7 +180,7 @@ export function Settings({ isOpen, onClose, initialSection }: SettingsProps) {
const platformToCheck = currentPlatform || platform;
if (platformToCheck === 'win32') {
const shellsResponse = await API.config.getAvailableShells();
if (shellsResponse.success) {
if (shellsResponse.success && shellsResponse.data) {
setAvailableShells(shellsResponse.data);
}
}
Expand All @@ -185,7 +191,7 @@ export function Settings({ isOpen, onClose, initialSection }: SettingsProps) {

// Load worktree file sync entries
setWorktreeFileSync(data.worktreeFileSync ?? DEFAULT_WORKTREE_FILE_SYNC_ENTRIES);
} catch (err) {
} catch {
setError('Failed to load configuration');
}
};
Expand Down
91 changes: 74 additions & 17 deletions frontend/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CloudVmConfig } from '../../../shared/types/cloud';
import type { RemoteDaemonConfig } from '../../../shared/types/remoteDaemon';
import type { WorktreeFileSyncEntry } from '../../../shared/types/worktreeFileSync';

export interface TerminalShortcut {
Expand All @@ -24,25 +25,52 @@ export interface AnalyticsIdentity {
gitUserName?: string;
}

export interface AnalyticsConfig {
enabled: boolean;
posthogApiKey?: string;
posthogHost?: string;
distinctId?: string;
identitySource?: AnalyticsIdentity['identitySource'];
githubUsername?: string;
githubEmail?: string;
gitEmail?: string;
gitEmailHash?: string;
gitUserName?: string;
}

export interface AppConfig {
gitRepoPath: string;
verbose?: boolean;
anthropicApiKey?: string;
openaiApiKey?: string;
// Legacy fields for backward compatibility
gitRepoPath?: string;
systemPromptAppend?: string;
runScript?: string[];
// Custom claude executable path (for when it's not in PATH)
claudeExecutablePath?: string;
// Permission mode for all sessions
defaultPermissionMode?: 'approve' | 'ignore';
// Default model for new sessions
defaultModel?: string;
// Auto-check for updates
autoCheckUpdates?: boolean;
// Stravu MCP integration
stravuApiKey?: string;
stravuServerUrl?: string;
// Theme preference
theme?: 'light' | 'light-rounded' | 'dark' | 'oled' | 'dusk' | 'dusk-oled' | 'forge' | 'ember' | 'aurora' | 'night-owl' | 'night-owl-oled' | 'terracotta';
// UI scale factor (0.75 to 1.5, default 1.0)
uiScale?: number;
// Notification settings
notifications?: {
playSound: boolean;
enabled: boolean;
};
// Dev mode for debugging
devMode?: boolean;
// Route PTY spawns through an isolated ptyHost UtilityProcess for crash
// isolation. Off by default. Requires app restart to take effect.
usePtyHost?: boolean;
// Additional paths to add to PATH environment variable
additionalPaths?: string[];
// Session creation preferences
sessionCreationPreferences?: {
sessionCount?: number;
toolType?: 'claude' | 'none';
Expand All @@ -59,29 +87,58 @@ export interface AppConfig {
};
// Pane commit footer setting (enabled by default)
enableCommitFooter?: boolean;
// Use interactive mode for Claude CLI (persistent process with stdin instead of spawn-per-message)
useInteractiveMode?: boolean;
// Route PTY spawns through an isolated ptyHost UtilityProcess for crash
// isolation. Off by default. Requires app restart; the supervisor is forked
// once at `app.whenReady`.
usePtyHost?: boolean;
// PostHog analytics settings
analytics?: {
enabled: boolean;
posthogApiKey?: string;
posthogHost?: string;
distinctId?: string;
identitySource?: AnalyticsIdentity['identitySource'];
githubUsername?: string;
githubEmail?: string;
gitEmail?: string;
gitEmailHash?: string;
gitUserName?: string;
};
analytics?: AnalyticsConfig;
// User-defined custom commands for the Add Tool picker
customCommands?: CustomCommand[];
// Terminal shortcuts — hotkey-triggered clipboard paste snippets
terminalShortcuts?: TerminalShortcut[];
// Worktree file sync — files/dirs to copy from main repo into new worktrees
worktreeFileSync?: WorktreeFileSyncEntry[];
// Preferred shell for terminal sessions on Windows
// Preferred shell for Windows terminals
preferredShell?: 'auto' | 'gitbash' | 'powershell' | 'pwsh' | 'cmd';
// Cloud VM settings
cloud?: CloudVmConfig;
// Self-hosted remote daemon settings and saved client profiles
remoteDaemon?: RemoteDaemonConfig;
terminalFontFamily?: string;
terminalFontSize?: number;
}

export type PreferredShell = NonNullable<AppConfig['preferredShell']>;

export interface UpdateConfigRequest {
verbose?: boolean;
anthropicApiKey?: string;
openaiApiKey?: string;
claudeExecutablePath?: string;
systemPromptAppend?: string;
defaultPermissionMode?: 'approve' | 'ignore';
defaultModel?: string;
autoCheckUpdates?: boolean;
stravuApiKey?: string;
stravuServerUrl?: string;
theme?: AppConfig['theme'];
uiScale?: number;
notifications?: AppConfig['notifications'];
devMode?: boolean;
additionalPaths?: string[];
sessionCreationPreferences?: AppConfig['sessionCreationPreferences'];
enableCommitFooter?: boolean;
useInteractiveMode?: boolean;
usePtyHost?: boolean;
analytics?: AnalyticsConfig;
customCommands?: CustomCommand[];
terminalShortcuts?: TerminalShortcut[];
worktreeFileSync?: WorktreeFileSyncEntry[];
preferredShell?: PreferredShell;
cloud?: CloudVmConfig;
terminalFontFamily?: string;
terminalFontSize?: number;
}
22 changes: 20 additions & 2 deletions frontend/src/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
import type { Session, SessionOutput, GitStatus, VersionUpdateInfo } from './session';
import type { Project } from './project';
import type { Folder } from './folder';
import type { AppConfig, UpdateConfigRequest } from './config';
import type { SessionCreationPreferences } from '../stores/sessionPreferencesStore';
import type {
RemoteDaemonClientRecord,
RemoteDaemonClientSettings,
RemoteDaemonConfig,
RemoteDaemonHostConfig,
RemotePaneConnectionProfile,
} from '../../../shared/types/remoteDaemon';
import type { ToolPanel } from '../../../shared/types/panels';
import type { CreateSessionRequest } from './session';
import type { DetectedProjectConfig } from '../../../shared/types/projectConfig';
Expand Down Expand Up @@ -208,14 +216,24 @@ interface ElectronAPI {

// Configuration
config: {
get: () => Promise<IPCResponse>;
update: (updates: Record<string, unknown>) => Promise<IPCResponse>;
get: () => Promise<IPCResponse<AppConfig>>;
update: (updates: UpdateConfigRequest) => Promise<IPCResponse>;
getSessionPreferences: () => Promise<IPCResponse>;
updateSessionPreferences: (preferences: SessionCreationPreferences) => Promise<IPCResponse>;
getAvailableShells: () => Promise<IPCResponse>;
getMonospaceFonts: () => Promise<IPCResponse>;
};

remoteDaemon: {
getConfig: () => Promise<IPCResponse<RemoteDaemonConfig>>;
updateHostConfig: (updates: Partial<RemoteDaemonHostConfig>) => Promise<IPCResponse<RemoteDaemonHostConfig>>;
upsertClientRecord: (record: RemoteDaemonClientRecord) => Promise<IPCResponse<RemoteDaemonClientRecord[]>>;
deleteClientRecord: (clientId: string) => Promise<IPCResponse<RemoteDaemonClientRecord[]>>;
upsertConnectionProfile: (profile: RemotePaneConnectionProfile) => Promise<IPCResponse<RemotePaneConnectionProfile[]>>;
deleteConnectionProfile: (profileId: string) => Promise<IPCResponse<RemoteDaemonClientSettings>>;
updateClientState: (updates: Partial<Pick<RemoteDaemonClientSettings, 'activeProfileId' | 'mode'>>) => Promise<IPCResponse<RemoteDaemonClientSettings>>;
};

// Prompts
prompts: {
getAll: () => Promise<IPCResponse>;
Expand Down
46 changes: 45 additions & 1 deletion frontend/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Utility for making API calls using Electron IPC
import type { CreateSessionRequest } from '../types/session';
import type { Project } from '../types/project';
import type { UpdateConfigRequest } from '../types/config';
import type { SessionCreationPreferences } from '../stores/sessionPreferencesStore';
import type {
RemoteDaemonClientRecord,
RemoteDaemonClientSettings,
RemoteDaemonHostConfig,
RemotePaneConnectionProfile,
} from '../../../shared/types/remoteDaemon';

// Type for IPC response
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter default for flexible API responses
Expand Down Expand Up @@ -443,7 +450,7 @@ export class API {
return window.electronAPI.config.get();
},

async update(updates: Record<string, unknown>) {
async update(updates: UpdateConfigRequest) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.config.update(updates);
},
Expand All @@ -464,6 +471,43 @@ export class API {
},
};

static remoteDaemon = {
async getConfig() {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.getConfig();
},

async updateHostConfig(updates: Partial<RemoteDaemonHostConfig>) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.updateHostConfig(updates);
},

async upsertClientRecord(record: RemoteDaemonClientRecord) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.upsertClientRecord(record);
},

async deleteClientRecord(clientId: string) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.deleteClientRecord(clientId);
},

async upsertConnectionProfile(profile: RemotePaneConnectionProfile) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.upsertConnectionProfile(profile);
},

async deleteConnectionProfile(profileId: string) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.deleteConnectionProfile(profileId);
},

async updateClientState(updates: Partial<Pick<RemoteDaemonClientSettings, 'activeProfileId' | 'mode'>>) {
if (!isElectron()) throw new Error('Electron API not available');
return window.electronAPI.remoteDaemon.updateClientState(updates);
},
};

// Prompts
static prompts = {
async getAll() {
Expand Down
7 changes: 4 additions & 3 deletions main/src/ipc/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { IpcMain } from 'electron';
import { execFile } from 'child_process';
import type { AppServices } from './types';
import type { AppConfig, UpdateConfigRequest } from '../types/config';
import { ShellDetector } from '../utils/shellDetector';

export function registerConfigHandlers(ipcMain: IpcMain, { configManager, claudeCodeManager, getMainWindow }: AppServices): void {
ipcMain.handle('config:get', async () => {
ipcMain.handle('config:get', async (): Promise<{ success: boolean; data?: AppConfig; error?: string }> => {
try {
// Always reload from disk to pick up external changes (e.g., from setup scripts)
const config = await configManager.reloadFromDisk();
Expand All @@ -15,7 +16,7 @@ export function registerConfigHandlers(ipcMain: IpcMain, { configManager, claude
}
});

ipcMain.handle('config:update', async (_event, updates: import('../types/config').UpdateConfigRequest) => {
ipcMain.handle('config:update', async (_event, updates: UpdateConfigRequest) => {
try {
// Check if Claude path is being updated
const oldConfig = configManager.getConfig();
Expand Down Expand Up @@ -165,4 +166,4 @@ export function registerConfigHandlers(ipcMain: IpcMain, { configManager, claude
return { success: false, data: [] };
}
});
}
}
2 changes: 2 additions & 0 deletions main/src/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { registerEditorPanelHandlers } from './editorPanel';
import { registerNimbalystHandlers } from './nimbalyst';
import { registerSpotlightHandlers } from './spotlight';
import { registerCloudHandlers } from './cloud';
import { registerRemoteDaemonHandlers } from './remoteDaemon';
import { registerClipboardHandlers } from './clipboard';
import { registerResourceMonitorHandlers } from './resourceMonitor';
import { registerOnboardingHandlers } from './onboarding';
Expand Down Expand Up @@ -48,6 +49,7 @@ export function registerIpcHandlers(services: AppServices): PaneCommandRegistry
registerNimbalystHandlers(ipcMain, services);
registerSpotlightHandlers(ipcMain, services);
registerCloudHandlers(ipcMain, services);
registerRemoteDaemonHandlers(ipcMain, services);
registerClipboardHandlers(ipcMain, services);
registerResourceMonitorHandlers(ipcMain, services, commandRegistry);
registerOnboardingHandlers(ipcMain, services);
Expand Down
Loading