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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 34 additions & 1 deletion src/components/PromptInput/PromptInputFooterLeftSide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<RssState | null>(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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -428,6 +449,18 @@ function ModeIndicator({
/>,
]
: []),
// RSS memory indicator — always visible
...(rssState
? [
<Text
key="rss"
dimColor={rssState.level === 'normal'}
color={rssState.level === 'error' ? 'error' : rssState.level === 'warning' ? 'warning' : undefined}
>
{rssState.text}
</Text>,
]
: []),
Comment on lines +452 to +463
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Always-on RSS entry breaks the empty-state fallbacks.

useRssDisplay() calls update() synchronously in its useEffect, so rssState becomes non-null on the second render and stays non-null for the rest of the session. That means parts will essentially always contain ≥1 element, which silently disables two existing branches:

  • Line 554: if (parts.length === 0 && !tasksPart && !modePart && showHint) — the "? for shortcuts" hint will no longer be shown to new users on an otherwise empty footer.
  • Line 640: if (parts.length === 0 && !tasksPart && !modePart) — the fullscreen row-reservation fallback (the deliberately-rendered <Text> </Text>) is now dead code; the component will instead render the truncating Box. The comment block above line 632 explicitly calls out that this empty-parts path matters for stable footer height.

Either gate the RSS push so it doesn't count for these checks, or push it last and re-evaluate emptiness against a "real content" subset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx` around lines 452 -
463, The always-on RSS entry (rssState from useRssDisplay) causes parts to be
non-empty and breaks the empty-state fallbacks; fix by ensuring the RSS element
does not count toward the "real" emptiness checks: either stop pushing the RSS
element into parts used by the emptiness logic (gate it behind a separate
rssPart variable or push it into partsAfterEmptyChecks) or push the RSS element
last and change all empty checks to compute emptiness from a filteredParts =
parts.filter(p => p !== rssPart) (used by the if (parts.length === 0 &&
!tasksPart && !modePart && showHint) and if (parts.length === 0 && !tasksPart &&
!modePart) conditions); update references to rssState, parts, tasksPart,
modePart, and showHint accordingly so the RSS indicator is rendered but does not
disable the empty-state branches.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the context of the file and the rssState
head -n 470 src/components/PromptInput/PromptInputFooterLeftSide.tsx | tail -n 30

Repository: claude-code-best/claude-code

Length of output: 946


🏁 Script executed:

# Check for rssState definition and initialization
rg "rssState" src/components/PromptInput/PromptInputFooterLeftSide.tsx -B 3 -A 1

Repository: claude-code-best/claude-code

Length of output: 585


🏁 Script executed:

# Search for isPrStatusEnabled pattern mentioned in the review
rg "isPrStatusEnabled" --type ts --type tsx

Repository: claude-code-best/claude-code

Length of output: 100


🏁 Script executed:

# Search for getGlobalConfig usage pattern
rg "getGlobalConfig" --type ts --type tsx | head -20

Repository: claude-code-best/claude-code

Length of output: 100


🏁 Script executed:

# Search for feature flag patterns in the codebase
rg "feature\(" --type ts --type tsx | head -20

Repository: claude-code-best/claude-code

Length of output: 100


🏁 Script executed:

# Check if there are other RSS-related configurations
rg "rss" src/ --type ts --type tsx -i | head -30

Repository: claude-code-best/claude-code

Length of output: 100


🏁 Script executed:

# Search for useRssDisplay hook definition
rg "useRssDisplay" -A 10

Repository: claude-code-best/claude-code

Length of output: 2259


🏁 Script executed:

# Search for isPrStatusEnabled without type filter
rg "isPrStatusEnabled" -B 2 -A 5

Repository: claude-code-best/claude-code

Length of output: 1975


🏁 Script executed:

# Search for feature flag usage patterns
rg "feature\(" -B 1 -A 1 | head -40

Repository: claude-code-best/claude-code

Length of output: 2409


🏁 Script executed:

# Check the imports and context of the PromptInputFooterLeftSide file
head -n 50 src/components/PromptInput/PromptInputFooterLeftSide.tsx

Repository: claude-code-best/claude-code

Length of output: 2573


🏁 Script executed:

# Look for config/shouldShow patterns related to RSS
rg "shouldShow|config\|getGlobalConfig" src/ -A 2 -B 2 | head -50

Repository: claude-code-best/claude-code

Length of output: 2451


🏁 Script executed:

# Find getGlobalConfig definition
rg "getGlobalConfig" -B 2 -A 10 | head -60

Repository: claude-code-best/claude-code

Length of output: 4131


🏁 Script executed:

# Check if rssFooterEnabled exists in config
rg "rssFooterEnabled"

Repository: claude-code-best/claude-code

Length of output: 54


🏁 Script executed:

# Find the config file structure
fd "config" -type f | grep -E "\.(ts|js)$" | head -10

Repository: claude-code-best/claude-code

Length of output: 243


🏁 Script executed:

# Search for GlobalConfig type definition
rg "interface GlobalConfig|type GlobalConfig" -A 15

Repository: claude-code-best/claude-code

Length of output: 5876


🏁 Script executed:

# Check RSS_UPDATE_INTERVAL_MS definition
rg "RSS_UPDATE_INTERVAL_MS"

Repository: claude-code-best/claude-code

Length of output: 285


🏁 Script executed:

# Get the full GlobalConfig type definition
sed -n '/^export type GlobalConfig = {/,/^}/p' src/utils/config.ts

Repository: claude-code-best/claude-code

Length of output: 18408


🏁 Script executed:

# Also check GLOBAL_CONFIG_KEYS to see all available keys
rg "GLOBAL_CONFIG_KEYS" -A 30 src/utils/config.ts

Repository: claude-code-best/claude-code

Length of output: 2073


🏁 Script executed:

# Check if there's any mention of RSS or memory monitoring in config
rg "memory|rss|footer" -i src/utils/config.ts

Repository: claude-code-best/claude-code

Length of output: 1015


Gate RSS footer behind config option, similar to prStatusFooterEnabled.

The RSS memory indicator currently displays for all users every 5 seconds with no way to disable it. This differs from the PR status footer, which respects getGlobalConfig().prStatusFooterEnabled. Memory bytes ticking in the footer may confuse non-developers and appears to be intended as a debugging aid rather than a default-on feature.

Apply the same gating pattern: add rssFooterEnabled?: boolean to GlobalConfig and check it in the render condition alongside the existence of rssState.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx` around lines 452 -
463, Add a feature-flag for the RSS footer and gate rendering on it: add
rssFooterEnabled?: boolean to the GlobalConfig type, ensure getGlobalConfig()
exposes that flag (similar to prStatusFooterEnabled), and update
PromptInputFooterLeftSide to only render the rssState block when rssState is
truthy AND getGlobalConfig().rssFooterEnabled is true; reference the rssState
rendering block in PromptInputFooterLeftSide and the GlobalConfig type so the
footer respects the new setting.

]

// Check if any in-process teammates exist (for hint text cycling)
Expand Down
34 changes: 18 additions & 16 deletions src/components/Settings/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import chalk from 'chalk';
import {
permissionModeTitle,
permissionModeShortTitle,
permissionModeFromString,
toExternalPermissionMode,
isExternalPermissionMode,
Expand Down Expand Up @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -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],
},
});

Expand Down Expand Up @@ -1548,6 +1548,8 @@ export function Config({
'scroll:lineUp': () => moveSelection(-1),
'scroll:lineDown': () => moveSelection(1),
'select:accept': toggleSetting,
'select:previousValue': () => toggleSetting(),
'select:nextValue': () => toggleSetting(),
Comment on lines +1551 to +1552
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

select:previousValue cycles forward — same as select:nextValue.

toggleSetting() only ever advances by +1 (line 1510: (currentIndex + 1) % setting.options.length). Both new keybindings invoke it, so left arrow behaves identically to right arrow. Users will expect left to step backward through enum values (e.g., default → plan → auto → default going right; reverse going left).

This preserves the prior handleKeyDown behavior at line 1593, but the names select:previousValue / select:nextValue now actively suggest a directionality that doesn't exist.

🐛 Proposed fix

Add a direction arg to toggleSetting, or split into two helpers:

-  const toggleSetting = useCallback(() => {
+  const toggleSetting = useCallback((direction: 1 | -1 = 1) => {
     const setting = filteredSettingsItems[selectedIndex];
     ...
     if (setting.type === 'enum') {
       isDirty.current = true;
       const currentIndex = setting.options.indexOf(setting.value);
-      const nextIndex = (currentIndex + 1) % setting.options.length;
+      const len = setting.options.length;
+      const nextIndex = (currentIndex + direction + len) % len;
       setting.onChange(setting.options[nextIndex]!);
       return;
     }
   }, [...]);
-      'select:previousValue': () => toggleSetting(),
-      'select:nextValue': () => toggleSetting(),
+      'select:previousValue': () => toggleSetting(-1),
+      'select:nextValue': () => toggleSetting(1),

(autoUpdatesChannel and other branches that don't read direction will keep the existing forward-only semantics.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/Config.tsx` around lines 1551 - 1552, The current
keybinding handlers call toggleSetting() for both 'select:previousValue' and
'select:nextValue', so left and right both advance forward; update toggleSetting
(used in the select handlers and referenced in handleKeyDown) to accept a
direction parameter (e.g., direction: 1 | -1) or add two helpers like
toggleSettingNext and toggleSettingPrev, then wire 'select:nextValue' to advance
and 'select:previousValue' to decrement (use modulo arithmetic to wrap negative
indices) so the previous/next keybindings produce opposite traversal through
setting.options; ensure any branches (e.g., autoUpdatesChannel) that don't need
direction keep current behavior.

'settings:search': () => {
setIsSearchMode(true);
setSearchQuery('');
Expand Down Expand Up @@ -1936,13 +1938,13 @@ export function Config({

return (
<React.Fragment key={setting.id}>
<Box>
<Box width="100%">
<Box width={44}>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? figures.pointer : ' '} {setting.label}
</Text>
</Box>
<Box key={isSelected ? 'selected' : 'unselected'}>
<Box flexGrow={1}>
{setting.type === 'boolean' ? (
<>
<Text color={isSelected ? 'suggestion' : undefined}>{setting.value.toString()}</Text>
Expand All @@ -1963,7 +1965,7 @@ export function Config({
</Text>
) : setting.id === 'defaultPermissionMode' ? (
<Text color={isSelected ? 'suggestion' : undefined}>
{permissionModeTitle(setting.value as PermissionMode)}
{permissionModeShortTitle(setting.value as PermissionMode)}
</Text>
) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? (
<Box flexDirection="column">
Expand Down
3 changes: 3 additions & 0 deletions src/keybindings/defaultBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/keybindings/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export const KEYBINDING_ACTIONS = [
'settings:search',
'settings:retry',
'settings:close',
'select:previousValue',
'select:nextValue',
// Voice actions
'voice:pushToTalk',
] as const
Expand Down
3 changes: 2 additions & 1 deletion src/services/langfuse/__tests__/langfuse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -255,7 +256,7 @@ describe('Langfuse integration', () => {
},
],
},
])
] as any)

expect(result).toEqual([
{
Expand Down
11 changes: 9 additions & 2 deletions src/services/langfuse/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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<string, unknown>): LangfuseContentPart | null {
const type = block.type as string | undefined
Expand Down Expand Up @@ -178,7 +185,7 @@ function toRoleFromWrappedMessage(msg: Record<string, unknown>): '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[] = []
Expand Down
4 changes: 3 additions & 1 deletion src/types/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 8 additions & 12 deletions src/utils/permissions/PermissionMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const PERMISSION_MODE_CONFIG: Partial<
external: 'acceptEdits',
},
bypassPermissions: {
title: 'Bypass Permissions',
title: 'Bypass',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
shortTitle: 'Bypass',
symbol: '⏵⏵',
color: 'error',
Expand All @@ -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,
},
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/utils/permissions/__tests__/PermissionMode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
6 changes: 1 addition & 5 deletions src/utils/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading