diff --git a/src/commands/poor/poorMode.ts b/src/commands/poor/poorMode.ts new file mode 100644 index 000000000..d6ab2e99f --- /dev/null +++ b/src/commands/poor/poorMode.ts @@ -0,0 +1,27 @@ +/** + * Poor mode state — when active, skips extract_memories and prompt_suggestion + * to reduce token consumption. + * + * Persisted to settings.json so it survives session restarts. + */ + +import { + getInitialSettings, + updateSettingsForSource, +} from '../../utils/settings/settings.js' + +let poorModeActive: boolean | null = null + +export function isPoorModeActive(): boolean { + if (poorModeActive === null) { + poorModeActive = getInitialSettings().poorMode === true + } + return poorModeActive +} + +export function setPoorMode(active: boolean): void { + poorModeActive = active + updateSettingsForSource('userSettings', { + poorMode: active || undefined, + }) +} diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 0768472d7..371b9aed6 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -1,101 +1,74 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered -import { feature } from 'bun:bundle' -import { type KeyboardEvent, Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '@anthropic/ink' -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 { feature } from 'bun:bundle'; +import { type KeyboardEvent, Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '@anthropic/ink'; +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' +} from '../../utils/config.js'; +import chalk from 'chalk'; import { + permissionModeShortTitle, permissionModeFromString, toExternalPermissionMode, isExternalPermissionMode, PERMISSION_MODES, type PermissionMode, -} from '../../utils/permissions/PermissionMode.js' +} from '../../utils/permissions/PermissionMode.js'; import { getAutoModeEnabledState, hasAutoModeOptInAnySource, transitionPlanAutoMode, -} from '../../utils/permissions/permissionSetup.js' -import { logError } from '../../utils/log.js' +} 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 '@anthropic/ink' -import { Select } from '../CustomSelect/index.js' -import { OutputStylePicker } from '../OutputStylePicker.js' -import { LanguagePicker } from '../LanguagePicker.js' +} 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 '@anthropic/ink'; +import { Select } from '../CustomSelect/index.js'; +import { OutputStylePicker } from '../OutputStylePicker.js'; +import { LanguagePicker } from '../LanguagePicker.js'; import { type MemoryFileInfo, getExternalClaudeMdIncludes, getMemoryFiles, hasExternalClaudeMdIncludes, -} from 'src/utils/claudemd.js' -import { Byline, KeyboardShortcutHint, useTabHeaderFocus } from '@anthropic/ink' -import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.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' +} from 'src/utils/claudemd.js'; +import { Byline, KeyboardShortcutHint, useTabHeaderFocus } from '@anthropic/ink'; +import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.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' +} 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, @@ -103,50 +76,48 @@ import { isFastModeEnabled, getFastModeModel, isFastModeSupportedByModel, -} from '../../utils/fastMode.js' -import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js' +} from '../../utils/fastMode.js'; +import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; +import { getPlatform } from '../../utils/platform.js'; type Props = { - onClose: ( - result?: string, - options?: { display?: CommandResultDisplay }, - ) => void - context: LocalJSXCommandContext - setTabsHidden: (hidden: boolean) => void - onIsSearchModeChange?: (inSearchMode: boolean) => void - contentHeight?: number -} + 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: string; } | { - id: string - label: React.ReactNode - searchText: string - } + id: string; + label: React.ReactNode; + searchText: string; + }; type Setting = | (SettingBase & { - value: boolean - onChange(value: boolean): void - type: 'boolean' + value: boolean; + onChange(value: boolean): void; + type: 'boolean'; }) | (SettingBase & { - value: string - options: string[] - onChange(value: string): void - type: 'enum' + value: string; + options: string[]; + onChange(value: string): void; + type: 'enum'; }) | (SettingBase & { // For enums that are set by a custom component, we don't need to pass options, // but we still need a value to display in the top-level config menu - value: string - onChange(value: string): void - type: 'managedEnum' - }) + value: string; + onChange(value: string): void; + type: 'managedEnum'; + }); type SubMenu = | 'Theme' @@ -156,7 +127,7 @@ type SubMenu = | 'OutputStyle' | 'ChannelDowngrade' | 'Language' - | 'EnableAutoUpdates' + | 'EnableAutoUpdates'; export function Config({ onClose, context, @@ -164,46 +135,43 @@ export function Config({ 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 { 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( settingsData?.outputStyle || DEFAULT_OUTPUT_STYLE_NAME, - ) - const initialOutputStyle = React.useRef(currentOutputStyle) - const [currentLanguage, setCurrentLanguage] = useState( - 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 initialOutputStyle = React.useRef(currentOutputStyle); + const [currentLanguage, setCurrentLanguage] = useState(settingsData?.language); + const initialLanguage = React.useRef(currentLanguage); + const [selectedIndex, setSelectedIndex] = useState(0); + const [scrollOffset, setScrollOffset] = useState(0); + const [isSearchMode, setIsSearchMode] = useState(false); + const isTerminalFocused = useTerminalFocus(); + const { rows } = useTerminalSize(); // contentHeight is set by Settings.tsx (same value passed to Tabs to fix // pane height across all tabs — prevents layout jank when switching). // Reserve ~10 rows for chrome (search box, gaps, footer, scroll hints). // Fallback calc for standalone rendering (tests). - 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 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 currentDefaultPermissionMode = permissionModeFromString(settingsData?.permissions?.defaultMode ?? 'default'); // Show auto in the default-mode dropdown when the user has opted in OR the // config is fully 'enabled' — even if currently circuit-broken ('disabled'), // an opted-in user should still see it in settings (it's a temporary state). const showAutoInDefaultModePicker = feature('TRANSCRIPT_CLASSIFIER') ? hasAutoModeOptInAnySource() || getAutoModeEnabledState() === 'enabled' - : false + : false; // Chat/Transcript view picker is visible to entitled users (pass the GB // gate) even if they haven't opted in this session — it IS the persistent // opt-in. 'chat' written here is read at next startup by main.tsx which @@ -214,28 +182,24 @@ export function Config({ ? ( require('@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js') as typeof import('@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js') ).isBriefEntitled() - : false + : false; /* eslint-enable @typescript-eslint/no-require-imports */ - const setAppState = useSetAppState() - const [changes, setChanges] = useState<{ [key: string]: unknown }>({}) - const initialThinkingEnabled = React.useRef(thinkingEnabled) + const setAppState = useSetAppState(); + const [changes, setChanges] = useState<{ [key: string]: unknown }>({}); + const initialThinkingEnabled = React.useRef(thinkingEnabled); // Per-source settings snapshots for revert-on-escape. getInitialSettings() // returns merged-across-sources which can't tell us what to delete vs // restore; per-source snapshots + updateSettingsForSource's // undefined-deletes-key semantics can. Lazy-init via useState (no setter) to // avoid reading settings files on every render — useRef evaluates its arg // eagerly even though only the first result is kept. - const [initialLocalSettings] = useState(() => - getSettingsForSource('localSettings'), - ) - const [initialUserSettings] = useState(() => - getSettingsForSource('userSettings'), - ) - const initialThemeSetting = React.useRef(themeSetting) + const [initialLocalSettings] = useState(() => getSettingsForSource('localSettings')); + const [initialUserSettings] = useState(() => getSettingsForSource('userSettings')); + const initialThemeSetting = React.useRef(themeSetting); // AppState fields Config may modify — snapshot once at mount. - const store = useAppStateStore() + const store = useAppStateStore(); const [initialAppState] = useState(() => { - const s = store.getState() + const s = store.getState(); return { mainLoopModel: s.mainLoopModel, mainLoopModelForSession: s.mainLoopModelForSession, @@ -247,19 +211,19 @@ export function Config({ replBridgeEnabled: s.replBridgeEnabled, replBridgeOutboundOnly: s.replBridgeOutboundOnly, settings: s.settings, - } - }) + }; + }); // Bootstrap state snapshot — userMsgOptIn is outside AppState, so // revertChanges needs to restore it separately. Without this, cycling // defaultView to 'chat' then Escape leaves the tool active while the // display filter reverts — the exact ambient-activation behavior this // PR's entitlement/opt-in split is meant to prevent. - const [initialUserMsgOptIn] = useState(() => getUserMsgOptIn()) + const [initialUserMsgOptIn] = useState(() => getUserMsgOptIn()); // Set on first user-visible change; gates revertChanges() on Escape so // opening-then-closing doesn't trigger redundant disk writes. - const isDirty = React.useRef(false) - const [showThinkingWarning, setShowThinkingWarning] = useState(false) - const [showSubmenu, setShowSubmenu] = useState(null) + const isDirty = React.useRef(false); + const [showThinkingWarning, setShowThinkingWarning] = useState(false); + const [showSubmenu, setShowSubmenu] = useState(null); const { query: searchQuery, setQuery: setSearchQuery, @@ -271,74 +235,65 @@ export function Config({ // Ctrl+C/D must reach Settings' useExitOnCtrlCD; 'd' also avoids // double-action (delete-char + exit-pending). passthroughCtrlKeys: ['c', 'd'], - }) + }); // Tell the parent when Config's own Esc handler is active so Settings cedes // confirm:no. Only true when search mode owns the keyboard — not when the // tab header is focused (then Settings must handle Esc-to-close). - const ownsEsc = isSearchMode && !headerFocused + const ownsEsc = isSearchMode && !headerFocused; React.useEffect(() => { - onIsSearchModeChange?.(ownsEsc) - }, [ownsEsc, onIsSearchModeChange]) + onIsSearchModeChange?.(ownsEsc); + }, [ownsEsc, onIsSearchModeChange]); - const isConnectedToIde = hasAccessToIDEExtensionDiffFeature( - context.options.mcpClients, - ) + const isConnectedToIde = hasAccessToIDEExtensionDiffFeature(context.options.mcpClients); - const isFileCheckpointingAvailable = !isEnvTruthy( - process.env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING, - ) + const isFileCheckpointingAvailable = !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING); - const memoryFiles = React.use(getMemoryFiles(true)) as MemoryFileInfo[] - const shouldShowExternalIncludesToggle = - hasExternalClaudeMdIncludes(memoryFiles) + const memoryFiles = React.use(getMemoryFiles(true)) as MemoryFileInfo[]; + const shouldShowExternalIncludesToggle = hasExternalClaudeMdIncludes(memoryFiles); - const autoUpdaterDisabledReason = getAutoUpdaterDisabledReason() + const autoUpdaterDisabledReason = getAutoUpdaterDisabledReason(); function onChangeMainModelConfig(value: string | null): void { - const previousModel = mainLoopModel + 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, - }) + 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' - : '') + (isBilledAsExtraUsage(value, false, isOpus1mMergeEnabled()) ? ' · Billed as extra usage' : ''); if ('model' in prev) { - const { model, ...rest } = prev - return { ...rest, model: valStr } + const { model, ...rest } = prev; + return { ...rest, model: valStr }; } - return { ...prev, model: valStr } - }) + return { ...prev, model: valStr }; + }); } function onChangeVerbose(value: boolean): void { // Update the global config to persist the setting - saveGlobalConfig(current => ({ ...current, verbose: value })) - setGlobalConfig({ ...getGlobalConfig(), verbose: value }) + saveGlobalConfig(current => ({ ...current, verbose: value })); + setGlobalConfig({ ...getGlobalConfig(), verbose: value }); // Update the app state for immediate UI feedback setAppState(prev => ({ ...prev, verbose: value, - })) + })); setChanges(prev => { if ('verbose' in prev) { - const { verbose, ...rest } = prev - return rest + const { verbose, ...rest } = prev; + return rest; } - return { ...prev, verbose: value } - }) + return { ...prev, verbose: value }; + }); } // TODO: Add MCP servers @@ -350,11 +305,11 @@ export function Config({ value: globalConfig.autoCompactEnabled, type: 'boolean' as const, onChange(autoCompactEnabled: boolean) { - saveGlobalConfig(current => ({ ...current, autoCompactEnabled })) - setGlobalConfig({ ...getGlobalConfig(), autoCompactEnabled }) + saveGlobalConfig(current => ({ ...current, autoCompactEnabled })); + setGlobalConfig({ ...getGlobalConfig(), autoCompactEnabled }); logEvent('tengu_auto_compact_setting_changed', { enabled: autoCompactEnabled, - }) + }); }, }, { @@ -365,15 +320,33 @@ export function Config({ onChange(spinnerTipsEnabled: boolean) { updateSettingsForSource('localSettings', { spinnerTipsEnabled, - }) + }); // Update local state to reflect the change immediately setSettingsData(prev => ({ ...prev, spinnerTipsEnabled, - })) + })); logEvent('tengu_tips_setting_changed', { enabled: spinnerTipsEnabled, - }) + }); + }, + }, + { + id: 'cacheWarningEnabled', + label: 'Cache warnings', + value: settingsData?.cacheWarningEnabled ?? true, + type: 'boolean' as const, + onChange(cacheWarningEnabled: boolean) { + updateSettingsForSource('localSettings', { + cacheWarningEnabled, + }); + setSettingsData(prev => ({ + ...prev, + cacheWarningEnabled, + })); + logEvent('tengu_cache_warning_setting_changed', { + enabled: cacheWarningEnabled, + }); }, }, { @@ -384,19 +357,19 @@ export function Config({ onChange(prefersReducedMotion: boolean) { updateSettingsForSource('localSettings', { prefersReducedMotion, - }) + }); setSettingsData(prev => ({ ...prev, prefersReducedMotion, - })) + })); // Sync to AppState so components react immediately setAppState(prev => ({ ...prev, settings: { ...prev.settings, prefersReducedMotion }, - })) + })); logEvent('tengu_reduce_motion_setting_changed', { enabled: prefersReducedMotion, - }) + }); }, }, { @@ -405,11 +378,11 @@ export function Config({ value: thinkingEnabled ?? true, type: 'boolean' as const, onChange(enabled: boolean) { - setAppState(prev => ({ ...prev, thinkingEnabled: enabled })) + setAppState(prev => ({ ...prev, thinkingEnabled: enabled })); updateSettingsForSource('userSettings', { alwaysThinkingEnabled: enabled ? undefined : false, - }) - logEvent('tengu_thinking_toggled', { enabled }) + }); + logEvent('tengu_thinking_toggled', { enabled }); }, }, // Fast mode toggle (ant-only, eliminated from external builds) @@ -421,28 +394,28 @@ export function Config({ value: !!isFastMode, type: 'boolean' as const, onChange(enabled: boolean) { - clearFastModeCooldown() + 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' })) + })); + setChanges(prev => ({ ...prev, 'Fast mode': 'OFF' })); } }, }, @@ -459,10 +432,33 @@ export function Config({ setAppState(prev => ({ ...prev, promptSuggestionEnabled: enabled, - })) + })); updateSettingsForSource('userSettings', { promptSuggestionEnabled: enabled ? undefined : false, - }) + }); + }, + }, + ] + : []), + ...(feature('POOR') + ? [ + { + id: 'poorMode', + label: 'Poor mode (save tokens)', + value: (() => { + const PoorMode = + require('../../commands/poor/poorMode.js') as typeof import('../../commands/poor/poorMode.js'); + return PoorMode.isPoorModeActive(); + })(), + type: 'boolean' as const, + onChange(enabled: boolean) { + const PoorMode = + require('../../commands/poor/poorMode.js') as typeof import('../../commands/poor/poorMode.js'); + PoorMode.setPoorMode(enabled); + setAppState(prev => ({ + ...prev, + promptSuggestionEnabled: !enabled, + })); }, }, ] @@ -477,19 +473,19 @@ export function Config({ type: 'boolean' as const, onChange(enabled: boolean) { saveGlobalConfig(current => { - if (current.speculationEnabled === enabled) return current + if (current.speculationEnabled === enabled) return current; return { ...current, speculationEnabled: enabled, - } - }) + }; + }); setGlobalConfig({ ...getGlobalConfig(), speculationEnabled: enabled, - }) + }); logEvent('tengu_speculation_setting_changed', { enabled, - }) + }); }, }, ] @@ -505,14 +501,14 @@ export function Config({ saveGlobalConfig(current => ({ ...current, fileCheckpointingEnabled: enabled, - })) + })); setGlobalConfig({ ...getGlobalConfig(), fileCheckpointingEnabled: enabled, - }) + }); logEvent('tengu_file_history_snapshots_setting_changed', { enabled: enabled, - }) + }); }, }, ] @@ -533,11 +529,11 @@ export function Config({ saveGlobalConfig(current => ({ ...current, terminalProgressBarEnabled, - })) - setGlobalConfig({ ...getGlobalConfig(), terminalProgressBarEnabled }) + })); + setGlobalConfig({ ...getGlobalConfig(), terminalProgressBarEnabled }); logEvent('tengu_terminal_progress_bar_setting_changed', { enabled: terminalProgressBarEnabled, - }) + }); }, }, ...(getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_sidebar', false) @@ -551,14 +547,14 @@ export function Config({ saveGlobalConfig(current => ({ ...current, showStatusInTerminalTab, - })) + })); setGlobalConfig({ ...getGlobalConfig(), showStatusInTerminalTab, - }) + }); logEvent('tengu_terminal_tab_status_setting_changed', { enabled: showStatusInTerminalTab, - }) + }); }, }, ] @@ -569,52 +565,42 @@ export function Config({ value: globalConfig.showTurnDuration, type: 'boolean' as const, onChange(showTurnDuration: boolean) { - saveGlobalConfig(current => ({ ...current, showTurnDuration })) - setGlobalConfig({ ...getGlobalConfig(), showTurnDuration }) + 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', + value: currentDefaultPermissionMode, 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), - ), - ] + const priorityOrder: PermissionMode[] = ['default', 'plan']; + return [...priorityOrder, ...PERMISSION_MODES.filter(m => !priorityOrder.includes(m))]; })(), type: 'enum' as const, onChange(mode: string) { - const parsedMode = permissionModeFromString(mode) - // Internal modes (e.g. auto) are stored directly - const validatedMode = isExternalPermissionMode(parsedMode) - ? toExternalPermissionMode(parsedMode) - : parsedMode + const parsedMode = permissionModeFromString(mode); + // auto is an internal-only mode — store it directly, don't convert + // to its external mapping ('default') which would make it invisible. + const validatedMode = + parsedMode === 'auto' + ? parsedMode + : isExternalPermissionMode(parsedMode) + ? toExternalPermissionMode(parsedMode) + : parsedMode; const result = updateSettingsForSource('userSettings', { permissions: { ...settingsData?.permissions, - defaultMode: validatedMode as ExternalPermissionMode, + defaultMode: validatedMode as (typeof PERMISSION_MODES)[number], }, - }) + }); if (result.error) { - logError(result.error) - return + logError(result.error); + return; } // Update local state to reflect the change immediately. @@ -627,15 +613,13 @@ export function Config({ ...prev?.permissions, defaultMode: validatedMode as (typeof PERMISSION_MODES)[number], }, - })) + })); // Track changes - setChanges(prev => ({ ...prev, defaultPermissionMode: mode })) + 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, - }) + 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 @@ -643,30 +627,28 @@ export function Config({ { id: 'useAutoModeDuringPlan', label: 'Use auto mode during plan', - value: - (settingsData as { useAutoModeDuringPlan?: boolean } | undefined) - ?.useAutoModeDuringPlan ?? true, + value: (settingsData as { useAutoModeDuringPlan?: boolean } | undefined)?.useAutoModeDuringPlan ?? true, type: 'boolean' as const, onChange(useAutoModeDuringPlan: boolean) { updateSettingsForSource('userSettings', { useAutoModeDuringPlan, - }) + }); setSettingsData(prev => ({ ...prev, useAutoModeDuringPlan, - })) + })); // Internal writes suppress the file watcher, so // applySettingsChange won't fire. Reconcile directly so // mid-plan toggles take effect immediately. setAppState(prev => { - const next = transitionPlanAutoMode(prev.toolPermissionContext) - if (next === prev.toolPermissionContext) return prev - return { ...prev, toolPermissionContext: next } - }) + const next = transitionPlanAutoMode(prev.toolPermissionContext); + if (next === prev.toolPermissionContext) return prev; + return { ...prev, toolPermissionContext: next }; + }); setChanges(prev => ({ ...prev, 'Use auto mode during plan': useAutoModeDuringPlan, - })) + })); }, }, ] @@ -677,11 +659,11 @@ export function Config({ value: globalConfig.respectGitignore, type: 'boolean' as const, onChange(respectGitignore: boolean) { - saveGlobalConfig(current => ({ ...current, respectGitignore })) - setGlobalConfig({ ...getGlobalConfig(), respectGitignore }) + saveGlobalConfig(current => ({ ...current, respectGitignore })); + setGlobalConfig({ ...getGlobalConfig(), respectGitignore }); logEvent('tengu_respect_gitignore_setting_changed', { enabled: respectGitignore, - }) + }); }, }, { @@ -690,15 +672,12 @@ export function Config({ value: globalConfig.copyFullResponse, type: 'boolean' as const, onChange(copyFullResponse: boolean) { - saveGlobalConfig(current => ({ ...current, copyFullResponse })) - setGlobalConfig({ ...getGlobalConfig(), copyFullResponse }) + 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, - }) + 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, + }); }, }, // Copy-on-select is only meaningful with in-app selection (fullscreen @@ -711,15 +690,12 @@ export function Config({ value: globalConfig.copyOnSelect ?? true, type: 'boolean' as const, onChange(copyOnSelect: boolean) { - saveGlobalConfig(current => ({ ...current, copyOnSelect })) - setGlobalConfig({ ...getGlobalConfig(), copyOnSelect }) + 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, - }) + 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, + }); }, }, ] @@ -751,30 +727,19 @@ export function Config({ }, { id: 'notifChannel', - label: - feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') - ? 'Local notifications' - : 'Notifications', + 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', - ], + 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') @@ -788,11 +753,11 @@ export function Config({ saveGlobalConfig(current => ({ ...current, taskCompleteNotifEnabled, - })) + })); setGlobalConfig({ ...getGlobalConfig(), taskCompleteNotifEnabled, - }) + }); }, }, { @@ -804,11 +769,11 @@ export function Config({ saveGlobalConfig(current => ({ ...current, inputNeededNotifEnabled, - })) + })); setGlobalConfig({ ...getGlobalConfig(), inputNeededNotifEnabled, - }) + }); }, }, { @@ -820,11 +785,11 @@ export function Config({ saveGlobalConfig(current => ({ ...current, agentPushNotifEnabled, - })) + })); setGlobalConfig({ ...getGlobalConfig(), agentPushNotifEnabled, - }) + }); }, }, ] @@ -844,34 +809,27 @@ export function Config({ // 'default' means the setting is unset — currently resolves to // transcript (main.tsx falls through when defaultView !== 'chat'). // String() narrows the conditional-schema-spread union to string. - value: - settingsData?.defaultView === undefined - ? 'default' - : String(settingsData.defaultView), + 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' + 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 } - }) + if (prev.isBriefOnly === nextBrief) return prev; + return { ...prev, isBriefOnly: nextBrief }; + }); // Keep userMsgOptIn in sync so the tool list follows the view. // Two-way now (same as /brief) — accepting a cache invalidation // is better than leaving the tool on after switching away. // Reverted on Escape via initialUserMsgOptIn snapshot. - setUserMsgOptIn(nextBrief) - setChanges(prev => ({ ...prev, 'Default view': selected })) + 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, - }) + value: (defaultView ?? 'unset') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }, }, ] @@ -887,27 +845,23 @@ export function Config({ id: 'editorMode', label: 'Editor mode', // Convert 'emacs' to 'normal' for backward compatibility - value: - globalConfig.editorMode === 'emacs' - ? 'normal' - : globalConfig.editorMode || 'normal', + 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, - }) + source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }, }, { @@ -917,19 +871,19 @@ export function Config({ type: 'boolean' as const, onChange(enabled: boolean) { saveGlobalConfig(current => { - if (current.prStatusFooterEnabled === enabled) return current + if (current.prStatusFooterEnabled === enabled) return current; return { ...current, prStatusFooterEnabled: enabled, - } - }) + }; + }); setGlobalConfig({ ...getGlobalConfig(), prStatusFooterEnabled: enabled, - }) + }); logEvent('tengu_pr_status_footer_setting_changed', { enabled, - }) + }); }, }, { @@ -951,17 +905,16 @@ export function Config({ 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, - }) + source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }, }, ] @@ -974,14 +927,13 @@ export function Config({ value: globalConfig.autoConnectIde ?? false, type: 'boolean' as const, onChange(autoConnectIde: boolean) { - saveGlobalConfig(current => ({ ...current, autoConnectIde })) - setGlobalConfig({ ...getGlobalConfig(), autoConnectIde }) + 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, - }) + source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }, }, ] @@ -997,14 +949,13 @@ export function Config({ saveGlobalConfig(current => ({ ...current, autoInstallIdeExtension, - })) - setGlobalConfig({ ...getGlobalConfig(), 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, - }) + source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }, }, ] @@ -1018,63 +969,62 @@ export function Config({ saveGlobalConfig(current => ({ ...current, claudeInChromeDefaultEnabled: enabled, - })) + })); setGlobalConfig({ ...getGlobalConfig(), claudeInChromeDefaultEnabled: enabled, - }) + }); logEvent('tengu_claude_in_chrome_setting_changed', { enabled, - }) + }); }, }, // Teammate mode (only shown when agent swarms are enabled) ...(isAgentSwarmsEnabled() ? (() => { - const cliOverride = getCliTeammateModeOverride() - const label = cliOverride - ? `Teammate mode [overridden: ${cliOverride}]` - : 'Teammate mode' + const cliOverride = getCliTeammateModeOverride(); + const label = cliOverride ? `Teammate mode [overridden: ${cliOverride}]` : 'Teammate mode'; + const isWindows = getPlatform() === 'windows'; + const teammateModeOptions = isWindows + ? ['auto', 'tmux', 'windows-terminal', 'in-process'] + : ['auto', 'tmux', 'in-process']; return [ { id: 'teammateMode', label, value: globalConfig.teammateMode ?? 'auto', - options: ['auto', 'tmux', 'in-process'], + options: teammateModeOptions, type: 'enum' as const, onChange(mode: string) { - if ( - mode !== 'auto' && - mode !== 'tmux' && - mode !== 'in-process' - ) { - return + if (mode !== 'auto' && mode !== 'tmux' && mode !== 'windows-terminal' && mode !== 'in-process') { + return; + } + if (mode === 'windows-terminal' && !isWindows) { + return; } // Clear CLI override and set new mode (pass mode to avoid race condition) - clearCliTeammateModeOverride(mode) + 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, - ), + value: teammateModelDisplayString(globalConfig.teammateDefaultModel), type: 'managedEnum' as const, onChange() {}, }, - ] + ]; })() : []), // Remote at startup toggle — gated on build flag + GrowthBook + policy @@ -1093,41 +1043,36 @@ export function Config({ if (selected === 'default') { // Unset the config key so it falls back to the platform default saveGlobalConfig(current => { - if (current.remoteControlAtStartup === undefined) - return current - const next = { ...current } - delete next.remoteControlAtStartup - return next - }) + if (current.remoteControlAtStartup === undefined) return current; + const next = { ...current }; + delete next.remoteControlAtStartup; + return next; + }); setGlobalConfig({ ...getGlobalConfig(), remoteControlAtStartup: undefined, - }) + }); } else { - const enabled = selected === 'true' + const enabled = selected === 'true'; saveGlobalConfig(current => { - if (current.remoteControlAtStartup === enabled) return current - return { ...current, remoteControlAtStartup: enabled } - }) + if (current.remoteControlAtStartup === enabled) return current; + return { ...current, remoteControlAtStartup: enabled }; + }); setGlobalConfig({ ...getGlobalConfig(), remoteControlAtStartup: enabled, - }) + }); } // Sync to AppState so useReplBridge reacts immediately - const resolved = getRemoteControlAtStartup() + const resolved = getRemoteControlAtStartup(); setAppState(prev => { - if ( - prev.replBridgeEnabled === resolved && - !prev.replBridgeOutboundOnly - ) - return prev + if (prev.replBridgeEnabled === resolved && !prev.replBridgeOutboundOnly) return prev; return { ...prev, replBridgeEnabled: resolved, replBridgeOutboundOnly: false, - } - }) + }; + }); }, }, ] @@ -1138,11 +1083,11 @@ export function Config({ id: 'showExternalIncludesDialog', label: 'External CLAUDE.md includes', value: (() => { - const projectConfig = getCurrentProjectConfig() + const projectConfig = getCurrentProjectConfig(); if (projectConfig.hasClaudeMdExternalIncludesApproved) { - return 'true' + return 'true'; } else { - return 'false' + return 'false'; } })(), type: 'managedEnum' as const, @@ -1158,10 +1103,7 @@ export function Config({ id: 'apiKey', label: ( - Use custom API key:{' '} - - {normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY)} - + Use custom API key: {normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY)} ), searchText: 'Use custom API key', @@ -1174,94 +1116,82 @@ export function Config({ type: 'boolean' as const, onChange(useCustomKey: boolean) { saveGlobalConfig(current => { - const updated = { ...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, - ) + const truncatedKey = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY); if (useCustomKey) { updated.customApiKeyResponses = { ...updated.customApiKeyResponses, approved: [ - ...( - updated.customApiKeyResponses.approved ?? [] - ).filter(k => k !== truncatedKey), + ...(updated.customApiKeyResponses.approved ?? []).filter(k => k !== truncatedKey), truncatedKey, ], - rejected: ( - updated.customApiKeyResponses.rejected ?? [] - ).filter(k => k !== truncatedKey), - } + rejected: (updated.customApiKeyResponses.rejected ?? []).filter(k => k !== truncatedKey), + }; } else { updated.customApiKeyResponses = { ...updated.customApiKeyResponses, - approved: ( - updated.customApiKeyResponses.approved ?? [] - ).filter(k => k !== truncatedKey), + approved: (updated.customApiKeyResponses.approved ?? []).filter(k => k !== truncatedKey), rejected: [ - ...( - updated.customApiKeyResponses.rejected ?? [] - ).filter(k => k !== truncatedKey), + ...(updated.customApiKeyResponses.rejected ?? []).filter(k => k !== truncatedKey), truncatedKey, ], - } + }; } } - return updated - }) - setGlobalConfig(getGlobalConfig()) + return updated; + }); + setGlobalConfig(getGlobalConfig()); }, }, ] : []), - ] + ]; // Filter settings based on search query const filteredSettingsItems = React.useMemo(() => { - if (!searchQuery) return settingsItems - const lowerQuery = searchQuery.toLowerCase() + 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]) + if (setting.id.toLowerCase().includes(lowerQuery)) return true; + const searchableText = 'searchText' in setting ? setting.searchText : setting.label; + return searchableText.toLowerCase().includes(lowerQuery); + }); + }, [settingsItems, searchQuery]); // Adjust selected index when filtered list shrinks, and keep the selected // item visible when maxVisible changes (e.g., terminal resize). React.useEffect(() => { if (selectedIndex >= filteredSettingsItems.length) { - const newIndex = Math.max(0, filteredSettingsItems.length - 1) - setSelectedIndex(newIndex) - setScrollOffset(Math.max(0, newIndex - maxVisible + 1)) - return + 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]) + if (selectedIndex < prev) return selectedIndex; + if (selectedIndex >= prev + maxVisible) return selectedIndex - maxVisible + 1; + return prev; + }); + }, [filteredSettingsItems.length, selectedIndex, maxVisible]); // Keep the selected item visible within the scroll window. // Called synchronously from navigation handlers to avoid a render frame @@ -1269,13 +1199,13 @@ export function Config({ const adjustScrollOffset = useCallback( (newIndex: number) => { setScrollOffset(prev => { - if (newIndex < prev) return newIndex - if (newIndex >= prev + maxVisible) return newIndex - maxVisible + 1 - return prev - }) + if (newIndex < prev) return newIndex; + if (newIndex >= prev + maxVisible) return newIndex - maxVisible + 1; + return prev; + }); }, [maxVisible], - ) + ); // Enter: keep all changes (already persisted by onChange handlers), close // with a summary of what changed. @@ -1283,164 +1213,101 @@ export function Config({ // Submenu handling: each submenu has its own Enter/Esc — don't close // the whole panel while one is open. if (showSubmenu !== null) { - return + return; } // Log any changes that were made // TODO: Make these proper messages - 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 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)}`; + }); // Check for API key changes // On homespace, ANTHROPIC_API_KEY is preserved in process.env for child // processes but ignored by Claude Code itself (see auth.ts). - const effectiveApiKey = isRunningOnHomespace() - ? undefined - : process.env.ANTHROPIC_API_KEY + const effectiveApiKey = isRunningOnHomespace() ? undefined : process.env.ANTHROPIC_API_KEY; const initialUsingCustomKey = Boolean( effectiveApiKey && - initialConfig.current.customApiKeyResponses?.approved?.includes( - normalizeApiKeyForConfig(effectiveApiKey), - ), - ) + initialConfig.current.customApiKeyResponses?.approved?.includes(normalizeApiKeyForConfig(effectiveApiKey)), + ); const currentUsingCustomKey = Boolean( effectiveApiKey && - globalConfig.customApiKeyResponses?.approved?.includes( - normalizeApiKeyForConfig(effectiveApiKey), - ), - ) + globalConfig.customApiKeyResponses?.approved?.includes(normalizeApiKeyForConfig(effectiveApiKey)), + ); if (initialUsingCustomKey !== currentUsingCustomKey) { - formattedChanges.push( - `${currentUsingCustomKey ? 'Enabled' : 'Disabled'} custom API key`, - ) + 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, - }) + 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)}`) + 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 (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)}`, - ) + formattedChanges.push(`Set output style to ${chalk.bold(currentOutputStyle)}`); } if (currentLanguage !== initialLanguage.current) { - formattedChanges.push( - `Set response language to ${chalk.bold(currentLanguage ?? 'Default (English)')}`, - ) + 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')}`, - ) + 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)}`, - ) + 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`, - ) + formattedChanges.push(`${globalConfig.autoConnectIde ? 'Enabled' : 'Disabled'} auto-connect to IDE`); } - if ( - globalConfig.autoInstallIdeExtension !== - initialConfig.current.autoInstallIdeExtension - ) { + 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.autoCompactEnabled !== initialConfig.current.autoCompactEnabled) { + formattedChanges.push(`${globalConfig.autoCompactEnabled ? 'Enabled' : 'Disabled'} auto-compact`); } - if ( - globalConfig.respectGitignore !== initialConfig.current.respectGitignore - ) { + 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.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`, - ) + formattedChanges.push(`${globalConfig.copyOnSelect ? 'Enabled' : 'Disabled'} copy on select`); } - if ( - globalConfig.terminalProgressBarEnabled !== - initialConfig.current.terminalProgressBarEnabled - ) { + 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.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.showTurnDuration !== initialConfig.current.showTurnDuration) { + formattedChanges.push(`${globalConfig.showTurnDuration ? 'Enabled' : 'Disabled'} turn duration`); } - if ( - globalConfig.remoteControlAtStartup !== - initialConfig.current.remoteControlAtStartup - ) { + 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) + : `${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 (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')) + onClose(formattedChanges.join('\n')); } else { - onClose('Config dialog dismissed', { display: 'system' }) + onClose('Config dialog dismissed', { display: 'system' }); } }, [ showSubmenu, @@ -1450,11 +1317,9 @@ export function Config({ currentOutputStyle, currentLanguage, settingsData?.autoUpdatesChannel, - isFastModeEnabled() - ? (settingsData as Record | undefined)?.fastMode - : undefined, + isFastModeEnabled() ? (settingsData as Record | undefined)?.fastMode : undefined, onClose, - ]) + ]); // Restore all state stores to their mount-time snapshots. Changes are // applied to disk/AppState immediately on toggle, so "cancel" means @@ -1464,22 +1329,22 @@ export function Config({ // config overwrite since setTheme internally calls saveGlobalConfig with // a partial update — we want the full snapshot to be the last write. if (themeSetting !== initialThemeSetting.current) { - setTheme(initialThemeSetting.current) + setTheme(initialThemeSetting.current); } // Global config: full overwrite from snapshot. saveGlobalConfig skips if // the returned ref equals current (test mode checks ref; prod writes to // disk but content is identical). - saveGlobalConfig(() => initialConfig.current) + saveGlobalConfig(() => initialConfig.current); // Settings files: restore each key Config may have touched. undefined // deletes the key (updateSettingsForSource customizer at settings.ts:368). - const il = initialLocalSettings + const il = initialLocalSettings; updateSettingsForSource('localSettings', { spinnerTipsEnabled: il?.spinnerTipsEnabled, prefersReducedMotion: il?.prefersReducedMotion, defaultView: il?.defaultView, outputStyle: il?.outputStyle, - }) - const iu = initialUserSettings + }); + const iu = initialUserSettings; updateSettingsForSource('userSettings', { alwaysThinkingEnabled: iu?.alwaysThinkingEnabled, fastMode: iu?.fastMode, @@ -1489,9 +1354,7 @@ export function Config({ language: iu?.language, ...(feature('TRANSCRIPT_CLASSIFIER') ? { - useAutoModeDuringPlan: ( - iu as { useAutoModeDuringPlan?: boolean } | undefined - )?.useAutoModeDuringPlan, + useAutoModeDuringPlan: (iu as { useAutoModeDuringPlan?: boolean } | undefined)?.useAutoModeDuringPlan, } : {}), // ThemePicker's Ctrl+T writes this key directly — include it so the @@ -1504,12 +1367,10 @@ export function Config({ // Explicitly include defaultMode so undefined triggers the customizer's // delete path even when iu.permissions lacks that key. permissions: - iu?.permissions === undefined - ? undefined - : { ...iu.permissions, defaultMode: iu.permissions.defaultMode }, - }) + iu?.permissions === undefined ? undefined : { ...iu.permissions, defaultMode: iu.permissions.defaultMode }, + }); // AppState: batch-restore all possibly-touched fields. - const ia = initialAppState + const ia = initialAppState; setAppState(prev => ({ ...prev, mainLoopModel: ia.mainLoopModel, @@ -1525,12 +1386,12 @@ export function Config({ // Reconcile auto-mode state after useAutoModeDuringPlan revert above — // the onChange handler may have activated/deactivated auto mid-plan. toolPermissionContext: transitionPlanAutoMode(prev.toolPermissionContext), - })) + })); // Bootstrap state: restore userMsgOptIn. Only touched by the defaultView // onChange above, so no feature() guard needed here (that path only // exists when showDefaultViewPicker is true). if (getUserMsgOptIn() !== initialUserMsgOptIn) { - setUserMsgOptIn(initialUserMsgOptIn) + setUserMsgOptIn(initialUserMsgOptIn); } }, [ themeSetting, @@ -1540,18 +1401,18 @@ export function Config({ initialAppState, initialUserMsgOptIn, setAppState, - ]) + ]); // Escape: revert all changes (if any) and close. const handleEscape = useCallback(() => { if (showSubmenu !== null) { - return + return; } if (isDirty.current) { - revertChanges() + revertChanges(); } - onClose('Config dialog dismissed', { display: 'system' }) - }, [showSubmenu, revertChanges, onClose]) + onClose('Config dialog dismissed', { display: 'system' }); + }, [showSubmenu, revertChanges, onClose]); // Disable when submenu is open so the submenu's Dialog handles ESC, and in // search mode so the onKeyDown handler (which clears-then-exits search) @@ -1559,35 +1420,35 @@ export function Config({ useKeybinding('confirm:no', handleEscape, { context: 'Settings', isActive: showSubmenu === null && !isSearchMode && !headerFocused, - }) + }); // Save-and-close fires on Enter only when not in search mode (Enter there // exits search to the list — see the isSearchMode branch in handleKeyDown). useKeybinding('settings:close', handleSaveAndClose, { context: 'Settings', isActive: showSubmenu === null && !isSearchMode && !headerFocused, - }) + }); // Settings navigation and toggle actions via configurable keybindings. // Only active when not in search mode and no submenu is open. const toggleSetting = useCallback(() => { - const setting = filteredSettingsItems[selectedIndex] + const setting = filteredSettingsItems[selectedIndex]; if (!setting || !setting.onChange) { - return + return; } if (setting.type === 'boolean') { - isDirty.current = true - setting.onChange(!setting.value) + isDirty.current = true; + setting.onChange(!setting.value); if (setting.id === 'thinkingEnabled') { - const newValue = !setting.value - const backToInitial = newValue === initialThinkingEnabled.current + const newValue = !setting.value; + const backToInitial = newValue === initialThinkingEnabled.current; if (backToInitial) { - setShowThinkingWarning(false) + setShowThinkingWarning(false); } else if (context.messages.some(m => m.type === 'assistant')) { - setShowThinkingWarning(true) + setShowThinkingWarning(true); } } - return + return; } if ( @@ -1602,70 +1463,69 @@ export function Config({ // completion callback, not here (submenu may be cancelled). switch (setting.id) { case 'theme': - setShowSubmenu('Theme') - setTabsHidden(true) - return + setShowSubmenu('Theme'); + setTabsHidden(true); + return; case 'model': - setShowSubmenu('Model') - setTabsHidden(true) - return + setShowSubmenu('Model'); + setTabsHidden(true); + return; case 'teammateDefaultModel': - setShowSubmenu('TeammateModel') - setTabsHidden(true) - return + setShowSubmenu('TeammateModel'); + setTabsHidden(true); + return; case 'showExternalIncludesDialog': - setShowSubmenu('ExternalIncludes') - setTabsHidden(true) - return + setShowSubmenu('ExternalIncludes'); + setTabsHidden(true); + return; case 'outputStyle': - setShowSubmenu('OutputStyle') - setTabsHidden(true) - return + setShowSubmenu('OutputStyle'); + setTabsHidden(true); + return; case 'language': - setShowSubmenu('Language') - setTabsHidden(true) - return + setShowSubmenu('Language'); + setTabsHidden(true); + return; } } if (setting.id === 'autoUpdatesChannel') { if (autoUpdaterDisabledReason) { // Auto-updates are disabled - show enable dialog instead - setShowSubmenu('EnableAutoUpdates') - setTabsHidden(true) - return + setShowSubmenu('EnableAutoUpdates'); + setTabsHidden(true); + return; } - const currentChannel = settingsData?.autoUpdatesChannel ?? 'latest' + const currentChannel = settingsData?.autoUpdatesChannel ?? 'latest'; if (currentChannel === 'latest') { // Switching to stable - show downgrade dialog - setShowSubmenu('ChannelDowngrade') - setTabsHidden(true) + setShowSubmenu('ChannelDowngrade'); + setTabsHidden(true); } else { // Switching to latest - just do it and clear minimumVersion - isDirty.current = true + 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, - }) + channel: 'latest' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); } - return + 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 + isDirty.current = true; + const currentIndex = setting.options.indexOf(setting.value); + const nextIndex = (currentIndex + 1) % setting.options.length; + setting.onChange(setting.options[nextIndex]!); + return; } }, [ autoUpdaterDisabledReason, @@ -1673,17 +1533,14 @@ export function Config({ 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) - } + setShowThinkingWarning(false); + const newIndex = Math.max(0, Math.min(filteredSettingsItems.length - 1, selectedIndex + delta)); + setSelectedIndex(newIndex); + adjustScrollOffset(newIndex); + }; useKeybindings( { @@ -1692,11 +1549,11 @@ export function Config({ // ↑ at top enters search mode so users can type-to-filter after // reaching the list boundary. Wheel-up (scroll:lineUp) clamps // instead — overshoot shouldn't move focus away from the list. - setShowThinkingWarning(false) - setIsSearchMode(true) - setScrollOffset(0) + setShowThinkingWarning(false); + setIsSearchMode(true); + setScrollOffset(0); } else { - moveSelection(-1) + moveSelection(-1); } }, 'select:next': () => moveSelection(1), @@ -1707,93 +1564,82 @@ export function Config({ 'scroll:lineUp': () => moveSelection(-1), 'scroll:lineDown': () => moveSelection(1), 'select:accept': toggleSetting, + 'select:previousValue': () => toggleSetting(), + 'select:nextValue': () => toggleSetting(), 'settings:search': () => { - setIsSearchMode(true) - setSearchQuery('') + setIsSearchMode(true); + setSearchQuery(''); }, }, { context: 'Settings', isActive: showSubmenu === null && !isSearchMode && !headerFocused, }, - ) + ); // Combined key handling across search/list modes. Branch order mirrors // the original useInput gate priority: submenu and header short-circuit // first (their own handlers own input), then search vs. list. const handleKeyDown = useCallback( (e: KeyboardEvent) => { - if (showSubmenu !== null) return - if (headerFocused) return + if (showSubmenu !== null) return; + if (headerFocused) return; // Search mode: Esc clears then exits, Enter/↓ moves to the list. if (isSearchMode) { if (e.key === 'escape') { - e.preventDefault() + e.preventDefault(); if (searchQuery.length > 0) { - setSearchQuery('') + setSearchQuery(''); } else { - setIsSearchMode(false) + setIsSearchMode(false); } - return + return; } if (e.key === 'return' || e.key === 'down' || e.key === 'wheeldown') { - e.preventDefault() - setIsSearchMode(false) - setSelectedIndex(0) - setScrollOffset(0) + e.preventDefault(); + setIsSearchMode(false); + setSelectedIndex(0); + setScrollOffset(0); } - return + return; } // List mode: left/right/tab cycle the selected option's value. These // keys used to switch tabs; now they only do so when the tab row is // explicitly focused (see headerFocused in Settings.tsx). if (e.key === 'left' || e.key === 'right' || e.key === 'tab') { - e.preventDefault() - toggleSetting() - return + e.preventDefault(); + toggleSetting(); + return; } // Fallback: printable characters (other than those bound to actions) // enter search mode. Carve out j/k// — useKeybindings (still on the // useInput path) consumes these via stopImmediatePropagation, but // onKeyDown dispatches independently so we must skip them explicitly. - if (e.ctrl || e.meta) return - if (e.key === 'j' || e.key === 'k' || e.key === '/') 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) + e.preventDefault(); + setIsSearchMode(true); + setSearchQuery(e.key); } }, - [ - showSubmenu, - headerFocused, - isSearchMode, - searchQuery, - setSearchQuery, - toggleSetting, - ], - ) + [showSubmenu, headerFocused, isSearchMode, searchQuery, setSearchQuery, toggleSetting], + ); return ( - + {showSubmenu === 'Theme' ? ( <> { - isDirty.current = true - setTheme(setting) - setShowSubmenu(null) - setTabsHidden(false) + isDirty.current = true; + setTheme(setting); + setShowSubmenu(null); + setTabsHidden(false); }} onCancel={() => { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); }} hideEscToCancel skipExitHandling={true} // Skip exit handling as Config already handles it @@ -1817,20 +1663,18 @@ export function Config({ { - isDirty.current = true - onChangeMainModelConfig(model) - setShowSubmenu(null) - setTabsHidden(false) + isDirty.current = true; + onChangeMainModelConfig(model); + setShowSubmenu(null); + setTabsHidden(false); }} onCancel={() => { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); }} showFastModeNotice={ isFastModeEnabled() - ? isFastMode && - isFastModeSupportedByModel(mainLoopModel) && - isFastModeAvailable() + ? isFastMode && isFastModeSupportedByModel(mainLoopModel) && isFastModeAvailable() : false } /> @@ -1853,39 +1697,33 @@ export function Config({ 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) + 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 + if (globalConfig.teammateDefaultModel === undefined && model === null) { + return; } - isDirty.current = true + isDirty.current = true; saveGlobalConfig(current => - current.teammateDefaultModel === model - ? current - : { ...current, teammateDefaultModel: model }, - ) + 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, - }) + model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }} onCancel={() => { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); }} /> @@ -1904,8 +1742,8 @@ export function Config({ <> { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); }} externalIncludes={getExternalClaudeMdIncludes(memoryFiles as MemoryFileInfo[])} /> @@ -1926,28 +1764,26 @@ export function Config({ { - isDirty.current = true - setCurrentOutputStyle(style ?? DEFAULT_OUTPUT_STYLE_NAME) - setShowSubmenu(null) - setTabsHidden(false) + 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, - }) + 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) + setShowSubmenu(null); + setTabsHidden(false); }} /> @@ -1967,37 +1803,30 @@ export function Config({ { - isDirty.current = true - setCurrentLanguage(language) - setShowSubmenu(null) - setTabsHidden(false) + 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, - }) + 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) + setShowSubmenu(null); + setTabsHidden(false); }} /> - + @@ -2005,8 +1834,8 @@ export function Config({ { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); }} hideBorder hideInputGuide @@ -2019,10 +1848,7 @@ export function Config({ : 'Auto-updates are disabled in development builds.'} {autoUpdaterDisabledReason?.type === 'env' && ( - - Unset {autoUpdaterDisabledReason.envVar} to re-enable - auto-updates. - + Unset {autoUpdaterDisabledReason.envVar} to re-enable auto-updates. )} ) : ( @@ -2038,29 +1864,28 @@ export function Config({ }, ]} onChange={(channel: string) => { - isDirty.current = true - setShowSubmenu(null) - setTabsHidden(false) + isDirty.current = true; + setShowSubmenu(null); + setTabsHidden(false); saveGlobalConfig(current => ({ ...current, autoUpdates: true, - })) - setGlobalConfig({ ...getGlobalConfig(), 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, - }) + channel: channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); }} /> )} @@ -2069,46 +1894,41 @@ export function Config({ { - setShowSubmenu(null) - setTabsHidden(false) + setShowSubmenu(null); + setTabsHidden(false); if (choice === 'cancel') { // User cancelled - don't change anything - return + return; } - isDirty.current = true + isDirty.current = true; // Switch to stable channel const newSettings: { - autoUpdatesChannel: 'stable' - minimumVersion?: string + autoUpdatesChannel: 'stable'; + minimumVersion?: string; } = { autoUpdatesChannel: 'stable', - } + }; if (choice === 'stay') { // User wants to stay on current version until stable catches up - newSettings.minimumVersion = MACRO.VERSION + newSettings.minimumVersion = MACRO.VERSION; } - updateSettingsForSource('userSettings', newSettings) + updateSettingsForSource('userSettings', newSettings); setSettingsData(prev => ({ ...prev, ...newSettings, - })) + })); logEvent('tengu_autoupdate_channel_changed', { - channel: - 'stable' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + channel: 'stable' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, minimum_version_set: choice === 'stay', - }) + }); }} /> ) : ( - + )} - {filteredSettingsItems - .slice(scrollOffset, scrollOffset + maxVisible) - .map((setting, i) => { - const actualIndex = scrollOffset + i - const isSelected = - actualIndex === selectedIndex && - !headerFocused && - !isSearchMode + {filteredSettingsItems.slice(scrollOffset, scrollOffset + maxVisible).map((setting, i) => { + const actualIndex = scrollOffset + i; + const isSelected = actualIndex === selectedIndex && !headerFocused && !isSearchMode; - return ( - - - - - {isSelected ? figures.pointer : ' '}{' '} - {setting.label} - - - - {setting.type === 'boolean' ? ( - <> - - {setting.value.toString()} - - {showThinkingWarning && - setting.id === 'thinkingEnabled' && ( - - {' '} - Changing thinking mode mid-conversation - will increase latency and may reduce - quality. - - )} - - ) : setting.id === 'theme' ? ( - - {THEME_LABELS[setting.value.toString()] ?? - setting.value.toString()} - - ) : setting.id === 'notifChannel' ? ( - - - - ) : setting.id === 'defaultPermissionMode' ? ( - - {permissionModeTitle( - setting.value as PermissionMode, - )} - - ) : setting.id === 'autoUpdatesChannel' && - autoUpdaterDisabledReason ? ( - - - disabled - - - ( - {formatAutoUpdaterDisabledReason( - autoUpdaterDisabledReason, - )} - ) - - - ) : ( - - {setting.value.toString()} - - )} - + return ( + + + + + {isSelected ? figures.pointer : ' '} {setting.label} + - - ) - })} + + {setting.type === 'boolean' ? ( + <> + {setting.value.toString()} + {showThinkingWarning && setting.id === 'thinkingEnabled' && ( + + {' '} + Changing thinking mode mid-conversation will increase latency and may reduce quality. + + )} + + ) : setting.id === 'theme' ? ( + + {THEME_LABELS[setting.value.toString()] ?? setting.value.toString()} + + ) : setting.id === 'notifChannel' ? ( + + + + ) : setting.id === 'defaultPermissionMode' ? ( + + {permissionModeShortTitle(setting.value as PermissionMode)} + + ) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? ( + + disabled + ({formatAutoUpdaterDisabledReason(autoUpdaterDisabledReason)}) + + ) : ( + {setting.value.toString()} + )} + + + + ); + })} {scrollOffset + maxVisible < filteredSettingsItems.length && ( - {figures.arrowDown}{' '} - {filteredSettingsItems.length - scrollOffset - maxVisible}{' '} - more below + {figures.arrowDown} {filteredSettingsItems.length - scrollOffset - maxVisible} more below )} @@ -2230,12 +2009,7 @@ export function Config({ - + ) : isSearchMode ? ( @@ -2244,12 +2018,7 @@ export function Config({ Type to filter - + ) : ( @@ -2273,27 +2042,22 @@ export function Config({ fallback="/" description="search" /> - + )} )} - ) + ); } function teammateModelDisplayString(value: string | null | undefined): string { if (value === undefined) { - return modelDisplayString(getHardcodedTeammateModelFallback()) + return modelDisplayString(getHardcodedTeammateModelFallback()); } - if (value === null) return "Default (leader's model)" - return modelDisplayString(value) + if (value === null) return "Default (leader's model)"; + return modelDisplayString(value); } const THEME_LABELS: Record = { @@ -2304,41 +2068,41 @@ const THEME_LABELS: Record = { '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' + return 'Auto'; case 'iterm2': return ( iTerm2 (OSC 9) - ) + ); case 'terminal_bell': return ( Terminal Bell (\a) - ) + ); case 'kitty': return ( Kitty (OSC 99) - ) + ); case 'ghostty': return ( Ghostty (OSC 777) - ) + ); case 'iterm2_with_bell': - return 'iTerm2 w/ Bell' + return 'iTerm2 w/ Bell'; case 'notifications_disabled': - return 'Disabled' + return 'Disabled'; default: - return value + return value; } } diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index 22f900aa6..d4784da84 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -1,5 +1,6 @@ import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js' import { getInitialSettings } from '../settings/settings.js' +import type { SettingsJson } from '../settings/types.js' import { isEnvTruthy } from '../envUtils.js' export type APIProvider = @@ -9,11 +10,15 @@ export type APIProvider = | 'foundry' | 'openai' | 'gemini' + | 'grok' -export function getAPIProvider(): APIProvider { - const modelType = getInitialSettings().modelType +export function getAPIProvider( + settings: Pick = getInitialSettings(), +): APIProvider { + const modelType = settings.modelType if (modelType === 'openai') return 'openai' if (modelType === 'gemini') return 'gemini' + if (modelType === 'grok') return 'grok' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) return 'bedrock' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) return 'vertex' @@ -21,6 +26,7 @@ export function getAPIProvider(): APIProvider { if (isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) return 'openai' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) return 'gemini' + if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GROK)) return 'grok' return 'firstParty' } @@ -36,6 +42,7 @@ export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS */ export function isFirstPartyAnthropicBaseUrl(): boolean { const baseUrl = process.env.ANTHROPIC_BASE_URL + // TODO: 这里会有问题, 只配置了 openai 协议的用户, 按理说会为 true 导致问题 if (!baseUrl) { return true }