Skip to content

Commit fc438bd

Browse files
Feature/add auto mode settings and fix bug (#368)
* refactor: 将 convertMessagesToLangfuse 参数类型从 unknown 收窄为联合类型 将 readonly unknown[] 改为 readonly LangfuseInputMessage[], 其中 LangfuseInputMessage = UserMessage | AssistantMessage | ChatCompletionMessageParam, 让调用方获得编译期类型检查。 * fix: 修复 Config 面板第二次进入时左右键无反应的问题 将左右键枚举值切换从依赖 DOM 焦点的 onKeyDown 改为 useKeybindings 系统, 确保按键在任何焦点状态下都能正确响应。同时修复 isSearchMode 初始值和布局问题。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 修复 PowerShellTool.isSearchOrReadCommand 在 input 为 undefined 时崩溃的问题 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: 添加 RSS 内存指示器并解绑 auto 权限模式与 TRANSCRIPT_CLASSIFIER - 在 REPL 底栏添加 RSS 内存使用显示,512MB 以下 dimColor,512MB-1GB warning 色,1GB 以上 error 色 - auto 权限模式不再依赖 TRANSCRIPT_CLASSIFIER feature flag,classifier 不可用时 fallback 到 prompting - Config 面板 defaultPermissionMode 使用类型安全的 permissionModeFromString,显示改用 shortTitle - bypassPermissions title 缩短为 Bypass 与 shortTitle 一致 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 同步 permissionModeTitle 测试断言与 bypassPermissions 的新 title 值 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4591432 commit fc438bd

11 files changed

Lines changed: 82 additions & 40 deletions

File tree

packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ export const PowerShellTool = buildTool({
421421
isSearch: boolean
422422
isRead: boolean
423423
} {
424-
if (!input.command) {
424+
if (!input?.command) {
425425
return { isSearch: false, isRead: false }
426426
}
427427
return isSearchOrReadPowerShellCommand(input.command)

src/components/PromptInput/PromptInputFooterLeftSide.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { usePrStatus } from '../../hooks/usePrStatus.js'
4242
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
4343
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
4444
import { useTasksV2 } from '../../hooks/useTasksV2.js'
45-
import { formatDuration } from '../../utils/format.js'
45+
import { formatDuration, formatFileSize } from '../../utils/format.js'
4646
import { VoiceWarmupHint } from './VoiceIndicator.js'
4747
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
4848
import { useVoiceState } from '../../context/voice.js'
@@ -63,6 +63,26 @@ const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {}
6363
const NULL = () => null
6464
const MAX_VOICE_HINT_SHOWS = 3
6565

66+
const RSS_UPDATE_INTERVAL_MS = 5_000
67+
68+
type RssState = { text: string; level: 'normal' | 'warning' | 'error' }
69+
70+
function useRssDisplay(): RssState | null {
71+
const [state, setState] = useState<RssState | null>(null)
72+
useEffect(() => {
73+
function update(): void {
74+
const mb = process.memoryUsage().rss / (1024 * 1024)
75+
const level = mb >= 1024 ? 'error' : mb >= 512 ? 'warning' : 'normal'
76+
const text = formatFileSize(mb * 1024 * 1024)
77+
setState(prev => (prev?.text === text ? prev : { text, level }))
78+
}
79+
update()
80+
const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS)
81+
return () => clearInterval(timer)
82+
}, [])
83+
return state
84+
}
85+
6686
type Props = {
6787
exitMessage: {
6888
show: boolean
@@ -315,6 +335,7 @@ function ModeIndicator({
315335
const isKillAgentsConfirmShowing = useAppState(
316336
s => s.notifications.current?.key === 'kill-agents-confirm',
317337
)
338+
const rssState = useRssDisplay()
318339

319340
// Derive team info from teamContext (no filesystem I/O needed)
320341
// Match the same logic as TeamStatus to avoid trailing separator
@@ -428,6 +449,18 @@ function ModeIndicator({
428449
/>,
429450
]
430451
: []),
452+
// RSS memory indicator — always visible
453+
...(rssState
454+
? [
455+
<Text
456+
key="rss"
457+
dimColor={rssState.level === 'normal'}
458+
color={rssState.level === 'error' ? 'error' : rssState.level === 'warning' ? 'warning' : undefined}
459+
>
460+
{rssState.text}
461+
</Text>,
462+
]
463+
: []),
431464
]
432465

433466
// Check if any in-process teammates exist (for hint text cycling)

src/components/Settings/Config.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import chalk from 'chalk';
1717
import {
1818
permissionModeTitle,
19+
permissionModeShortTitle,
1920
permissionModeFromString,
2021
toExternalPermissionMode,
2122
isExternalPermissionMode,
@@ -153,7 +154,7 @@ export function Config({
153154
const initialLanguage = React.useRef(currentLanguage);
154155
const [selectedIndex, setSelectedIndex] = useState(0);
155156
const [scrollOffset, setScrollOffset] = useState(0);
156-
const [isSearchMode, setIsSearchMode] = useState(true);
157+
const [isSearchMode, setIsSearchMode] = useState(false);
157158
const isTerminalFocused = useTerminalFocus();
158159
const { rows } = useTerminalSize();
159160
// contentHeight is set by Settings.tsx (same value passed to Tabs to fix
@@ -167,6 +168,9 @@ export function Config({
167168
const thinkingEnabled = useAppState(s => s.thinkingEnabled);
168169
const isFastMode = useAppState(s => (isFastModeEnabled() ? s.fastMode : false));
169170
const promptSuggestionEnabled = useAppState(s => s.promptSuggestionEnabled);
171+
const currentDefaultPermissionMode = permissionModeFromString(
172+
settingsData?.permissions?.defaultMode ?? 'default',
173+
);
170174
// Show auto in the default-mode dropdown when the user has opted in OR the
171175
// config is fully 'enabled' — even if currently circuit-broken ('disabled'),
172176
// an opted-in user should still see it in settings (it's a temporary state).
@@ -558,27 +562,23 @@ export function Config({
558562
{
559563
id: 'defaultPermissionMode',
560564
label: 'Default permission mode',
561-
value: settingsData?.permissions?.defaultMode || 'default',
565+
value: currentDefaultPermissionMode,
562566
options: (() => {
563567
const priorityOrder: PermissionMode[] = ['default', 'plan'];
564-
const allModes: readonly PermissionMode[] = feature('TRANSCRIPT_CLASSIFIER')
565-
? PERMISSION_MODES
566-
: EXTERNAL_PERMISSION_MODES;
567-
const excluded: PermissionMode[] = ['bypassPermissions'];
568-
if (feature('TRANSCRIPT_CLASSIFIER') && !showAutoInDefaultModePicker) {
569-
excluded.push('auto');
570-
}
571-
return [...priorityOrder, ...allModes.filter(m => !priorityOrder.includes(m) && !excluded.includes(m))];
568+
return [...priorityOrder, ...PERMISSION_MODES.filter(m => !priorityOrder.includes(m))];
572569
})(),
573570
type: 'enum' as const,
574571
onChange(mode: string) {
575572
const parsedMode = permissionModeFromString(mode);
576-
// Internal modes (e.g. auto) are stored directly
577-
const validatedMode = isExternalPermissionMode(parsedMode) ? toExternalPermissionMode(parsedMode) : parsedMode;
573+
// auto is an internal-only mode — store it directly, don't convert
574+
// to its external mapping ('default') which would make it invisible.
575+
const validatedMode = parsedMode === 'auto'
576+
? parsedMode
577+
: (isExternalPermissionMode(parsedMode) ? toExternalPermissionMode(parsedMode) : parsedMode);
578578
const result = updateSettingsForSource('userSettings', {
579579
permissions: {
580580
...settingsData?.permissions,
581-
defaultMode: validatedMode as ExternalPermissionMode,
581+
defaultMode: validatedMode as (typeof PERMISSION_MODES)[number],
582582
},
583583
});
584584

@@ -1548,6 +1548,8 @@ export function Config({
15481548
'scroll:lineUp': () => moveSelection(-1),
15491549
'scroll:lineDown': () => moveSelection(1),
15501550
'select:accept': toggleSetting,
1551+
'select:previousValue': () => toggleSetting(),
1552+
'select:nextValue': () => toggleSetting(),
15511553
'settings:search': () => {
15521554
setIsSearchMode(true);
15531555
setSearchQuery('');
@@ -1936,13 +1938,13 @@ export function Config({
19361938

19371939
return (
19381940
<React.Fragment key={setting.id}>
1939-
<Box>
1941+
<Box width="100%">
19401942
<Box width={44}>
19411943
<Text color={isSelected ? 'suggestion' : undefined}>
19421944
{isSelected ? figures.pointer : ' '} {setting.label}
19431945
</Text>
19441946
</Box>
1945-
<Box key={isSelected ? 'selected' : 'unselected'}>
1947+
<Box flexGrow={1}>
19461948
{setting.type === 'boolean' ? (
19471949
<>
19481950
<Text color={isSelected ? 'suggestion' : undefined}>{setting.value.toString()}</Text>
@@ -1963,7 +1965,7 @@ export function Config({
19631965
</Text>
19641966
) : setting.id === 'defaultPermissionMode' ? (
19651967
<Text color={isSelected ? 'suggestion' : undefined}>
1966-
{permissionModeTitle(setting.value as PermissionMode)}
1968+
{permissionModeShortTitle(setting.value as PermissionMode)}
19671969
</Text>
19681970
) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? (
19691971
<Box flexDirection="column">

src/keybindings/defaultBindings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
117117
j: 'select:next',
118118
'ctrl+p': 'select:previous',
119119
'ctrl+n': 'select:next',
120+
// Cycle enum values left/right (same as left/right arrow in handleKeyDown)
121+
left: 'select:previousValue',
122+
right: 'select:nextValue',
120123
// Toggle/activate the selected setting (space only — enter saves & closes)
121124
space: 'select:accept',
122125
// Save and close the config panel

src/keybindings/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export const KEYBINDING_ACTIONS = [
168168
'settings:search',
169169
'settings:retry',
170170
'settings:close',
171+
'select:previousValue',
172+
'select:nextValue',
171173
// Voice actions
172174
'voice:pushToTalk',
173175
] as const

src/services/langfuse/__tests__/langfuse.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ describe('Langfuse integration', () => {
231231

232232
test('merges assistant tool calls from OpenAI-style array content', async () => {
233233
const { convertMessagesToLangfuse } = await import('../convert.js')
234+
// Content part with embedded tool_calls is non-standard; cast for defensive test
234235
const result = convertMessagesToLangfuse([
235236
{
236237
role: 'assistant',
@@ -255,7 +256,7 @@ describe('Langfuse integration', () => {
255256
},
256257
],
257258
},
258-
])
259+
] as any)
259260

260261
expect(result).toEqual([
261262
{

src/services/langfuse/convert.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
* - tool_result blocks → separate { role: 'tool' } messages
1111
*/
1212

13-
import type { AssistantMessage } from 'src/types/message.js'
13+
import type { AssistantMessage, UserMessage } from 'src/types/message.js'
14+
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions.mjs'
1415

1516
type LangfuseContentPart =
1617
| { type: 'text'; text: string }
@@ -79,6 +80,12 @@ function mergeToolCalls(
7980
return [...merged.values()]
8081
}
8182

83+
/** Union of all message formats accepted by Langfuse converters. */
84+
type LangfuseInputMessage =
85+
| UserMessage
86+
| AssistantMessage
87+
| ChatCompletionMessageParam
88+
8289
/** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */
8390
function toContentPart(block: Record<string, unknown>): LangfuseContentPart | null {
8491
const type = block.type as string | undefined
@@ -178,7 +185,7 @@ function toRoleFromWrappedMessage(msg: Record<string, unknown>): 'user' | 'assis
178185

179186
/** Convert internal or OpenAI-style messages → Langfuse input format */
180187
export function convertMessagesToLangfuse(
181-
messages: readonly unknown[],
188+
messages: readonly LangfuseInputMessage[],
182189
systemPrompt?: readonly string[],
183190
): LangfuseChatMessage[] {
184191
const result: LangfuseChatMessage[] = []

src/types/permissions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export type PermissionMode = InternalPermissionMode
3030

3131
// Runtime validation set: modes that are user-addressable (settings.json
3232
// defaultMode, --permission-mode CLI flag, conversation recovery).
33+
// 'auto' is always available — when TRANSCRIPT_CLASSIFIER is off, the
34+
// classifier is unavailable and auto mode falls back to prompting.
3335
export const INTERNAL_PERMISSION_MODES = [
3436
...EXTERNAL_PERMISSION_MODES,
35-
...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
37+
'auto' as const,
3638
] as const satisfies readonly PermissionMode[]
3739

3840
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES

src/utils/permissions/PermissionMode.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const PERMISSION_MODE_CONFIG: Partial<
6464
external: 'acceptEdits',
6565
},
6666
bypassPermissions: {
67-
title: 'Bypass Permissions',
67+
title: 'Bypass',
6868
shortTitle: 'Bypass',
6969
symbol: '⏵⏵',
7070
color: 'error',
@@ -77,17 +77,13 @@ const PERMISSION_MODE_CONFIG: Partial<
7777
color: 'error',
7878
external: 'dontAsk',
7979
},
80-
...(feature('TRANSCRIPT_CLASSIFIER')
81-
? {
82-
auto: {
83-
title: 'Auto mode',
84-
shortTitle: 'Auto',
85-
symbol: '⏵⏵',
86-
color: 'warning' as ModeColorKey,
87-
external: 'default' as ExternalPermissionMode,
88-
},
89-
}
90-
: {}),
80+
auto: {
81+
title: 'Auto',
82+
shortTitle: 'Auto',
83+
symbol: '⏵⏵',
84+
color: 'warning' as ModeColorKey,
85+
external: 'default' as ExternalPermissionMode,
86+
},
9187
}
9288

9389
/**

src/utils/permissions/__tests__/PermissionMode.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe("permissionModeTitle", () => {
7070
expect(permissionModeTitle("default")).toBe("Default");
7171
expect(permissionModeTitle("plan")).toBe("Plan Mode");
7272
expect(permissionModeTitle("acceptEdits")).toBe("Accept edits");
73-
expect(permissionModeTitle("bypassPermissions")).toBe("Bypass Permissions");
73+
expect(permissionModeTitle("bypassPermissions")).toBe("Bypass");
7474
expect(permissionModeTitle("dontAsk")).toBe("Don't Ask");
7575
});
7676

0 commit comments

Comments
 (0)