mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
Compare commits
5 Commits
v2.0.2
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a90b218c3 | ||
|
|
de9494c0a3 | ||
|
|
e7e1f7a34d | ||
|
|
9a5998eaef | ||
|
|
dff678924e |
@@ -421,7 +421,7 @@ export const PowerShellTool = buildTool({
|
|||||||
isSearch: boolean
|
isSearch: boolean
|
||||||
isRead: boolean
|
isRead: boolean
|
||||||
} {
|
} {
|
||||||
if (!input.command) {
|
if (!input?.command) {
|
||||||
return { isSearch: false, isRead: false }
|
return { isSearch: false, isRead: false }
|
||||||
}
|
}
|
||||||
return isSearchOrReadPowerShellCommand(input.command)
|
return isSearchOrReadPowerShellCommand(input.command)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { usePrStatus } from '../../hooks/usePrStatus.js'
|
|||||||
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
|
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { useTasksV2 } from '../../hooks/useTasksV2.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 { VoiceWarmupHint } from './VoiceIndicator.js'
|
||||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
|
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
|
||||||
import { useVoiceState } from '../../context/voice.js'
|
import { useVoiceState } from '../../context/voice.js'
|
||||||
@@ -63,6 +63,26 @@ const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {}
|
|||||||
const NULL = () => null
|
const NULL = () => null
|
||||||
const MAX_VOICE_HINT_SHOWS = 3
|
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 = {
|
type Props = {
|
||||||
exitMessage: {
|
exitMessage: {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -315,6 +335,7 @@ function ModeIndicator({
|
|||||||
const isKillAgentsConfirmShowing = useAppState(
|
const isKillAgentsConfirmShowing = useAppState(
|
||||||
s => s.notifications.current?.key === 'kill-agents-confirm',
|
s => s.notifications.current?.key === 'kill-agents-confirm',
|
||||||
)
|
)
|
||||||
|
const rssState = useRssDisplay()
|
||||||
|
|
||||||
// Derive team info from teamContext (no filesystem I/O needed)
|
// Derive team info from teamContext (no filesystem I/O needed)
|
||||||
// Match the same logic as TeamStatus to avoid trailing separator
|
// Match the same logic as TeamStatus to avoid trailing separator
|
||||||
@@ -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>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Check if any in-process teammates exist (for hint text cycling)
|
// Check if any in-process teammates exist (for hint text cycling)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {
|
import {
|
||||||
permissionModeTitle,
|
permissionModeTitle,
|
||||||
|
permissionModeShortTitle,
|
||||||
permissionModeFromString,
|
permissionModeFromString,
|
||||||
toExternalPermissionMode,
|
toExternalPermissionMode,
|
||||||
isExternalPermissionMode,
|
isExternalPermissionMode,
|
||||||
@@ -153,7 +154,7 @@ export function Config({
|
|||||||
const initialLanguage = React.useRef(currentLanguage);
|
const initialLanguage = React.useRef(currentLanguage);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const [scrollOffset, setScrollOffset] = useState(0);
|
const [scrollOffset, setScrollOffset] = useState(0);
|
||||||
const [isSearchMode, setIsSearchMode] = useState(true);
|
const [isSearchMode, setIsSearchMode] = useState(false);
|
||||||
const isTerminalFocused = useTerminalFocus();
|
const isTerminalFocused = useTerminalFocus();
|
||||||
const { rows } = useTerminalSize();
|
const { rows } = useTerminalSize();
|
||||||
// contentHeight is set by Settings.tsx (same value passed to Tabs to fix
|
// 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 thinkingEnabled = useAppState(s => s.thinkingEnabled);
|
||||||
const isFastMode = useAppState(s => (isFastModeEnabled() ? s.fastMode : false));
|
const isFastMode = useAppState(s => (isFastModeEnabled() ? s.fastMode : false));
|
||||||
const promptSuggestionEnabled = useAppState(s => s.promptSuggestionEnabled);
|
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
|
// 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'),
|
// 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).
|
// an opted-in user should still see it in settings (it's a temporary state).
|
||||||
@@ -558,27 +562,23 @@ export function Config({
|
|||||||
{
|
{
|
||||||
id: 'defaultPermissionMode',
|
id: 'defaultPermissionMode',
|
||||||
label: 'Default permission mode',
|
label: 'Default permission mode',
|
||||||
value: settingsData?.permissions?.defaultMode || 'default',
|
value: currentDefaultPermissionMode,
|
||||||
options: (() => {
|
options: (() => {
|
||||||
const priorityOrder: PermissionMode[] = ['default', 'plan'];
|
const priorityOrder: PermissionMode[] = ['default', 'plan'];
|
||||||
const allModes: readonly PermissionMode[] = feature('TRANSCRIPT_CLASSIFIER')
|
return [...priorityOrder, ...PERMISSION_MODES.filter(m => !priorityOrder.includes(m))];
|
||||||
? PERMISSION_MODES
|
|
||||||
: EXTERNAL_PERMISSION_MODES;
|
|
||||||
const excluded: PermissionMode[] = ['bypassPermissions'];
|
|
||||||
if (feature('TRANSCRIPT_CLASSIFIER') && !showAutoInDefaultModePicker) {
|
|
||||||
excluded.push('auto');
|
|
||||||
}
|
|
||||||
return [...priorityOrder, ...allModes.filter(m => !priorityOrder.includes(m) && !excluded.includes(m))];
|
|
||||||
})(),
|
})(),
|
||||||
type: 'enum' as const,
|
type: 'enum' as const,
|
||||||
onChange(mode: string) {
|
onChange(mode: string) {
|
||||||
const parsedMode = permissionModeFromString(mode);
|
const parsedMode = permissionModeFromString(mode);
|
||||||
// Internal modes (e.g. auto) are stored directly
|
// auto is an internal-only mode — store it directly, don't convert
|
||||||
const validatedMode = isExternalPermissionMode(parsedMode) ? toExternalPermissionMode(parsedMode) : parsedMode;
|
// to its external mapping ('default') which would make it invisible.
|
||||||
|
const validatedMode = parsedMode === 'auto'
|
||||||
|
? parsedMode
|
||||||
|
: (isExternalPermissionMode(parsedMode) ? toExternalPermissionMode(parsedMode) : parsedMode);
|
||||||
const result = updateSettingsForSource('userSettings', {
|
const result = updateSettingsForSource('userSettings', {
|
||||||
permissions: {
|
permissions: {
|
||||||
...settingsData?.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:lineUp': () => moveSelection(-1),
|
||||||
'scroll:lineDown': () => moveSelection(1),
|
'scroll:lineDown': () => moveSelection(1),
|
||||||
'select:accept': toggleSetting,
|
'select:accept': toggleSetting,
|
||||||
|
'select:previousValue': () => toggleSetting(),
|
||||||
|
'select:nextValue': () => toggleSetting(),
|
||||||
'settings:search': () => {
|
'settings:search': () => {
|
||||||
setIsSearchMode(true);
|
setIsSearchMode(true);
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@@ -1936,13 +1938,13 @@ export function Config({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={setting.id}>
|
<React.Fragment key={setting.id}>
|
||||||
<Box>
|
<Box width="100%">
|
||||||
<Box width={44}>
|
<Box width={44}>
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||||
{isSelected ? figures.pointer : ' '} {setting.label}
|
{isSelected ? figures.pointer : ' '} {setting.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box key={isSelected ? 'selected' : 'unselected'}>
|
<Box flexGrow={1}>
|
||||||
{setting.type === 'boolean' ? (
|
{setting.type === 'boolean' ? (
|
||||||
<>
|
<>
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>{setting.value.toString()}</Text>
|
<Text color={isSelected ? 'suggestion' : undefined}>{setting.value.toString()}</Text>
|
||||||
@@ -1963,7 +1965,7 @@ export function Config({
|
|||||||
</Text>
|
</Text>
|
||||||
) : setting.id === 'defaultPermissionMode' ? (
|
) : setting.id === 'defaultPermissionMode' ? (
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||||
{permissionModeTitle(setting.value as PermissionMode)}
|
{permissionModeShortTitle(setting.value as PermissionMode)}
|
||||||
</Text>
|
</Text>
|
||||||
) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? (
|
) : setting.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
|
|||||||
j: 'select:next',
|
j: 'select:next',
|
||||||
'ctrl+p': 'select:previous',
|
'ctrl+p': 'select:previous',
|
||||||
'ctrl+n': 'select:next',
|
'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)
|
// Toggle/activate the selected setting (space only — enter saves & closes)
|
||||||
space: 'select:accept',
|
space: 'select:accept',
|
||||||
// Save and close the config panel
|
// Save and close the config panel
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ export const KEYBINDING_ACTIONS = [
|
|||||||
'settings:search',
|
'settings:search',
|
||||||
'settings:retry',
|
'settings:retry',
|
||||||
'settings:close',
|
'settings:close',
|
||||||
|
'select:previousValue',
|
||||||
|
'select:nextValue',
|
||||||
// Voice actions
|
// Voice actions
|
||||||
'voice:pushToTalk',
|
'voice:pushToTalk',
|
||||||
] as const
|
] as const
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ describe('Langfuse integration', () => {
|
|||||||
|
|
||||||
test('merges assistant tool calls from OpenAI-style array content', async () => {
|
test('merges assistant tool calls from OpenAI-style array content', async () => {
|
||||||
const { convertMessagesToLangfuse } = await import('../convert.js')
|
const { convertMessagesToLangfuse } = await import('../convert.js')
|
||||||
|
// Content part with embedded tool_calls is non-standard; cast for defensive test
|
||||||
const result = convertMessagesToLangfuse([
|
const result = convertMessagesToLangfuse([
|
||||||
{
|
{
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -255,7 +256,7 @@ describe('Langfuse integration', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
] as any)
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
* - tool_result blocks → separate { role: 'tool' } messages
|
* - 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 LangfuseContentPart =
|
||||||
| { type: 'text'; text: string }
|
| { type: 'text'; text: string }
|
||||||
@@ -79,6 +80,12 @@ function mergeToolCalls(
|
|||||||
return [...merged.values()]
|
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) */
|
/** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */
|
||||||
function toContentPart(block: Record<string, unknown>): LangfuseContentPart | null {
|
function toContentPart(block: Record<string, unknown>): LangfuseContentPart | null {
|
||||||
const type = block.type as string | undefined
|
const type = block.type as string | undefined
|
||||||
@@ -178,7 +185,7 @@ function toRoleFromWrappedMessage(msg: Record<string, unknown>): 'user' | 'assis
|
|||||||
|
|
||||||
/** Convert internal or OpenAI-style messages → Langfuse input format */
|
/** Convert internal or OpenAI-style messages → Langfuse input format */
|
||||||
export function convertMessagesToLangfuse(
|
export function convertMessagesToLangfuse(
|
||||||
messages: readonly unknown[],
|
messages: readonly LangfuseInputMessage[],
|
||||||
systemPrompt?: readonly string[],
|
systemPrompt?: readonly string[],
|
||||||
): LangfuseChatMessage[] {
|
): LangfuseChatMessage[] {
|
||||||
const result: LangfuseChatMessage[] = []
|
const result: LangfuseChatMessage[] = []
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ export type PermissionMode = InternalPermissionMode
|
|||||||
|
|
||||||
// Runtime validation set: modes that are user-addressable (settings.json
|
// Runtime validation set: modes that are user-addressable (settings.json
|
||||||
// defaultMode, --permission-mode CLI flag, conversation recovery).
|
// 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 = [
|
export const INTERNAL_PERMISSION_MODES = [
|
||||||
...EXTERNAL_PERMISSION_MODES,
|
...EXTERNAL_PERMISSION_MODES,
|
||||||
...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
|
'auto' as const,
|
||||||
] as const satisfies readonly PermissionMode[]
|
] as const satisfies readonly PermissionMode[]
|
||||||
|
|
||||||
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
|
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const PERMISSION_MODE_CONFIG: Partial<
|
|||||||
external: 'acceptEdits',
|
external: 'acceptEdits',
|
||||||
},
|
},
|
||||||
bypassPermissions: {
|
bypassPermissions: {
|
||||||
title: 'Bypass Permissions',
|
title: 'Bypass',
|
||||||
shortTitle: 'Bypass',
|
shortTitle: 'Bypass',
|
||||||
symbol: '⏵⏵',
|
symbol: '⏵⏵',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
@@ -77,17 +77,13 @@ const PERMISSION_MODE_CONFIG: Partial<
|
|||||||
color: 'error',
|
color: 'error',
|
||||||
external: 'dontAsk',
|
external: 'dontAsk',
|
||||||
},
|
},
|
||||||
...(feature('TRANSCRIPT_CLASSIFIER')
|
auto: {
|
||||||
? {
|
title: 'Auto',
|
||||||
auto: {
|
shortTitle: 'Auto',
|
||||||
title: 'Auto mode',
|
symbol: '⏵⏵',
|
||||||
shortTitle: 'Auto',
|
color: 'warning' as ModeColorKey,
|
||||||
symbol: '⏵⏵',
|
external: 'default' as ExternalPermissionMode,
|
||||||
color: 'warning' as ModeColorKey,
|
},
|
||||||
external: 'default' as ExternalPermissionMode,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe("permissionModeTitle", () => {
|
|||||||
expect(permissionModeTitle("default")).toBe("Default");
|
expect(permissionModeTitle("default")).toBe("Default");
|
||||||
expect(permissionModeTitle("plan")).toBe("Plan Mode");
|
expect(permissionModeTitle("plan")).toBe("Plan Mode");
|
||||||
expect(permissionModeTitle("acceptEdits")).toBe("Accept edits");
|
expect(permissionModeTitle("acceptEdits")).toBe("Accept edits");
|
||||||
expect(permissionModeTitle("bypassPermissions")).toBe("Bypass Permissions");
|
expect(permissionModeTitle("bypassPermissions")).toBe("Bypass");
|
||||||
expect(permissionModeTitle("dontAsk")).toBe("Don't Ask");
|
expect(permissionModeTitle("dontAsk")).toBe("Don't Ask");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,7 @@ export const PermissionsSchema = lazySchema(() =>
|
|||||||
'List of permission rules that should always prompt for confirmation',
|
'List of permission rules that should always prompt for confirmation',
|
||||||
),
|
),
|
||||||
defaultMode: z
|
defaultMode: z
|
||||||
.enum(
|
.enum(PERMISSION_MODES)
|
||||||
feature('TRANSCRIPT_CLASSIFIER')
|
|
||||||
? PERMISSION_MODES
|
|
||||||
: EXTERNAL_PERMISSION_MODES,
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Default permission mode when Claude Code needs access'),
|
.describe('Default permission mode when Claude Code needs access'),
|
||||||
disableBypassPermissionsMode: z
|
disableBypassPermissionsMode: z
|
||||||
|
|||||||
Reference in New Issue
Block a user