diff --git a/packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsx b/packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsx index 2b5d7f7fb..afa469c46 100644 --- a/packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsx +++ b/packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsx @@ -421,7 +421,7 @@ export const PowerShellTool = buildTool({ isSearch: boolean isRead: boolean } { - if (!input.command) { + if (!input?.command) { return { isSearch: false, isRead: false } } return isSearchOrReadPowerShellCommand(input.command) diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx index 8130c8ef1..ab3814786 100644 --- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx +++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx @@ -42,7 +42,7 @@ import { usePrStatus } from '../../hooks/usePrStatus.js' import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { useTerminalSize } from '../../hooks/useTerminalSize.js' import { useTasksV2 } from '../../hooks/useTasksV2.js' -import { formatDuration } from '../../utils/format.js' +import { formatDuration, formatFileSize } from '../../utils/format.js' import { VoiceWarmupHint } from './VoiceIndicator.js' import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js' import { useVoiceState } from '../../context/voice.js' @@ -63,6 +63,26 @@ const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {} const NULL = () => null const MAX_VOICE_HINT_SHOWS = 3 +const RSS_UPDATE_INTERVAL_MS = 5_000 + +type RssState = { text: string; level: 'normal' | 'warning' | 'error' } + +function useRssDisplay(): RssState | null { + const [state, setState] = useState(null) + useEffect(() => { + function update(): void { + const mb = process.memoryUsage().rss / (1024 * 1024) + const level = mb >= 1024 ? 'error' : mb >= 512 ? 'warning' : 'normal' + const text = formatFileSize(mb * 1024 * 1024) + setState(prev => (prev?.text === text ? prev : { text, level })) + } + update() + const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS) + return () => clearInterval(timer) + }, []) + return state +} + type Props = { exitMessage: { show: boolean @@ -315,6 +335,7 @@ function ModeIndicator({ const isKillAgentsConfirmShowing = useAppState( s => s.notifications.current?.key === 'kill-agents-confirm', ) + const rssState = useRssDisplay() // Derive team info from teamContext (no filesystem I/O needed) // Match the same logic as TeamStatus to avoid trailing separator @@ -428,6 +449,18 @@ function ModeIndicator({ />, ] : []), + // RSS memory indicator — always visible + ...(rssState + ? [ + + {rssState.text} + , + ] + : []), ] // Check if any in-process teammates exist (for hint text cycling) diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 8c84a0360..7f8207d0f 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -16,6 +16,7 @@ import { import chalk from 'chalk'; import { permissionModeTitle, + permissionModeShortTitle, permissionModeFromString, toExternalPermissionMode, isExternalPermissionMode, @@ -153,7 +154,7 @@ export function Config({ const initialLanguage = React.useRef(currentLanguage); const [selectedIndex, setSelectedIndex] = useState(0); const [scrollOffset, setScrollOffset] = useState(0); - const [isSearchMode, setIsSearchMode] = useState(true); + const [isSearchMode, setIsSearchMode] = useState(false); const isTerminalFocused = useTerminalFocus(); const { rows } = useTerminalSize(); // contentHeight is set by Settings.tsx (same value passed to Tabs to fix @@ -167,6 +168,9 @@ export function Config({ 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). @@ -558,27 +562,23 @@ export function Config({ { 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))]; + 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; + // 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], }, }); @@ -1548,6 +1548,8 @@ 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(''); @@ -1936,13 +1938,13 @@ export function Config({ return ( - + {isSelected ? figures.pointer : ' '} {setting.label} - + {setting.type === 'boolean' ? ( <> {setting.value.toString()} @@ -1963,7 +1965,7 @@ export function Config({ ) : setting.id === 'defaultPermissionMode' ? ( - {permissionModeTitle(setting.value as PermissionMode)} + {permissionModeShortTitle(setting.value as PermissionMode)} ) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? ( diff --git a/src/keybindings/defaultBindings.ts b/src/keybindings/defaultBindings.ts index e9562fcb0..1d9ef10e2 100644 --- a/src/keybindings/defaultBindings.ts +++ b/src/keybindings/defaultBindings.ts @@ -117,6 +117,9 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [ j: 'select:next', 'ctrl+p': 'select:previous', 'ctrl+n': 'select:next', + // Cycle enum values left/right (same as left/right arrow in handleKeyDown) + left: 'select:previousValue', + right: 'select:nextValue', // Toggle/activate the selected setting (space only — enter saves & closes) space: 'select:accept', // Save and close the config panel diff --git a/src/keybindings/schema.ts b/src/keybindings/schema.ts index 8f15231d2..83e6fb28d 100644 --- a/src/keybindings/schema.ts +++ b/src/keybindings/schema.ts @@ -168,6 +168,8 @@ export const KEYBINDING_ACTIONS = [ 'settings:search', 'settings:retry', 'settings:close', + 'select:previousValue', + 'select:nextValue', // Voice actions 'voice:pushToTalk', ] as const diff --git a/src/services/langfuse/__tests__/langfuse.test.ts b/src/services/langfuse/__tests__/langfuse.test.ts index dab33fd60..6c5a87947 100644 --- a/src/services/langfuse/__tests__/langfuse.test.ts +++ b/src/services/langfuse/__tests__/langfuse.test.ts @@ -231,6 +231,7 @@ describe('Langfuse integration', () => { test('merges assistant tool calls from OpenAI-style array content', async () => { const { convertMessagesToLangfuse } = await import('../convert.js') + // Content part with embedded tool_calls is non-standard; cast for defensive test const result = convertMessagesToLangfuse([ { role: 'assistant', @@ -255,7 +256,7 @@ describe('Langfuse integration', () => { }, ], }, - ]) + ] as any) expect(result).toEqual([ { diff --git a/src/services/langfuse/convert.ts b/src/services/langfuse/convert.ts index ad324c2a0..411692bce 100644 --- a/src/services/langfuse/convert.ts +++ b/src/services/langfuse/convert.ts @@ -10,7 +10,8 @@ * - tool_result blocks → separate { role: 'tool' } messages */ -import type { AssistantMessage } from 'src/types/message.js' +import type { AssistantMessage, UserMessage } from 'src/types/message.js' +import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions.mjs' type LangfuseContentPart = | { type: 'text'; text: string } @@ -79,6 +80,12 @@ function mergeToolCalls( return [...merged.values()] } +/** Union of all message formats accepted by Langfuse converters. */ +type LangfuseInputMessage = + | UserMessage + | AssistantMessage + | ChatCompletionMessageParam + /** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */ function toContentPart(block: Record): LangfuseContentPart | null { const type = block.type as string | undefined @@ -178,7 +185,7 @@ function toRoleFromWrappedMessage(msg: Record): 'user' | 'assis /** Convert internal or OpenAI-style messages → Langfuse input format */ export function convertMessagesToLangfuse( - messages: readonly unknown[], + messages: readonly LangfuseInputMessage[], systemPrompt?: readonly string[], ): LangfuseChatMessage[] { const result: LangfuseChatMessage[] = [] diff --git a/src/types/permissions.ts b/src/types/permissions.ts index 32ef776b2..b0bfe917c 100644 --- a/src/types/permissions.ts +++ b/src/types/permissions.ts @@ -30,9 +30,11 @@ export type PermissionMode = InternalPermissionMode // Runtime validation set: modes that are user-addressable (settings.json // defaultMode, --permission-mode CLI flag, conversation recovery). +// 'auto' is always available — when TRANSCRIPT_CLASSIFIER is off, the +// classifier is unavailable and auto mode falls back to prompting. export const INTERNAL_PERMISSION_MODES = [ ...EXTERNAL_PERMISSION_MODES, - ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)), + 'auto' as const, ] as const satisfies readonly PermissionMode[] export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES diff --git a/src/utils/permissions/PermissionMode.ts b/src/utils/permissions/PermissionMode.ts index 0dbd93639..ac02d9bbf 100644 --- a/src/utils/permissions/PermissionMode.ts +++ b/src/utils/permissions/PermissionMode.ts @@ -64,7 +64,7 @@ const PERMISSION_MODE_CONFIG: Partial< external: 'acceptEdits', }, bypassPermissions: { - title: 'Bypass Permissions', + title: 'Bypass', shortTitle: 'Bypass', symbol: '⏵⏵', color: 'error', @@ -77,17 +77,13 @@ const PERMISSION_MODE_CONFIG: Partial< color: 'error', external: 'dontAsk', }, - ...(feature('TRANSCRIPT_CLASSIFIER') - ? { - auto: { - title: 'Auto mode', - shortTitle: 'Auto', - symbol: '⏵⏵', - color: 'warning' as ModeColorKey, - external: 'default' as ExternalPermissionMode, - }, - } - : {}), + auto: { + title: 'Auto', + shortTitle: 'Auto', + symbol: '⏵⏵', + color: 'warning' as ModeColorKey, + external: 'default' as ExternalPermissionMode, + }, } /** diff --git a/src/utils/permissions/__tests__/PermissionMode.test.ts b/src/utils/permissions/__tests__/PermissionMode.test.ts index 0f2d065d9..100bd81cc 100644 --- a/src/utils/permissions/__tests__/PermissionMode.test.ts +++ b/src/utils/permissions/__tests__/PermissionMode.test.ts @@ -70,7 +70,7 @@ describe("permissionModeTitle", () => { expect(permissionModeTitle("default")).toBe("Default"); expect(permissionModeTitle("plan")).toBe("Plan Mode"); expect(permissionModeTitle("acceptEdits")).toBe("Accept edits"); - expect(permissionModeTitle("bypassPermissions")).toBe("Bypass Permissions"); + expect(permissionModeTitle("bypassPermissions")).toBe("Bypass"); expect(permissionModeTitle("dontAsk")).toBe("Don't Ask"); }); diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index f715c568d..e7b0bbfb5 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -57,11 +57,7 @@ export const PermissionsSchema = lazySchema(() => 'List of permission rules that should always prompt for confirmation', ), defaultMode: z - .enum( - feature('TRANSCRIPT_CLASSIFIER') - ? PERMISSION_MODES - : EXTERNAL_PERMISSION_MODES, - ) + .enum(PERMISSION_MODES) .optional() .describe('Default permission mode when Claude Code needs access'), disableBypassPermissionsMode: z