| |
| import { feature } from 'bun:bundle' |
| import { |
| Box, |
| Text, |
| useTheme, |
| useThemeSetting, |
| useTerminalFocus, |
| } from '../../ink.js' |
| import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' |
| import * as React from 'react' |
| import { useState, useCallback } from 'react' |
| import { |
| useKeybinding, |
| useKeybindings, |
| } from '../../keybindings/useKeybinding.js' |
| import figures from 'figures' |
| import { |
| type GlobalConfig, |
| saveGlobalConfig, |
| getCurrentProjectConfig, |
| type OutputStyle, |
| } from '../../utils/config.js' |
| import { normalizeApiKeyForConfig } from '../../utils/authPortable.js' |
| import { |
| getGlobalConfig, |
| getAutoUpdaterDisabledReason, |
| formatAutoUpdaterDisabledReason, |
| getRemoteControlAtStartup, |
| } from '../../utils/config.js' |
| import chalk from 'chalk' |
| import { |
| permissionModeTitle, |
| permissionModeFromString, |
| toExternalPermissionMode, |
| isExternalPermissionMode, |
| EXTERNAL_PERMISSION_MODES, |
| PERMISSION_MODES, |
| type ExternalPermissionMode, |
| type PermissionMode, |
| } from '../../utils/permissions/PermissionMode.js' |
| import { |
| getAutoModeEnabledState, |
| hasAutoModeOptInAnySource, |
| transitionPlanAutoMode, |
| } from '../../utils/permissions/permissionSetup.js' |
| import { logError } from '../../utils/log.js' |
| import { |
| logEvent, |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } from 'src/services/analytics/index.js' |
| import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js' |
| import { ThemePicker } from '../ThemePicker.js' |
| import { |
| useAppState, |
| useSetAppState, |
| useAppStateStore, |
| } from '../../state/AppState.js' |
| import { ModelPicker } from '../ModelPicker.js' |
| import { |
| modelDisplayString, |
| isOpus1mMergeEnabled, |
| } from '../../utils/model/model.js' |
| import { isBilledAsExtraUsage } from '../../utils/extraUsage.js' |
| import { ClaudeMdExternalIncludesDialog } from '../ClaudeMdExternalIncludesDialog.js' |
| import { |
| ChannelDowngradeDialog, |
| type ChannelDowngradeChoice, |
| } from '../ChannelDowngradeDialog.js' |
| import { Dialog } from '../design-system/Dialog.js' |
| import { Select } from '../CustomSelect/index.js' |
| import { OutputStylePicker } from '../OutputStylePicker.js' |
| import { LanguagePicker } from '../LanguagePicker.js' |
| import { |
| getExternalClaudeMdIncludes, |
| getMemoryFiles, |
| hasExternalClaudeMdIncludes, |
| } from 'src/utils/claudemd.js' |
| import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' |
| import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' |
| import { Byline } from '../design-system/Byline.js' |
| import { useTabHeaderFocus } from '../design-system/Tabs.js' |
| import { useIsInsideModal } from '../../context/modalContext.js' |
| import { SearchBox } from '../SearchBox.js' |
| import { |
| isSupportedTerminal, |
| hasAccessToIDEExtensionDiffFeature, |
| } from '../../utils/ide.js' |
| import { |
| getInitialSettings, |
| getSettingsForSource, |
| updateSettingsForSource, |
| } from '../../utils/settings/settings.js' |
| import { getUserMsgOptIn, setUserMsgOptIn } from '../../bootstrap/state.js' |
| import { DEFAULT_OUTPUT_STYLE_NAME } from 'src/constants/outputStyles.js' |
| import { isEnvTruthy, isRunningOnHomespace } from 'src/utils/envUtils.js' |
| import type { |
| LocalJSXCommandContext, |
| CommandResultDisplay, |
| } from '../../commands.js' |
| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' |
| import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' |
| import { |
| getCliTeammateModeOverride, |
| clearCliTeammateModeOverride, |
| } from '../../utils/swarm/backends/teammateModeSnapshot.js' |
| import { getHardcodedTeammateModelFallback } from '../../utils/swarm/teammateModel.js' |
| import { useSearchInput } from '../../hooks/useSearchInput.js' |
| import { useTerminalSize } from '../../hooks/useTerminalSize.js' |
| import { |
| clearFastModeCooldown, |
| FAST_MODE_MODEL_DISPLAY, |
| isFastModeAvailable, |
| isFastModeEnabled, |
| getFastModeModel, |
| isFastModeSupportedByModel, |
| } from '../../utils/fastMode.js' |
| import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js' |
|
|
| type Props = { |
| onClose: ( |
| result?: string, |
| options?: { display?: CommandResultDisplay }, |
| ) => void |
| context: LocalJSXCommandContext |
| setTabsHidden: (hidden: boolean) => void |
| onIsSearchModeChange?: (inSearchMode: boolean) => void |
| contentHeight?: number |
| } |
|
|
| type SettingBase = |
| | { |
| id: string |
| label: string |
| } |
| | { |
| id: string |
| label: React.ReactNode |
| searchText: string |
| } |
|
|
| type Setting = |
| | (SettingBase & { |
| value: boolean |
| onChange(value: boolean): void |
| type: 'boolean' |
| }) |
| | (SettingBase & { |
| value: string |
| options: string[] |
| onChange(value: string): void |
| type: 'enum' |
| }) |
| | (SettingBase & { |
| |
| |
| value: string |
| onChange(value: string): void |
| type: 'managedEnum' |
| }) |
|
|
| type SubMenu = |
| | 'Theme' |
| | 'Model' |
| | 'TeammateModel' |
| | 'ExternalIncludes' |
| | 'OutputStyle' |
| | 'ChannelDowngrade' |
| | 'Language' |
| | 'EnableAutoUpdates' |
| export function Config({ |
| onClose, |
| context, |
| setTabsHidden, |
| onIsSearchModeChange, |
| contentHeight, |
| }: Props): React.ReactNode { |
| const { headerFocused, focusHeader } = useTabHeaderFocus() |
| const insideModal = useIsInsideModal() |
| const [, setTheme] = useTheme() |
| const themeSetting = useThemeSetting() |
| const [globalConfig, setGlobalConfig] = useState(getGlobalConfig()) |
| const initialConfig = React.useRef(getGlobalConfig()) |
| const [settingsData, setSettingsData] = useState(getInitialSettings()) |
| const initialSettingsData = React.useRef(getInitialSettings()) |
| const [currentOutputStyle, setCurrentOutputStyle] = useState<OutputStyle>( |
| settingsData?.outputStyle || DEFAULT_OUTPUT_STYLE_NAME, |
| ) |
| const initialOutputStyle = React.useRef(currentOutputStyle) |
| const [currentLanguage, setCurrentLanguage] = useState<string | undefined>( |
| settingsData?.language, |
| ) |
| const initialLanguage = React.useRef(currentLanguage) |
| const [selectedIndex, setSelectedIndex] = useState(0) |
| const [scrollOffset, setScrollOffset] = useState(0) |
| const [isSearchMode, setIsSearchMode] = useState(true) |
| const isTerminalFocused = useTerminalFocus() |
| const { rows } = useTerminalSize() |
| |
| |
| |
| |
| const paneCap = contentHeight ?? Math.min(Math.floor(rows * 0.8), 30) |
| const maxVisible = Math.max(5, paneCap - 10) |
| const mainLoopModel = useAppState(s => s.mainLoopModel) |
| const verbose = useAppState(s => s.verbose) |
| const thinkingEnabled = useAppState(s => s.thinkingEnabled) |
| const isFastMode = useAppState(s => |
| isFastModeEnabled() ? s.fastMode : false, |
| ) |
| const promptSuggestionEnabled = useAppState(s => s.promptSuggestionEnabled) |
| |
| |
| |
| const showAutoInDefaultModePicker = feature('TRANSCRIPT_CLASSIFIER') |
| ? hasAutoModeOptInAnySource() || getAutoModeEnabledState() === 'enabled' |
| : false |
| |
| |
| |
| |
| |
| const showDefaultViewPicker = |
| feature('KAIROS') || feature('KAIROS_BRIEF') |
| ? ( |
| require('../../tools/BriefTool/BriefTool.js') as typeof import('../../tools/BriefTool/BriefTool.js') |
| ).isBriefEntitled() |
| : false |
| |
| const setAppState = useSetAppState() |
| const [changes, setChanges] = useState<{ [key: string]: unknown }>({}) |
| const initialThinkingEnabled = React.useRef(thinkingEnabled) |
| |
| |
| |
| |
| |
| |
| const [initialLocalSettings] = useState(() => |
| getSettingsForSource('localSettings'), |
| ) |
| const [initialUserSettings] = useState(() => |
| getSettingsForSource('userSettings'), |
| ) |
| const initialThemeSetting = React.useRef(themeSetting) |
| |
| const store = useAppStateStore() |
| const [initialAppState] = useState(() => { |
| const s = store.getState() |
| return { |
| mainLoopModel: s.mainLoopModel, |
| mainLoopModelForSession: s.mainLoopModelForSession, |
| verbose: s.verbose, |
| thinkingEnabled: s.thinkingEnabled, |
| fastMode: s.fastMode, |
| promptSuggestionEnabled: s.promptSuggestionEnabled, |
| isBriefOnly: s.isBriefOnly, |
| replBridgeEnabled: s.replBridgeEnabled, |
| replBridgeOutboundOnly: s.replBridgeOutboundOnly, |
| settings: s.settings, |
| } |
| }) |
| |
| |
| |
| |
| |
| const [initialUserMsgOptIn] = useState(() => getUserMsgOptIn()) |
| |
| |
| const isDirty = React.useRef(false) |
| const [showThinkingWarning, setShowThinkingWarning] = useState(false) |
| const [showSubmenu, setShowSubmenu] = useState<SubMenu | null>(null) |
| const { |
| query: searchQuery, |
| setQuery: setSearchQuery, |
| cursorOffset: searchCursorOffset, |
| } = useSearchInput({ |
| isActive: isSearchMode && showSubmenu === null && !headerFocused, |
| onExit: () => setIsSearchMode(false), |
| onExitUp: focusHeader, |
| |
| |
| passthroughCtrlKeys: ['c', 'd'], |
| }) |
|
|
| |
| |
| |
| const ownsEsc = isSearchMode && !headerFocused |
| React.useEffect(() => { |
| onIsSearchModeChange?.(ownsEsc) |
| }, [ownsEsc, onIsSearchModeChange]) |
|
|
| const isConnectedToIde = hasAccessToIDEExtensionDiffFeature( |
| context.options.mcpClients, |
| ) |
|
|
| const isFileCheckpointingAvailable = !isEnvTruthy( |
| process.env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING, |
| ) |
|
|
| const memoryFiles = React.use(getMemoryFiles(true)) |
| const shouldShowExternalIncludesToggle = |
| hasExternalClaudeMdIncludes(memoryFiles) |
|
|
| const autoUpdaterDisabledReason = getAutoUpdaterDisabledReason() |
|
|
| function onChangeMainModelConfig(value: string | null): void { |
| const previousModel = mainLoopModel |
| logEvent('tengu_config_model_changed', { |
| from_model: |
| previousModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| to_model: |
| value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| setAppState(prev => ({ |
| ...prev, |
| mainLoopModel: value, |
| mainLoopModelForSession: null, |
| })) |
| setChanges(prev => { |
| const valStr = |
| modelDisplayString(value) + |
| (isBilledAsExtraUsage(value, false, isOpus1mMergeEnabled()) |
| ? ' · Billed as extra usage' |
| : '') |
| if ('model' in prev) { |
| const { model, ...rest } = prev |
| return { ...rest, model: valStr } |
| } |
| return { ...prev, model: valStr } |
| }) |
| } |
|
|
| function onChangeVerbose(value: boolean): void { |
| |
| saveGlobalConfig(current => ({ ...current, verbose: value })) |
| setGlobalConfig({ ...getGlobalConfig(), verbose: value }) |
|
|
| |
| setAppState(prev => ({ |
| ...prev, |
| verbose: value, |
| })) |
| setChanges(prev => { |
| if ('verbose' in prev) { |
| const { verbose, ...rest } = prev |
| return rest |
| } |
| return { ...prev, verbose: value } |
| }) |
| } |
|
|
| |
| const settingsItems: Setting[] = [ |
| |
| { |
| id: 'autoCompactEnabled', |
| label: 'Auto-compact', |
| value: globalConfig.autoCompactEnabled, |
| type: 'boolean' as const, |
| onChange(autoCompactEnabled: boolean) { |
| saveGlobalConfig(current => ({ ...current, autoCompactEnabled })) |
| setGlobalConfig({ ...getGlobalConfig(), autoCompactEnabled }) |
| logEvent('tengu_auto_compact_setting_changed', { |
| enabled: autoCompactEnabled, |
| }) |
| }, |
| }, |
| { |
| id: 'spinnerTipsEnabled', |
| label: 'Show tips', |
| value: settingsData?.spinnerTipsEnabled ?? true, |
| type: 'boolean' as const, |
| onChange(spinnerTipsEnabled: boolean) { |
| updateSettingsForSource('localSettings', { |
| spinnerTipsEnabled, |
| }) |
| |
| setSettingsData(prev => ({ |
| ...prev, |
| spinnerTipsEnabled, |
| })) |
| logEvent('tengu_tips_setting_changed', { |
| enabled: spinnerTipsEnabled, |
| }) |
| }, |
| }, |
| { |
| id: 'prefersReducedMotion', |
| label: 'Reduce motion', |
| value: settingsData?.prefersReducedMotion ?? false, |
| type: 'boolean' as const, |
| onChange(prefersReducedMotion: boolean) { |
| updateSettingsForSource('localSettings', { |
| prefersReducedMotion, |
| }) |
| setSettingsData(prev => ({ |
| ...prev, |
| prefersReducedMotion, |
| })) |
| |
| setAppState(prev => ({ |
| ...prev, |
| settings: { ...prev.settings, prefersReducedMotion }, |
| })) |
| logEvent('tengu_reduce_motion_setting_changed', { |
| enabled: prefersReducedMotion, |
| }) |
| }, |
| }, |
| { |
| id: 'thinkingEnabled', |
| label: 'Thinking mode', |
| value: thinkingEnabled ?? true, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| setAppState(prev => ({ ...prev, thinkingEnabled: enabled })) |
| updateSettingsForSource('userSettings', { |
| alwaysThinkingEnabled: enabled ? undefined : false, |
| }) |
| logEvent('tengu_thinking_toggled', { enabled }) |
| }, |
| }, |
| |
| ...(isFastModeEnabled() && isFastModeAvailable() |
| ? [ |
| { |
| id: 'fastMode', |
| label: `Fast mode (${FAST_MODE_MODEL_DISPLAY} only)`, |
| value: !!isFastMode, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| clearFastModeCooldown() |
| updateSettingsForSource('userSettings', { |
| fastMode: enabled ? true : undefined, |
| }) |
| if (enabled) { |
| setAppState(prev => ({ |
| ...prev, |
| mainLoopModel: getFastModeModel(), |
| mainLoopModelForSession: null, |
| fastMode: true, |
| })) |
| setChanges(prev => ({ |
| ...prev, |
| model: getFastModeModel(), |
| 'Fast mode': 'ON', |
| })) |
| } else { |
| setAppState(prev => ({ |
| ...prev, |
| fastMode: false, |
| })) |
| setChanges(prev => ({ ...prev, 'Fast mode': 'OFF' })) |
| } |
| }, |
| }, |
| ] |
| : []), |
| ...(getFeatureValue_CACHED_MAY_BE_STALE('tengu_chomp_inflection', false) |
| ? [ |
| { |
| id: 'promptSuggestionEnabled', |
| label: 'Prompt suggestions', |
| value: promptSuggestionEnabled, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| setAppState(prev => ({ |
| ...prev, |
| promptSuggestionEnabled: enabled, |
| })) |
| updateSettingsForSource('userSettings', { |
| promptSuggestionEnabled: enabled ? undefined : false, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| |
| ...("external" === 'ant' |
| ? [ |
| { |
| id: 'speculationEnabled', |
| label: 'Speculative execution', |
| value: globalConfig.speculationEnabled ?? true, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| saveGlobalConfig(current => { |
| if (current.speculationEnabled === enabled) return current |
| return { |
| ...current, |
| speculationEnabled: enabled, |
| } |
| }) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| speculationEnabled: enabled, |
| }) |
| logEvent('tengu_speculation_setting_changed', { |
| enabled, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| ...(isFileCheckpointingAvailable |
| ? [ |
| { |
| id: 'fileCheckpointingEnabled', |
| label: 'Rewind code (checkpoints)', |
| value: globalConfig.fileCheckpointingEnabled, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| fileCheckpointingEnabled: enabled, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| fileCheckpointingEnabled: enabled, |
| }) |
| logEvent('tengu_file_history_snapshots_setting_changed', { |
| enabled: enabled, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'verbose', |
| label: 'Verbose output', |
| value: verbose, |
| type: 'boolean', |
| onChange: onChangeVerbose, |
| }, |
| { |
| id: 'terminalProgressBarEnabled', |
| label: 'Terminal progress bar', |
| value: globalConfig.terminalProgressBarEnabled, |
| type: 'boolean' as const, |
| onChange(terminalProgressBarEnabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| terminalProgressBarEnabled, |
| })) |
| setGlobalConfig({ ...getGlobalConfig(), terminalProgressBarEnabled }) |
| logEvent('tengu_terminal_progress_bar_setting_changed', { |
| enabled: terminalProgressBarEnabled, |
| }) |
| }, |
| }, |
| ...(getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_sidebar', false) |
| ? [ |
| { |
| id: 'showStatusInTerminalTab', |
| label: 'Show status in terminal tab', |
| value: globalConfig.showStatusInTerminalTab ?? false, |
| type: 'boolean' as const, |
| onChange(showStatusInTerminalTab: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| showStatusInTerminalTab, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| showStatusInTerminalTab, |
| }) |
| logEvent('tengu_terminal_tab_status_setting_changed', { |
| enabled: showStatusInTerminalTab, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'showTurnDuration', |
| label: 'Show turn duration', |
| value: globalConfig.showTurnDuration, |
| type: 'boolean' as const, |
| onChange(showTurnDuration: boolean) { |
| saveGlobalConfig(current => ({ ...current, showTurnDuration })) |
| setGlobalConfig({ ...getGlobalConfig(), showTurnDuration }) |
| logEvent('tengu_show_turn_duration_setting_changed', { |
| enabled: showTurnDuration, |
| }) |
| }, |
| }, |
| { |
| id: 'defaultPermissionMode', |
| label: 'Default permission mode', |
| value: settingsData?.permissions?.defaultMode || 'default', |
| options: (() => { |
| const priorityOrder: PermissionMode[] = ['default', 'plan'] |
| const allModes: readonly PermissionMode[] = feature( |
| 'TRANSCRIPT_CLASSIFIER', |
| ) |
| ? PERMISSION_MODES |
| : EXTERNAL_PERMISSION_MODES |
| const excluded: PermissionMode[] = ['bypassPermissions'] |
| if (feature('TRANSCRIPT_CLASSIFIER') && !showAutoInDefaultModePicker) { |
| excluded.push('auto') |
| } |
| return [ |
| ...priorityOrder, |
| ...allModes.filter( |
| m => !priorityOrder.includes(m) && !excluded.includes(m), |
| ), |
| ] |
| })(), |
| type: 'enum' as const, |
| onChange(mode: string) { |
| const parsedMode = permissionModeFromString(mode) |
| |
| const validatedMode = isExternalPermissionMode(parsedMode) |
| ? toExternalPermissionMode(parsedMode) |
| : parsedMode |
| const result = updateSettingsForSource('userSettings', { |
| permissions: { |
| ...settingsData?.permissions, |
| defaultMode: validatedMode as ExternalPermissionMode, |
| }, |
| }) |
|
|
| if (result.error) { |
| logError(result.error) |
| return |
| } |
|
|
| |
| |
| |
| |
| setSettingsData(prev => ({ |
| ...prev, |
| permissions: { |
| ...prev?.permissions, |
| defaultMode: validatedMode as (typeof PERMISSION_MODES)[number], |
| }, |
| })) |
| |
| setChanges(prev => ({ ...prev, defaultPermissionMode: mode })) |
| logEvent('tengu_config_changed', { |
| setting: |
| 'defaultPermissionMode' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| value: |
| mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ...(feature('TRANSCRIPT_CLASSIFIER') && showAutoInDefaultModePicker |
| ? [ |
| { |
| id: 'useAutoModeDuringPlan', |
| label: 'Use auto mode during plan', |
| value: |
| (settingsData as { useAutoModeDuringPlan?: boolean } | undefined) |
| ?.useAutoModeDuringPlan ?? true, |
| type: 'boolean' as const, |
| onChange(useAutoModeDuringPlan: boolean) { |
| updateSettingsForSource('userSettings', { |
| useAutoModeDuringPlan, |
| }) |
| setSettingsData(prev => ({ |
| ...prev, |
| useAutoModeDuringPlan, |
| })) |
| |
| |
| |
| setAppState(prev => { |
| const next = transitionPlanAutoMode(prev.toolPermissionContext) |
| if (next === prev.toolPermissionContext) return prev |
| return { ...prev, toolPermissionContext: next } |
| }) |
| setChanges(prev => ({ |
| ...prev, |
| 'Use auto mode during plan': useAutoModeDuringPlan, |
| })) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'respectGitignore', |
| label: 'Respect .gitignore in file picker', |
| value: globalConfig.respectGitignore, |
| type: 'boolean' as const, |
| onChange(respectGitignore: boolean) { |
| saveGlobalConfig(current => ({ ...current, respectGitignore })) |
| setGlobalConfig({ ...getGlobalConfig(), respectGitignore }) |
| logEvent('tengu_respect_gitignore_setting_changed', { |
| enabled: respectGitignore, |
| }) |
| }, |
| }, |
| { |
| id: 'copyFullResponse', |
| label: 'Always copy full response (skip /copy picker)', |
| value: globalConfig.copyFullResponse, |
| type: 'boolean' as const, |
| onChange(copyFullResponse: boolean) { |
| saveGlobalConfig(current => ({ ...current, copyFullResponse })) |
| setGlobalConfig({ ...getGlobalConfig(), copyFullResponse }) |
| logEvent('tengu_config_changed', { |
| setting: |
| 'copyFullResponse' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| value: String( |
| copyFullResponse, |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| |
| |
| ...(isFullscreenEnvEnabled() |
| ? [ |
| { |
| id: 'copyOnSelect', |
| label: 'Copy on select', |
| value: globalConfig.copyOnSelect ?? true, |
| type: 'boolean' as const, |
| onChange(copyOnSelect: boolean) { |
| saveGlobalConfig(current => ({ ...current, copyOnSelect })) |
| setGlobalConfig({ ...getGlobalConfig(), copyOnSelect }) |
| logEvent('tengu_config_changed', { |
| setting: |
| 'copyOnSelect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| value: String( |
| copyOnSelect, |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| |
| autoUpdaterDisabledReason |
| ? { |
| id: 'autoUpdatesChannel', |
| label: 'Auto-update channel', |
| value: 'disabled', |
| type: 'managedEnum' as const, |
| onChange() {}, |
| } |
| : { |
| id: 'autoUpdatesChannel', |
| label: 'Auto-update channel', |
| value: settingsData?.autoUpdatesChannel ?? 'latest', |
| type: 'managedEnum' as const, |
| onChange() { |
| |
| }, |
| }, |
| { |
| id: 'theme', |
| label: 'Theme', |
| value: themeSetting, |
| type: 'managedEnum', |
| onChange: setTheme, |
| }, |
| { |
| id: 'notifChannel', |
| label: |
| feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') |
| ? 'Local notifications' |
| : 'Notifications', |
| value: globalConfig.preferredNotifChannel, |
| options: [ |
| 'auto', |
| 'iterm2', |
| 'terminal_bell', |
| 'iterm2_with_bell', |
| 'kitty', |
| 'ghostty', |
| 'notifications_disabled', |
| ], |
| type: 'enum', |
| onChange(notifChannel: GlobalConfig['preferredNotifChannel']) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| preferredNotifChannel: notifChannel, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| preferredNotifChannel: notifChannel, |
| }) |
| }, |
| }, |
| ...(feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') |
| ? [ |
| { |
| id: 'taskCompleteNotifEnabled', |
| label: 'Push when idle', |
| value: globalConfig.taskCompleteNotifEnabled ?? false, |
| type: 'boolean' as const, |
| onChange(taskCompleteNotifEnabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| taskCompleteNotifEnabled, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| taskCompleteNotifEnabled, |
| }) |
| }, |
| }, |
| { |
| id: 'inputNeededNotifEnabled', |
| label: 'Push when input needed', |
| value: globalConfig.inputNeededNotifEnabled ?? false, |
| type: 'boolean' as const, |
| onChange(inputNeededNotifEnabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| inputNeededNotifEnabled, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| inputNeededNotifEnabled, |
| }) |
| }, |
| }, |
| { |
| id: 'agentPushNotifEnabled', |
| label: 'Push when Claude decides', |
| value: globalConfig.agentPushNotifEnabled ?? false, |
| type: 'boolean' as const, |
| onChange(agentPushNotifEnabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| agentPushNotifEnabled, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| agentPushNotifEnabled, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'outputStyle', |
| label: 'Output style', |
| value: currentOutputStyle, |
| type: 'managedEnum' as const, |
| onChange: () => {}, |
| }, |
| ...(showDefaultViewPicker |
| ? [ |
| { |
| id: 'defaultView', |
| label: 'What you see by default', |
| |
| |
| |
| value: |
| settingsData?.defaultView === undefined |
| ? 'default' |
| : String(settingsData.defaultView), |
| options: ['transcript', 'chat', 'default'], |
| type: 'enum' as const, |
| onChange(selected: string) { |
| const defaultView = |
| selected === 'default' |
| ? undefined |
| : (selected as 'chat' | 'transcript') |
| updateSettingsForSource('localSettings', { defaultView }) |
| setSettingsData(prev => ({ ...prev, defaultView })) |
| const nextBrief = defaultView === 'chat' |
| setAppState(prev => { |
| if (prev.isBriefOnly === nextBrief) return prev |
| return { ...prev, isBriefOnly: nextBrief } |
| }) |
| |
| |
| |
| |
| setUserMsgOptIn(nextBrief) |
| setChanges(prev => ({ ...prev, 'Default view': selected })) |
| logEvent('tengu_default_view_setting_changed', { |
| value: (defaultView ?? |
| 'unset') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'language', |
| label: 'Language', |
| value: currentLanguage ?? 'Default (English)', |
| type: 'managedEnum' as const, |
| onChange: () => {}, |
| }, |
| { |
| id: 'editorMode', |
| label: 'Editor mode', |
| |
| value: |
| globalConfig.editorMode === 'emacs' |
| ? 'normal' |
| : globalConfig.editorMode || 'normal', |
| options: ['normal', 'vim'], |
| type: 'enum', |
| onChange(value: string) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| editorMode: value as GlobalConfig['editorMode'], |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| editorMode: value as GlobalConfig['editorMode'], |
| }) |
|
|
| logEvent('tengu_editor_mode_changed', { |
| mode: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| { |
| id: 'prStatusFooterEnabled', |
| label: 'Show PR status footer', |
| value: globalConfig.prStatusFooterEnabled ?? true, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| saveGlobalConfig(current => { |
| if (current.prStatusFooterEnabled === enabled) return current |
| return { |
| ...current, |
| prStatusFooterEnabled: enabled, |
| } |
| }) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| prStatusFooterEnabled: enabled, |
| }) |
| logEvent('tengu_pr_status_footer_setting_changed', { |
| enabled, |
| }) |
| }, |
| }, |
| { |
| id: 'model', |
| label: 'Model', |
| value: mainLoopModel === null ? 'Default (recommended)' : mainLoopModel, |
| type: 'managedEnum' as const, |
| onChange: onChangeMainModelConfig, |
| }, |
| ...(isConnectedToIde |
| ? [ |
| { |
| id: 'diffTool', |
| label: 'Diff tool', |
| value: globalConfig.diffTool ?? 'auto', |
| options: ['terminal', 'auto'], |
| type: 'enum' as const, |
| onChange(diffTool: string) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| diffTool: diffTool as GlobalConfig['diffTool'], |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| diffTool: diffTool as GlobalConfig['diffTool'], |
| }) |
|
|
| logEvent('tengu_diff_tool_changed', { |
| tool: diffTool as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| ...(!isSupportedTerminal() |
| ? [ |
| { |
| id: 'autoConnectIde', |
| label: 'Auto-connect to IDE (external terminal)', |
| value: globalConfig.autoConnectIde ?? false, |
| type: 'boolean' as const, |
| onChange(autoConnectIde: boolean) { |
| saveGlobalConfig(current => ({ ...current, autoConnectIde })) |
| setGlobalConfig({ ...getGlobalConfig(), autoConnectIde }) |
|
|
| logEvent('tengu_auto_connect_ide_changed', { |
| enabled: autoConnectIde, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| ...(isSupportedTerminal() |
| ? [ |
| { |
| id: 'autoInstallIdeExtension', |
| label: 'Auto-install IDE extension', |
| value: globalConfig.autoInstallIdeExtension ?? true, |
| type: 'boolean' as const, |
| onChange(autoInstallIdeExtension: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| autoInstallIdeExtension, |
| })) |
| setGlobalConfig({ ...getGlobalConfig(), autoInstallIdeExtension }) |
|
|
| logEvent('tengu_auto_install_ide_extension_changed', { |
| enabled: autoInstallIdeExtension, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| ] |
| : []), |
| { |
| id: 'claudeInChromeDefaultEnabled', |
| label: 'Claude in Chrome enabled by default', |
| value: globalConfig.claudeInChromeDefaultEnabled ?? true, |
| type: 'boolean' as const, |
| onChange(enabled: boolean) { |
| saveGlobalConfig(current => ({ |
| ...current, |
| claudeInChromeDefaultEnabled: enabled, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| claudeInChromeDefaultEnabled: enabled, |
| }) |
| logEvent('tengu_claude_in_chrome_setting_changed', { |
| enabled, |
| }) |
| }, |
| }, |
| |
| ...(isAgentSwarmsEnabled() |
| ? (() => { |
| const cliOverride = getCliTeammateModeOverride() |
| const label = cliOverride |
| ? `Teammate mode [overridden: ${cliOverride}]` |
| : 'Teammate mode' |
| return [ |
| { |
| id: 'teammateMode', |
| label, |
| value: globalConfig.teammateMode ?? 'auto', |
| options: ['auto', 'tmux', 'in-process'], |
| type: 'enum' as const, |
| onChange(mode: string) { |
| if ( |
| mode !== 'auto' && |
| mode !== 'tmux' && |
| mode !== 'in-process' |
| ) { |
| return |
| } |
| |
| clearCliTeammateModeOverride(mode) |
| saveGlobalConfig(current => ({ |
| ...current, |
| teammateMode: mode, |
| })) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| teammateMode: mode, |
| }) |
| logEvent('tengu_teammate_mode_changed', { |
| mode: mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }, |
| }, |
| { |
| id: 'teammateDefaultModel', |
| label: 'Default teammate model', |
| value: teammateModelDisplayString( |
| globalConfig.teammateDefaultModel, |
| ), |
| type: 'managedEnum' as const, |
| onChange() {}, |
| }, |
| ] |
| })() |
| : []), |
| |
| ...(feature('BRIDGE_MODE') && isBridgeEnabled() |
| ? [ |
| { |
| id: 'remoteControlAtStartup', |
| label: 'Enable Remote Control for all sessions', |
| value: |
| globalConfig.remoteControlAtStartup === undefined |
| ? 'default' |
| : String(globalConfig.remoteControlAtStartup), |
| options: ['true', 'false', 'default'], |
| type: 'enum' as const, |
| onChange(selected: string) { |
| if (selected === 'default') { |
| |
| saveGlobalConfig(current => { |
| if (current.remoteControlAtStartup === undefined) |
| return current |
| const next = { ...current } |
| delete next.remoteControlAtStartup |
| return next |
| }) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| remoteControlAtStartup: undefined, |
| }) |
| } else { |
| const enabled = selected === 'true' |
| saveGlobalConfig(current => { |
| if (current.remoteControlAtStartup === enabled) return current |
| return { ...current, remoteControlAtStartup: enabled } |
| }) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| remoteControlAtStartup: enabled, |
| }) |
| } |
| |
| const resolved = getRemoteControlAtStartup() |
| setAppState(prev => { |
| if ( |
| prev.replBridgeEnabled === resolved && |
| !prev.replBridgeOutboundOnly |
| ) |
| return prev |
| return { |
| ...prev, |
| replBridgeEnabled: resolved, |
| replBridgeOutboundOnly: false, |
| } |
| }) |
| }, |
| }, |
| ] |
| : []), |
| ...(shouldShowExternalIncludesToggle |
| ? [ |
| { |
| id: 'showExternalIncludesDialog', |
| label: 'External CLAUDE.md includes', |
| value: (() => { |
| const projectConfig = getCurrentProjectConfig() |
| if (projectConfig.hasClaudeMdExternalIncludesApproved) { |
| return 'true' |
| } else { |
| return 'false' |
| } |
| })(), |
| type: 'managedEnum' as const, |
| onChange() { |
| |
| }, |
| }, |
| ] |
| : []), |
| ...(process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace() |
| ? [ |
| { |
| id: 'apiKey', |
| label: ( |
| <Text> |
| Use custom API key:{' '} |
| <Text bold> |
| {normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY)} |
| </Text> |
| </Text> |
| ), |
| searchText: 'Use custom API key', |
| value: Boolean( |
| process.env.ANTHROPIC_API_KEY && |
| globalConfig.customApiKeyResponses?.approved?.includes( |
| normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY), |
| ), |
| ), |
| type: 'boolean' as const, |
| onChange(useCustomKey: boolean) { |
| saveGlobalConfig(current => { |
| const updated = { ...current } |
| if (!updated.customApiKeyResponses) { |
| updated.customApiKeyResponses = { |
| approved: [], |
| rejected: [], |
| } |
| } |
| if (!updated.customApiKeyResponses.approved) { |
| updated.customApiKeyResponses = { |
| ...updated.customApiKeyResponses, |
| approved: [], |
| } |
| } |
| if (!updated.customApiKeyResponses.rejected) { |
| updated.customApiKeyResponses = { |
| ...updated.customApiKeyResponses, |
| rejected: [], |
| } |
| } |
| if (process.env.ANTHROPIC_API_KEY) { |
| const truncatedKey = normalizeApiKeyForConfig( |
| process.env.ANTHROPIC_API_KEY, |
| ) |
| if (useCustomKey) { |
| updated.customApiKeyResponses = { |
| ...updated.customApiKeyResponses, |
| approved: [ |
| ...( |
| updated.customApiKeyResponses.approved ?? [] |
| ).filter(k => k !== truncatedKey), |
| truncatedKey, |
| ], |
| rejected: ( |
| updated.customApiKeyResponses.rejected ?? [] |
| ).filter(k => k !== truncatedKey), |
| } |
| } else { |
| updated.customApiKeyResponses = { |
| ...updated.customApiKeyResponses, |
| approved: ( |
| updated.customApiKeyResponses.approved ?? [] |
| ).filter(k => k !== truncatedKey), |
| rejected: [ |
| ...( |
| updated.customApiKeyResponses.rejected ?? [] |
| ).filter(k => k !== truncatedKey), |
| truncatedKey, |
| ], |
| } |
| } |
| } |
| return updated |
| }) |
| setGlobalConfig(getGlobalConfig()) |
| }, |
| }, |
| ] |
| : []), |
| ] |
|
|
| |
| const filteredSettingsItems = React.useMemo(() => { |
| if (!searchQuery) return settingsItems |
| const lowerQuery = searchQuery.toLowerCase() |
| return settingsItems.filter(setting => { |
| if (setting.id.toLowerCase().includes(lowerQuery)) return true |
| const searchableText = |
| 'searchText' in setting ? setting.searchText : setting.label |
| return searchableText.toLowerCase().includes(lowerQuery) |
| }) |
| }, [settingsItems, searchQuery]) |
|
|
| |
| |
| React.useEffect(() => { |
| if (selectedIndex >= filteredSettingsItems.length) { |
| const newIndex = Math.max(0, filteredSettingsItems.length - 1) |
| setSelectedIndex(newIndex) |
| setScrollOffset(Math.max(0, newIndex - maxVisible + 1)) |
| return |
| } |
| setScrollOffset(prev => { |
| if (selectedIndex < prev) return selectedIndex |
| if (selectedIndex >= prev + maxVisible) |
| return selectedIndex - maxVisible + 1 |
| return prev |
| }) |
| }, [filteredSettingsItems.length, selectedIndex, maxVisible]) |
|
|
| |
| |
| |
| const adjustScrollOffset = useCallback( |
| (newIndex: number) => { |
| setScrollOffset(prev => { |
| if (newIndex < prev) return newIndex |
| if (newIndex >= prev + maxVisible) return newIndex - maxVisible + 1 |
| return prev |
| }) |
| }, |
| [maxVisible], |
| ) |
|
|
| |
| |
| const handleSaveAndClose = useCallback(() => { |
| |
| |
| if (showSubmenu !== null) { |
| return |
| } |
| |
| |
| const formattedChanges: string[] = Object.entries(changes).map( |
| ([key, value]) => { |
| logEvent('tengu_config_changed', { |
| key: key as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| value: |
| value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| return `Set ${key} to ${chalk.bold(value)}` |
| }, |
| ) |
| |
| |
| |
| const effectiveApiKey = isRunningOnHomespace() |
| ? undefined |
| : process.env.ANTHROPIC_API_KEY |
| const initialUsingCustomKey = Boolean( |
| effectiveApiKey && |
| initialConfig.current.customApiKeyResponses?.approved?.includes( |
| normalizeApiKeyForConfig(effectiveApiKey), |
| ), |
| ) |
| const currentUsingCustomKey = Boolean( |
| effectiveApiKey && |
| globalConfig.customApiKeyResponses?.approved?.includes( |
| normalizeApiKeyForConfig(effectiveApiKey), |
| ), |
| ) |
| if (initialUsingCustomKey !== currentUsingCustomKey) { |
| formattedChanges.push( |
| `${currentUsingCustomKey ? 'Enabled' : 'Disabled'} custom API key`, |
| ) |
| logEvent('tengu_config_changed', { |
| key: 'env.ANTHROPIC_API_KEY' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| value: |
| currentUsingCustomKey as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
| if (globalConfig.theme !== initialConfig.current.theme) { |
| formattedChanges.push(`Set theme to ${chalk.bold(globalConfig.theme)}`) |
| } |
| if ( |
| globalConfig.preferredNotifChannel !== |
| initialConfig.current.preferredNotifChannel |
| ) { |
| formattedChanges.push( |
| `Set notifications to ${chalk.bold(globalConfig.preferredNotifChannel)}`, |
| ) |
| } |
| if (currentOutputStyle !== initialOutputStyle.current) { |
| formattedChanges.push( |
| `Set output style to ${chalk.bold(currentOutputStyle)}`, |
| ) |
| } |
| if (currentLanguage !== initialLanguage.current) { |
| formattedChanges.push( |
| `Set response language to ${chalk.bold(currentLanguage ?? 'Default (English)')}`, |
| ) |
| } |
| if (globalConfig.editorMode !== initialConfig.current.editorMode) { |
| formattedChanges.push( |
| `Set editor mode to ${chalk.bold(globalConfig.editorMode || 'emacs')}`, |
| ) |
| } |
| if (globalConfig.diffTool !== initialConfig.current.diffTool) { |
| formattedChanges.push( |
| `Set diff tool to ${chalk.bold(globalConfig.diffTool)}`, |
| ) |
| } |
| if (globalConfig.autoConnectIde !== initialConfig.current.autoConnectIde) { |
| formattedChanges.push( |
| `${globalConfig.autoConnectIde ? 'Enabled' : 'Disabled'} auto-connect to IDE`, |
| ) |
| } |
| if ( |
| globalConfig.autoInstallIdeExtension !== |
| initialConfig.current.autoInstallIdeExtension |
| ) { |
| formattedChanges.push( |
| `${globalConfig.autoInstallIdeExtension ? 'Enabled' : 'Disabled'} auto-install IDE extension`, |
| ) |
| } |
| if ( |
| globalConfig.autoCompactEnabled !== |
| initialConfig.current.autoCompactEnabled |
| ) { |
| formattedChanges.push( |
| `${globalConfig.autoCompactEnabled ? 'Enabled' : 'Disabled'} auto-compact`, |
| ) |
| } |
| if ( |
| globalConfig.respectGitignore !== initialConfig.current.respectGitignore |
| ) { |
| formattedChanges.push( |
| `${globalConfig.respectGitignore ? 'Enabled' : 'Disabled'} respect .gitignore in file picker`, |
| ) |
| } |
| if ( |
| globalConfig.copyFullResponse !== initialConfig.current.copyFullResponse |
| ) { |
| formattedChanges.push( |
| `${globalConfig.copyFullResponse ? 'Enabled' : 'Disabled'} always copy full response`, |
| ) |
| } |
| if (globalConfig.copyOnSelect !== initialConfig.current.copyOnSelect) { |
| formattedChanges.push( |
| `${globalConfig.copyOnSelect ? 'Enabled' : 'Disabled'} copy on select`, |
| ) |
| } |
| if ( |
| globalConfig.terminalProgressBarEnabled !== |
| initialConfig.current.terminalProgressBarEnabled |
| ) { |
| formattedChanges.push( |
| `${globalConfig.terminalProgressBarEnabled ? 'Enabled' : 'Disabled'} terminal progress bar`, |
| ) |
| } |
| if ( |
| globalConfig.showStatusInTerminalTab !== |
| initialConfig.current.showStatusInTerminalTab |
| ) { |
| formattedChanges.push( |
| `${globalConfig.showStatusInTerminalTab ? 'Enabled' : 'Disabled'} terminal tab status`, |
| ) |
| } |
| if ( |
| globalConfig.showTurnDuration !== initialConfig.current.showTurnDuration |
| ) { |
| formattedChanges.push( |
| `${globalConfig.showTurnDuration ? 'Enabled' : 'Disabled'} turn duration`, |
| ) |
| } |
| if ( |
| globalConfig.remoteControlAtStartup !== |
| initialConfig.current.remoteControlAtStartup |
| ) { |
| const remoteLabel = |
| globalConfig.remoteControlAtStartup === undefined |
| ? 'Reset Remote Control to default' |
| : `${globalConfig.remoteControlAtStartup ? 'Enabled' : 'Disabled'} Remote Control for all sessions` |
| formattedChanges.push(remoteLabel) |
| } |
| if ( |
| settingsData?.autoUpdatesChannel !== |
| initialSettingsData.current?.autoUpdatesChannel |
| ) { |
| formattedChanges.push( |
| `Set auto-update channel to ${chalk.bold(settingsData?.autoUpdatesChannel ?? 'latest')}`, |
| ) |
| } |
| if (formattedChanges.length > 0) { |
| onClose(formattedChanges.join('\n')) |
| } else { |
| onClose('Config dialog dismissed', { display: 'system' }) |
| } |
| }, [ |
| showSubmenu, |
| changes, |
| globalConfig, |
| mainLoopModel, |
| currentOutputStyle, |
| currentLanguage, |
| settingsData?.autoUpdatesChannel, |
| isFastModeEnabled() |
| ? (settingsData as Record<string, unknown> | undefined)?.fastMode |
| : undefined, |
| onClose, |
| ]) |
|
|
| |
| |
| |
| const revertChanges = useCallback(() => { |
| |
| |
| |
| if (themeSetting !== initialThemeSetting.current) { |
| setTheme(initialThemeSetting.current) |
| } |
| |
| |
| |
| saveGlobalConfig(() => initialConfig.current) |
| |
| |
| const il = initialLocalSettings |
| updateSettingsForSource('localSettings', { |
| spinnerTipsEnabled: il?.spinnerTipsEnabled, |
| prefersReducedMotion: il?.prefersReducedMotion, |
| defaultView: il?.defaultView, |
| outputStyle: il?.outputStyle, |
| }) |
| const iu = initialUserSettings |
| updateSettingsForSource('userSettings', { |
| alwaysThinkingEnabled: iu?.alwaysThinkingEnabled, |
| fastMode: iu?.fastMode, |
| promptSuggestionEnabled: iu?.promptSuggestionEnabled, |
| autoUpdatesChannel: iu?.autoUpdatesChannel, |
| minimumVersion: iu?.minimumVersion, |
| language: iu?.language, |
| ...(feature('TRANSCRIPT_CLASSIFIER') |
| ? { |
| useAutoModeDuringPlan: ( |
| iu as { useAutoModeDuringPlan?: boolean } | undefined |
| )?.useAutoModeDuringPlan, |
| } |
| : {}), |
| |
| |
| syntaxHighlightingDisabled: iu?.syntaxHighlightingDisabled, |
| |
| |
| |
| |
| |
| |
| permissions: |
| iu?.permissions === undefined |
| ? undefined |
| : { ...iu.permissions, defaultMode: iu.permissions.defaultMode }, |
| }) |
| |
| const ia = initialAppState |
| setAppState(prev => ({ |
| ...prev, |
| mainLoopModel: ia.mainLoopModel, |
| mainLoopModelForSession: ia.mainLoopModelForSession, |
| verbose: ia.verbose, |
| thinkingEnabled: ia.thinkingEnabled, |
| fastMode: ia.fastMode, |
| promptSuggestionEnabled: ia.promptSuggestionEnabled, |
| isBriefOnly: ia.isBriefOnly, |
| replBridgeEnabled: ia.replBridgeEnabled, |
| replBridgeOutboundOnly: ia.replBridgeOutboundOnly, |
| settings: ia.settings, |
| |
| |
| toolPermissionContext: transitionPlanAutoMode(prev.toolPermissionContext), |
| })) |
| |
| |
| |
| if (getUserMsgOptIn() !== initialUserMsgOptIn) { |
| setUserMsgOptIn(initialUserMsgOptIn) |
| } |
| }, [ |
| themeSetting, |
| setTheme, |
| initialLocalSettings, |
| initialUserSettings, |
| initialAppState, |
| initialUserMsgOptIn, |
| setAppState, |
| ]) |
|
|
| |
| const handleEscape = useCallback(() => { |
| if (showSubmenu !== null) { |
| return |
| } |
| if (isDirty.current) { |
| revertChanges() |
| } |
| onClose('Config dialog dismissed', { display: 'system' }) |
| }, [showSubmenu, revertChanges, onClose]) |
|
|
| |
| |
| |
| useKeybinding('confirm:no', handleEscape, { |
| context: 'Settings', |
| isActive: showSubmenu === null && !isSearchMode && !headerFocused, |
| }) |
| |
| |
| useKeybinding('settings:close', handleSaveAndClose, { |
| context: 'Settings', |
| isActive: showSubmenu === null && !isSearchMode && !headerFocused, |
| }) |
|
|
| |
| |
| const toggleSetting = useCallback(() => { |
| const setting = filteredSettingsItems[selectedIndex] |
| if (!setting || !setting.onChange) { |
| return |
| } |
|
|
| if (setting.type === 'boolean') { |
| isDirty.current = true |
| setting.onChange(!setting.value) |
| if (setting.id === 'thinkingEnabled') { |
| const newValue = !setting.value |
| const backToInitial = newValue === initialThinkingEnabled.current |
| if (backToInitial) { |
| setShowThinkingWarning(false) |
| } else if (context.messages.some(m => m.type === 'assistant')) { |
| setShowThinkingWarning(true) |
| } |
| } |
| return |
| } |
|
|
| if ( |
| setting.id === 'theme' || |
| setting.id === 'model' || |
| setting.id === 'teammateDefaultModel' || |
| setting.id === 'showExternalIncludesDialog' || |
| setting.id === 'outputStyle' || |
| setting.id === 'language' |
| ) { |
| |
| |
| switch (setting.id) { |
| case 'theme': |
| setShowSubmenu('Theme') |
| setTabsHidden(true) |
| return |
| case 'model': |
| setShowSubmenu('Model') |
| setTabsHidden(true) |
| return |
| case 'teammateDefaultModel': |
| setShowSubmenu('TeammateModel') |
| setTabsHidden(true) |
| return |
| case 'showExternalIncludesDialog': |
| setShowSubmenu('ExternalIncludes') |
| setTabsHidden(true) |
| return |
| case 'outputStyle': |
| setShowSubmenu('OutputStyle') |
| setTabsHidden(true) |
| return |
| case 'language': |
| setShowSubmenu('Language') |
| setTabsHidden(true) |
| return |
| } |
| } |
|
|
| if (setting.id === 'autoUpdatesChannel') { |
| if (autoUpdaterDisabledReason) { |
| |
| setShowSubmenu('EnableAutoUpdates') |
| setTabsHidden(true) |
| return |
| } |
| const currentChannel = settingsData?.autoUpdatesChannel ?? 'latest' |
| if (currentChannel === 'latest') { |
| |
| setShowSubmenu('ChannelDowngrade') |
| setTabsHidden(true) |
| } else { |
| |
| isDirty.current = true |
| updateSettingsForSource('userSettings', { |
| autoUpdatesChannel: 'latest', |
| minimumVersion: undefined, |
| }) |
| setSettingsData(prev => ({ |
| ...prev, |
| autoUpdatesChannel: 'latest', |
| minimumVersion: undefined, |
| })) |
| logEvent('tengu_autoupdate_channel_changed', { |
| channel: |
| 'latest' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
| return |
| } |
|
|
| if (setting.type === 'enum') { |
| isDirty.current = true |
| const currentIndex = setting.options.indexOf(setting.value) |
| const nextIndex = (currentIndex + 1) % setting.options.length |
| setting.onChange(setting.options[nextIndex]!) |
| return |
| } |
| }, [ |
| autoUpdaterDisabledReason, |
| filteredSettingsItems, |
| selectedIndex, |
| settingsData?.autoUpdatesChannel, |
| setTabsHidden, |
| ]) |
|
|
| const moveSelection = (delta: -1 | 1): void => { |
| setShowThinkingWarning(false) |
| const newIndex = Math.max( |
| 0, |
| Math.min(filteredSettingsItems.length - 1, selectedIndex + delta), |
| ) |
| setSelectedIndex(newIndex) |
| adjustScrollOffset(newIndex) |
| } |
|
|
| useKeybindings( |
| { |
| 'select:previous': () => { |
| if (selectedIndex === 0) { |
| |
| |
| |
| setShowThinkingWarning(false) |
| setIsSearchMode(true) |
| setScrollOffset(0) |
| } else { |
| moveSelection(-1) |
| } |
| }, |
| 'select:next': () => moveSelection(1), |
| |
| |
| |
| |
| 'scroll:lineUp': () => moveSelection(-1), |
| 'scroll:lineDown': () => moveSelection(1), |
| 'select:accept': toggleSetting, |
| 'settings:search': () => { |
| setIsSearchMode(true) |
| setSearchQuery('') |
| }, |
| }, |
| { |
| context: 'Settings', |
| isActive: showSubmenu === null && !isSearchMode && !headerFocused, |
| }, |
| ) |
|
|
| |
| |
| |
| const handleKeyDown = useCallback( |
| (e: KeyboardEvent) => { |
| if (showSubmenu !== null) return |
| if (headerFocused) return |
| |
| if (isSearchMode) { |
| if (e.key === 'escape') { |
| e.preventDefault() |
| if (searchQuery.length > 0) { |
| setSearchQuery('') |
| } else { |
| setIsSearchMode(false) |
| } |
| return |
| } |
| if (e.key === 'return' || e.key === 'down' || e.key === 'wheeldown') { |
| e.preventDefault() |
| setIsSearchMode(false) |
| setSelectedIndex(0) |
| setScrollOffset(0) |
| } |
| return |
| } |
| |
| |
| |
| if (e.key === 'left' || e.key === 'right' || e.key === 'tab') { |
| e.preventDefault() |
| toggleSetting() |
| return |
| } |
| |
| |
| |
| |
| if (e.ctrl || e.meta) return |
| if (e.key === 'j' || e.key === 'k' || e.key === '/') return |
| if (e.key.length === 1 && e.key !== ' ') { |
| e.preventDefault() |
| setIsSearchMode(true) |
| setSearchQuery(e.key) |
| } |
| }, |
| [ |
| showSubmenu, |
| headerFocused, |
| isSearchMode, |
| searchQuery, |
| setSearchQuery, |
| toggleSetting, |
| ], |
| ) |
|
|
| return ( |
| <Box |
| flexDirection="column" |
| width="100%" |
| tabIndex={0} |
| autoFocus |
| onKeyDown={handleKeyDown} |
| > |
| {showSubmenu === 'Theme' ? ( |
| <> |
| <ThemePicker |
| onThemeSelect={setting => { |
| isDirty.current = true |
| setTheme(setting) |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| hideEscToCancel |
| skipExitHandling={true} // Skip exit handling as Config already handles it |
| /> |
| <Box> |
| <Text dimColor italic> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="select" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Confirmation" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| </Box> |
| </> |
| ) : showSubmenu === 'Model' ? ( |
| <> |
| <ModelPicker |
| initial={mainLoopModel} |
| onSelect={(model, _effort) => { |
| isDirty.current = true |
| onChangeMainModelConfig(model) |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| showFastModeNotice={ |
| isFastModeEnabled() |
| ? isFastMode && |
| isFastModeSupportedByModel(mainLoopModel) && |
| isFastModeAvailable() |
| : false |
| } |
| /> |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="confirm" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Confirmation" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| </> |
| ) : showSubmenu === 'TeammateModel' ? ( |
| <> |
| <ModelPicker |
| initial={globalConfig.teammateDefaultModel ?? null} |
| skipSettingsWrite |
| headerText="Default model for newly spawned teammates. The leader can override via the tool call's model parameter." |
| onSelect={(model, _effort) => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| // First-open-then-Enter from unset: picker highlights "Default" |
| // (initial=null) and confirming would write null, silently |
| // switching Opus-fallback → follow-leader. Treat as no-op. |
| if ( |
| globalConfig.teammateDefaultModel === undefined && |
| model === null |
| ) { |
| return |
| } |
| isDirty.current = true |
| saveGlobalConfig(current => |
| current.teammateDefaultModel === model |
| ? current |
| : { ...current, teammateDefaultModel: model }, |
| ) |
| setGlobalConfig({ |
| ...getGlobalConfig(), |
| teammateDefaultModel: model, |
| }) |
| setChanges(prev => ({ |
| ...prev, |
| teammateDefaultModel: teammateModelDisplayString(model), |
| })) |
| logEvent('tengu_teammate_default_model_changed', { |
| model: |
| model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }} |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| /> |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="confirm" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Confirmation" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| </> |
| ) : showSubmenu === 'ExternalIncludes' ? ( |
| <> |
| <ClaudeMdExternalIncludesDialog |
| onDone={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| externalIncludes={getExternalClaudeMdIncludes(memoryFiles)} |
| /> |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="confirm" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Confirmation" |
| fallback="Esc" |
| description="disable external includes" |
| /> |
| </Byline> |
| </Text> |
| </> |
| ) : showSubmenu === 'OutputStyle' ? ( |
| <> |
| <OutputStylePicker |
| initialStyle={currentOutputStyle} |
| onComplete={style => { |
| isDirty.current = true |
| setCurrentOutputStyle(style ?? DEFAULT_OUTPUT_STYLE_NAME) |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| |
| // Save to local settings |
| updateSettingsForSource('localSettings', { |
| outputStyle: style, |
| }) |
| |
| void logEvent('tengu_output_style_changed', { |
| style: (style ?? |
| DEFAULT_OUTPUT_STYLE_NAME) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| settings_source: |
| 'localSettings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }} |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| /> |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="confirm" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Confirmation" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| </> |
| ) : showSubmenu === 'Language' ? ( |
| <> |
| <LanguagePicker |
| initialLanguage={currentLanguage} |
| onComplete={language => { |
| isDirty.current = true |
| setCurrentLanguage(language) |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| |
| // Save to user settings |
| updateSettingsForSource('userSettings', { |
| language, |
| }) |
| |
| void logEvent('tengu_language_changed', { |
| language: (language ?? |
| 'default') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }} |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| /> |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="Enter" action="confirm" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Settings" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| </> |
| ) : showSubmenu === 'EnableAutoUpdates' ? ( |
| <Dialog |
| title="Enable Auto-Updates" |
| onCancel={() => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| }} |
| hideBorder |
| hideInputGuide |
| > |
| {autoUpdaterDisabledReason?.type !== 'config' ? ( |
| <> |
| <Text> |
| {autoUpdaterDisabledReason?.type === 'env' |
| ? 'Auto-updates are controlled by an environment variable and cannot be changed here.' |
| : 'Auto-updates are disabled in development builds.'} |
| </Text> |
| {autoUpdaterDisabledReason?.type === 'env' && ( |
| <Text dimColor> |
| Unset {autoUpdaterDisabledReason.envVar} to re-enable |
| auto-updates. |
| </Text> |
| )} |
| </> |
| ) : ( |
| <Select |
| options={[ |
| { |
| label: 'Enable with latest channel', |
| value: 'latest', |
| }, |
| { |
| label: 'Enable with stable channel', |
| value: 'stable', |
| }, |
| ]} |
| onChange={(channel: string) => { |
| isDirty.current = true |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| |
| saveGlobalConfig(current => ({ |
| ...current, |
| autoUpdates: true, |
| })) |
| setGlobalConfig({ ...getGlobalConfig(), autoUpdates: true }) |
| |
| updateSettingsForSource('userSettings', { |
| autoUpdatesChannel: channel as 'latest' | 'stable', |
| minimumVersion: undefined, |
| }) |
| setSettingsData(prev => ({ |
| ...prev, |
| autoUpdatesChannel: channel as 'latest' | 'stable', |
| minimumVersion: undefined, |
| })) |
| logEvent('tengu_autoupdate_enabled', { |
| channel: |
| channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| }} |
| /> |
| )} |
| </Dialog> |
| ) : showSubmenu === 'ChannelDowngrade' ? ( |
| <ChannelDowngradeDialog |
| currentVersion={MACRO.VERSION} |
| onChoice={(choice: ChannelDowngradeChoice) => { |
| setShowSubmenu(null) |
| setTabsHidden(false) |
| |
| if (choice === 'cancel') { |
| // User cancelled - don't change anything |
| return |
| } |
| |
| isDirty.current = true |
| // Switch to stable channel |
| const newSettings: { |
| autoUpdatesChannel: 'stable' |
| minimumVersion?: string |
| } = { |
| autoUpdatesChannel: 'stable', |
| } |
| |
| if (choice === 'stay') { |
| // User wants to stay on current version until stable catches up |
| newSettings.minimumVersion = MACRO.VERSION |
| } |
| |
| updateSettingsForSource('userSettings', newSettings) |
| setSettingsData(prev => ({ |
| ...prev, |
| ...newSettings, |
| })) |
| logEvent('tengu_autoupdate_channel_changed', { |
| channel: |
| 'stable' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| minimum_version_set: choice === 'stay', |
| }) |
| }} |
| /> |
| ) : ( |
| <Box |
| flexDirection="column" |
| gap={1} |
| marginY={insideModal ? undefined : 1} |
| > |
| <SearchBox |
| query={searchQuery} |
| isFocused={isSearchMode && !headerFocused} |
| isTerminalFocused={isTerminalFocused} |
| cursorOffset={searchCursorOffset} |
| placeholder="Search settings…" |
| /> |
| <Box flexDirection="column"> |
| {filteredSettingsItems.length === 0 ? ( |
| <Text dimColor italic> |
| No settings match "{searchQuery}" |
| </Text> |
| ) : ( |
| <> |
| {scrollOffset > 0 && ( |
| <Text dimColor> |
| {figures.arrowUp} {scrollOffset} more above |
| </Text> |
| )} |
| {filteredSettingsItems |
| .slice(scrollOffset, scrollOffset + maxVisible) |
| .map((setting, i) => { |
| const actualIndex = scrollOffset + i |
| const isSelected = |
| actualIndex === selectedIndex && |
| !headerFocused && |
| !isSearchMode |
| |
| return ( |
| <React.Fragment key={setting.id}> |
| <Box> |
| <Box width={44}> |
| <Text color={isSelected ? 'suggestion' : undefined}> |
| {isSelected ? figures.pointer : ' '}{' '} |
| {setting.label} |
| </Text> |
| </Box> |
| <Box key={isSelected ? 'selected' : 'unselected'}> |
| {setting.type === 'boolean' ? ( |
| <> |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| {setting.value.toString()} |
| </Text> |
| {showThinkingWarning && |
| setting.id === 'thinkingEnabled' && ( |
| <Text color="warning"> |
| {' '} |
| Changing thinking mode mid-conversation |
| will increase latency and may reduce |
| quality. |
| </Text> |
| )} |
| </> |
| ) : setting.id === 'theme' ? ( |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| {THEME_LABELS[setting.value.toString()] ?? |
| setting.value.toString()} |
| </Text> |
| ) : setting.id === 'notifChannel' ? ( |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| <NotifChannelLabel |
| value={setting.value.toString()} |
| /> |
| </Text> |
| ) : setting.id === 'defaultPermissionMode' ? ( |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| {permissionModeTitle( |
| setting.value as PermissionMode, |
| )} |
| </Text> |
| ) : setting.id === 'autoUpdatesChannel' && |
| autoUpdaterDisabledReason ? ( |
| <Box flexDirection="column"> |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| disabled |
| </Text> |
| <Text dimColor> |
| ( |
| {formatAutoUpdaterDisabledReason( |
| autoUpdaterDisabledReason, |
| )} |
| ) |
| </Text> |
| </Box> |
| ) : ( |
| <Text |
| color={isSelected ? 'suggestion' : undefined} |
| > |
| {setting.value.toString()} |
| </Text> |
| )} |
| </Box> |
| </Box> |
| </React.Fragment> |
| ) |
| })} |
| {scrollOffset + maxVisible < filteredSettingsItems.length && ( |
| <Text dimColor> |
| {figures.arrowDown}{' '} |
| {filteredSettingsItems.length - scrollOffset - maxVisible}{' '} |
| more below |
| </Text> |
| )} |
| </> |
| )} |
| </Box> |
| {headerFocused ? ( |
| <Text dimColor> |
| <Byline> |
| <KeyboardShortcutHint shortcut="←/→ tab" action="switch" /> |
| <KeyboardShortcutHint shortcut="↓" action="return" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Settings" |
| fallback="Esc" |
| description="close" |
| /> |
| </Byline> |
| </Text> |
| ) : isSearchMode ? ( |
| <Text dimColor> |
| <Byline> |
| <Text>Type to filter</Text> |
| <KeyboardShortcutHint shortcut="Enter/↓" action="select" /> |
| <KeyboardShortcutHint shortcut="↑" action="tabs" /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Settings" |
| fallback="Esc" |
| description="clear" |
| /> |
| </Byline> |
| </Text> |
| ) : ( |
| <Text dimColor> |
| <Byline> |
| <ConfigurableShortcutHint |
| action="select:accept" |
| context="Settings" |
| fallback="Space" |
| description="change" |
| /> |
| <ConfigurableShortcutHint |
| action="settings:close" |
| context="Settings" |
| fallback="Enter" |
| description="save" |
| /> |
| <ConfigurableShortcutHint |
| action="settings:search" |
| context="Settings" |
| fallback="/" |
| description="search" |
| /> |
| <ConfigurableShortcutHint |
| action="confirm:no" |
| context="Settings" |
| fallback="Esc" |
| description="cancel" |
| /> |
| </Byline> |
| </Text> |
| )} |
| </Box> |
| )} |
| </Box> |
| ) |
| } |
|
|
| function teammateModelDisplayString(value: string | null | undefined): string { |
| if (value === undefined) { |
| return modelDisplayString(getHardcodedTeammateModelFallback()) |
| } |
| if (value === null) return "Default (leader's model)" |
| return modelDisplayString(value) |
| } |
|
|
| const THEME_LABELS: Record<string, string> = { |
| auto: 'Auto (match terminal)', |
| dark: 'Dark mode', |
| light: 'Light mode', |
| 'dark-daltonized': 'Dark mode (colorblind-friendly)', |
| 'light-daltonized': 'Light mode (colorblind-friendly)', |
| 'dark-ansi': 'Dark mode (ANSI colors only)', |
| 'light-ansi': 'Light mode (ANSI colors only)', |
| } |
|
|
| function NotifChannelLabel({ value }: { value: string }): React.ReactNode { |
| switch (value) { |
| case 'auto': |
| return 'Auto' |
| case 'iterm2': |
| return ( |
| <Text> |
| iTerm2 <Text dimColor>(OSC 9)</Text> |
| </Text> |
| ) |
| case 'terminal_bell': |
| return ( |
| <Text> |
| Terminal Bell <Text dimColor>(\a)</Text> |
| </Text> |
| ) |
| case 'kitty': |
| return ( |
| <Text> |
| Kitty <Text dimColor>(OSC 99)</Text> |
| </Text> |
| ) |
| case 'ghostty': |
| return ( |
| <Text> |
| Ghostty <Text dimColor>(OSC 777)</Text> |
| </Text> |
| ) |
| case 'iterm2_with_bell': |
| return 'iTerm2 w/ Bell' |
| case 'notifications_disabled': |
| return 'Disabled' |
| default: |
| return value |
| } |
| } |
|
|