mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
12 Commits
v2.6.13
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc132688d | ||
|
|
551a62a2c2 | ||
|
|
2a1f5697cc | ||
|
|
6942f5393f | ||
|
|
7b209ed0cc | ||
|
|
f9a3654167 | ||
|
|
d866b076ba | ||
|
|
3c960323d7 | ||
|
|
2044858b9f | ||
|
|
da10c45e76 | ||
|
|
21c17b4c6e | ||
|
|
6e52043555 |
808545
dist-nosplit/cli.js
808545
dist-nosplit/cli.js
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
dist-nosplit/vendor/ripgrep/arm64-linux/rg
vendored
BIN
dist-nosplit/vendor/ripgrep/arm64-linux/rg
vendored
Binary file not shown.
@@ -73,7 +73,7 @@ function isAddressed(messages: Message[], name: string): boolean {
|
|||||||
) {
|
) {
|
||||||
const m = messages[i]
|
const m = messages[i]
|
||||||
if (m?.type !== 'user') continue
|
if (m?.type !== 'user') continue
|
||||||
const content = (m as any).message?.content
|
const content = m.message?.content
|
||||||
if (typeof content === 'string' && pattern.test(content)) return true
|
if (typeof content === 'string' && pattern.test(content)) return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -89,7 +89,7 @@ function buildTranscript(messages: Message[]): string {
|
|||||||
.filter(m => m.type === 'user' || m.type === 'assistant')
|
.filter(m => m.type === 'user' || m.type === 'assistant')
|
||||||
.map(m => {
|
.map(m => {
|
||||||
const role = m.type === 'user' ? 'user' : 'claude'
|
const role = m.type === 'user' ? 'user' : 'claude'
|
||||||
const content = (m as any).message?.content
|
const content = m.message?.content
|
||||||
const text =
|
const text =
|
||||||
typeof content === 'string'
|
typeof content === 'string'
|
||||||
? content.slice(0, 300)
|
? content.slice(0, 300)
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export class CCRClient {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError(
|
||||||
'client event POST failed',
|
'client event POST failed',
|
||||||
(result as any).retryAfterMs,
|
result.retryAfterMs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -404,7 +404,7 @@ export class CCRClient {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError(
|
||||||
'internal event POST failed',
|
'internal event POST failed',
|
||||||
(result as any).retryAfterMs,
|
result.retryAfterMs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,10 +433,7 @@ export class CCRClient {
|
|||||||
'delivery batch',
|
'delivery batch',
|
||||||
)
|
)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError('delivery POST failed', result.retryAfterMs)
|
||||||
'delivery POST failed',
|
|
||||||
(result as any).retryAfterMs,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseDelayMs: 500,
|
baseDelayMs: 500,
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ export function ConsoleOAuthFlow({
|
|||||||
throw new Error((orgResult as { valid: false; message: string }).message);
|
throw new Error((orgResult as { valid: false; message: string }).message);
|
||||||
}
|
}
|
||||||
// Reset modelType to anthropic when using OAuth login
|
// Reset modelType to anthropic when using OAuth login
|
||||||
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any);
|
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as unknown as Parameters<
|
||||||
|
typeof updateSettingsForSource
|
||||||
|
>[1]);
|
||||||
|
|
||||||
setOAuthStatus({ state: 'success' });
|
setOAuthStatus({ state: 'success' });
|
||||||
void sendNotification(
|
void sendNotification(
|
||||||
@@ -662,9 +664,9 @@ function OAuthStatusMessage({
|
|||||||
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||||
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||||
const { error } = updateSettingsForSource('userSettings', {
|
const { error } = updateSettingsForSource('userSettings', {
|
||||||
modelType: 'anthropic' as any,
|
modelType: 'anthropic',
|
||||||
env,
|
env,
|
||||||
} as any);
|
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||||
if (error) {
|
if (error) {
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
@@ -1153,9 +1155,9 @@ function OAuthStatusMessage({
|
|||||||
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||||
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||||
const { error } = updateSettingsForSource('userSettings', {
|
const { error } = updateSettingsForSource('userSettings', {
|
||||||
modelType: 'gemini' as any,
|
modelType: 'gemini',
|
||||||
env,
|
env,
|
||||||
} as any);
|
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||||
if (error) {
|
if (error) {
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export type FrustrationDetectionResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectFrustration(messages: Message[]): boolean {
|
function detectFrustration(messages: Message[]): boolean {
|
||||||
const apiErrors = messages.filter(m => (m as any).isApiErrorMessage)
|
const apiErrors = messages.filter(
|
||||||
|
m => 'isApiErrorMessage' in m && m.isApiErrorMessage === true,
|
||||||
|
)
|
||||||
return apiErrors.length >= 2
|
return apiErrors.length >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +27,9 @@ export function useFrustrationDetection(
|
|||||||
const [state, setState] = useState<FrustrationState>('closed')
|
const [state, setState] = useState<FrustrationState>('closed')
|
||||||
|
|
||||||
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
|
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
|
||||||
const policyAllowed = isPolicyAllowed('product_feedback' as any)
|
const policyAllowed = isPolicyAllowed(
|
||||||
|
'product_feedback' as Parameters<typeof isPolicyAllowed>[0],
|
||||||
|
)
|
||||||
const shouldSkip =
|
const shouldSkip =
|
||||||
config.transcriptShareDismissed ||
|
config.transcriptShareDismissed ||
|
||||||
!policyAllowed ||
|
!policyAllowed ||
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ function PipeStatusInline(): React.ReactNode {
|
|||||||
if (!feature('UDS_INBOX')) return null;
|
if (!feature('UDS_INBOX')) return null;
|
||||||
// All hooks must be called before any conditional return to maintain
|
// All hooks must be called before any conditional return to maintain
|
||||||
// consistent hook count across renders (React rules of hooks).
|
// consistent hook count across renders (React rules of hooks).
|
||||||
const pipeIpc = useAppState(s => (s as any).pipeIpc);
|
const pipeIpc = useAppState(s => s.pipeIpc);
|
||||||
const setAppState = useSetAppState();
|
const setAppState = useSetAppState();
|
||||||
const [cursorIndex, setCursorIndex] = useState(0);
|
const [cursorIndex, setCursorIndex] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const NULL = () => null;
|
|||||||
const MAX_VOICE_HINT_SHOWS = 3;
|
const MAX_VOICE_HINT_SHOWS = 3;
|
||||||
|
|
||||||
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
||||||
|
const GOAL_TICK_INTERVAL_MS = 1_000;
|
||||||
|
|
||||||
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
||||||
|
|
||||||
@@ -127,6 +128,55 @@ function ProactiveCountdown(): React.ReactNode {
|
|||||||
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compact "goal (1h22min)" pill for the footer — colored by status. */
|
||||||
|
function GoalElapsedIndicator(): React.ReactNode {
|
||||||
|
const [tick, setTick] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setTick(t => t + 1), GOAL_TICK_INTERVAL_MS);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
void tick;
|
||||||
|
|
||||||
|
const goalModule = require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState');
|
||||||
|
const goal = goalModule.getGoal();
|
||||||
|
if (!goal) return null;
|
||||||
|
|
||||||
|
const elapsedMs = goalModule.getActiveElapsedMs(goal);
|
||||||
|
const totalSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
let timeStr: string;
|
||||||
|
if (hours >= 1) {
|
||||||
|
timeStr = `${hours}h${minutes}min`;
|
||||||
|
} else if (minutes >= 1) {
|
||||||
|
timeStr = `${minutes}min`;
|
||||||
|
} else {
|
||||||
|
timeStr = `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color: string | undefined;
|
||||||
|
switch (goal.status) {
|
||||||
|
case 'active':
|
||||||
|
color = 'ansi:green';
|
||||||
|
break;
|
||||||
|
case 'paused':
|
||||||
|
case 'budget_limited':
|
||||||
|
case 'usage_limited':
|
||||||
|
color = 'ansi:yellow';
|
||||||
|
break;
|
||||||
|
case 'blocked':
|
||||||
|
color = 'ansi:red';
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
color = 'ansi:cyan';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text color={color as 'ansi:green'}>goal ({timeStr})</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
export function PromptInputFooterLeftSide({
|
export function PromptInputFooterLeftSide({
|
||||||
exitMessage,
|
exitMessage,
|
||||||
vimMode,
|
vimMode,
|
||||||
@@ -376,6 +426,11 @@ function ModeIndicator({
|
|||||||
</Text>,
|
</Text>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
// Goal elapsed indicator — compact "goal (XhYmin)" after PID
|
||||||
|
...(feature('GOAL') &&
|
||||||
|
(require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState')).getGoal()
|
||||||
|
? [<GoalElapsedIndicator key="goal-elapsed" />]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if any in-process teammates exist (for hint text cycling)
|
// Check if any in-process teammates exist (for hint text cycling)
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ export function UltraplanChoiceDialog({
|
|||||||
if (!isScrollable) return;
|
if (!isScrollable) return;
|
||||||
const halfPage = Math.max(1, Math.floor(visibleHeight / 2));
|
const halfPage = Math.max(1, Math.floor(visibleHeight / 2));
|
||||||
|
|
||||||
if ((key.ctrl && input === 'd') || (key as any).wheelDown) {
|
if ((key.ctrl && input === 'd') || key.wheelDown) {
|
||||||
const step = (key as any).wheelDown ? 3 : halfPage;
|
const step = key.wheelDown ? 3 : halfPage;
|
||||||
setScrollOffset(prev => Math.min(prev + step, maxOffset));
|
setScrollOffset(prev => Math.min(prev + step, maxOffset));
|
||||||
} else if ((key.ctrl && input === 'u') || (key as any).wheelUp) {
|
} else if ((key.ctrl && input === 'u') || key.wheelUp) {
|
||||||
const step = (key as any).wheelUp ? 3 : halfPage;
|
const step = key.wheelUp ? 3 : halfPage;
|
||||||
setScrollOffset(prev => Math.max(prev - step, 0));
|
setScrollOffset(prev => Math.max(prev - step, 0));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export async function startMCPServer(
|
|||||||
)
|
)
|
||||||
if (validationResult && !validationResult.result) {
|
if (validationResult && !validationResult.result) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Tool ${name} input is invalid: ${(validationResult as any).message}`,
|
`Tool ${name} input is invalid: ${'message' in validationResult ? validationResult.message : String(validationResult)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const finalResult = await tool.call(
|
const finalResult = await tool.call(
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function useBackgroundTaskNavigation(options?: {
|
|||||||
const viewSelectionMode = useAppState(s => s.viewSelectionMode)
|
const viewSelectionMode = useAppState(s => s.viewSelectionMode)
|
||||||
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
||||||
const selectedIPAgentIndex = useAppState(s => s.selectedIPAgentIndex)
|
const selectedIPAgentIndex = useAppState(s => s.selectedIPAgentIndex)
|
||||||
const pipeIpc = useAppState(s => (s as any).pipeIpc)
|
const pipeIpc = useAppState(s => s.pipeIpc)
|
||||||
const setAppState = useSetAppState()
|
const setAppState = useSetAppState()
|
||||||
|
|
||||||
// Filter to running teammates and sort alphabetically to match TeammateSpinnerTree display
|
// Filter to running teammates and sort alphabetically to match TeammateSpinnerTree display
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function usePipeRouter({ store, setAppState, addNotification }: Deps): {
|
|||||||
if (!input.trim() || input.trim().startsWith('/')) return false
|
if (!input.trim() || input.trim().startsWith('/')) return false
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const pipeState = (store.getState() as any).pipeIpc
|
const pipeState = store.getState().pipeIpc
|
||||||
const selectedPipes: string[] = pipeState?.selectedPipes ?? []
|
const selectedPipes: string[] = pipeState?.selectedPipes ?? []
|
||||||
const routeMode: 'selected' | 'local' = pipeState?.routeMode ?? 'selected'
|
const routeMode: 'selected' | 'local' = pipeState?.routeMode ?? 'selected'
|
||||||
|
|
||||||
|
|||||||
@@ -4966,7 +4966,7 @@ export function REPL({
|
|||||||
useMailboxBridge({ isLoading, onSubmitMessage: handleIncomingPrompt });
|
useMailboxBridge({ isLoading, onSubmitMessage: handleIncomingPrompt });
|
||||||
useMasterMonitor();
|
useMasterMonitor();
|
||||||
useSlaveNotifications();
|
useSlaveNotifications();
|
||||||
const _pipeIpcState = useAppState(s => getPipeIpc(s as any));
|
const _pipeIpcState = useAppState(s => getPipeIpc(s));
|
||||||
|
|
||||||
usePipePermissionForward({ store, tools, setMessages, setToolUseConfirmQueue, getToolUseContext, mainLoopModel });
|
usePipePermissionForward({ store, tools, setMessages, setToolUseConfirmQueue, getToolUseContext, mainLoopModel });
|
||||||
usePipeMuteSync({ setToolUseConfirmQueue });
|
usePipeMuteSync({ setToolUseConfirmQueue });
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
@@ -112,21 +115,21 @@ export async function* queryModelGemini(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
|
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let ttftMs = 0
|
let ttftMs = 0
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|
||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start':
|
case 'message_start':
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
break
|
break
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -139,17 +142,19 @@ export async function* queryModelGemini(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
|
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
if (block.type === 'thinking') {
|
if (block.type === 'thinking') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
@@ -160,15 +165,19 @@ export async function* queryModelGemini(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_stop': {
|
case 'content_block_stop': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block || !partialMessage) break
|
if (!block || !partialMessage) break
|
||||||
|
|
||||||
const message: AssistantMessage = {
|
const message: AssistantMessage = {
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
content: normalizeContentFromAPI(
|
||||||
},
|
[block] as unknown as BetaMessage['content'],
|
||||||
|
tools,
|
||||||
|
options.agentId,
|
||||||
|
),
|
||||||
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
BetaUsage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -119,10 +123,15 @@ export async function* queryModelGrok(
|
|||||||
grokModel,
|
grokModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let usage = {
|
let usage: {
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
cache_creation_input_tokens: number
|
||||||
|
cache_read_input_tokens: number
|
||||||
|
} = {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
output_tokens: 0,
|
output_tokens: 0,
|
||||||
cache_creation_input_tokens: 0,
|
cache_creation_input_tokens: 0,
|
||||||
@@ -134,16 +143,21 @@ export async function* queryModelGrok(
|
|||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start': {
|
case 'message_start': {
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
if ((event as any).message?.usage) {
|
if (event.message.usage) {
|
||||||
usage = updateOpenAIUsage(usage, (event as any).message.usage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
event.message.usage as unknown as Parameters<
|
||||||
|
typeof updateOpenAIUsage
|
||||||
|
>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -156,31 +170,37 @@ export async function* queryModelGrok(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_stop': {
|
case 'content_block_stop': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block || !partialMessage) break
|
if (!block || !partialMessage) break
|
||||||
|
|
||||||
const m: AssistantMessage = {
|
const m: AssistantMessage = {
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
content: normalizeContentFromAPI(
|
||||||
},
|
[block] as unknown as BetaMessage['content'],
|
||||||
|
tools,
|
||||||
|
options.agentId,
|
||||||
|
),
|
||||||
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
@@ -191,9 +211,12 @@ export async function* queryModelGrok(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = event.usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -205,8 +228,15 @@ export async function* queryModelGrok(
|
|||||||
event.type === 'message_stop' &&
|
event.type === 'message_stop' &&
|
||||||
usage.input_tokens + usage.output_tokens > 0
|
usage.input_tokens + usage.output_tokens > 0
|
||||||
) {
|
) {
|
||||||
const costUSD = calculateUSDCost(grokModel, usage as any)
|
const costUSD = calculateUSDCost(
|
||||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
grokModel,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
)
|
||||||
|
addToTotalSessionCost(
|
||||||
|
costUSD,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
options.model,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
BetaUsage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -137,8 +141,8 @@ function isOpenAIConvertibleMessage(
|
|||||||
* `message_stop` handler and the post-loop safety fallback.
|
* `message_stop` handler and the post-loop safety fallback.
|
||||||
*/
|
*/
|
||||||
function assembleFinalAssistantOutputs(params: {
|
function assembleFinalAssistantOutputs(params: {
|
||||||
partialMessage: any
|
partialMessage: BetaMessage | null
|
||||||
contentBlocks: Record<number, any>
|
contentBlocks: Record<number, Record<string, unknown>>
|
||||||
tools: Tools
|
tools: Tools
|
||||||
agentId: string | undefined
|
agentId: string | undefined
|
||||||
usage: {
|
usage: {
|
||||||
@@ -166,19 +170,19 @@ function assembleFinalAssistantOutputs(params: {
|
|||||||
.map(k => contentBlocks[Number(k)])
|
.map(k => contentBlocks[Number(k)])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
if (allBlocks.length > 0) {
|
if (allBlocks.length > 0 && partialMessage) {
|
||||||
outputs.push({
|
outputs.push({
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI(
|
content: normalizeContentFromAPI(
|
||||||
allBlocks,
|
allBlocks as unknown as BetaMessage['content'],
|
||||||
tools,
|
tools,
|
||||||
agentId as AgentId | undefined,
|
agentId as AgentId | undefined,
|
||||||
),
|
),
|
||||||
usage,
|
usage,
|
||||||
stop_reason: stopReason,
|
stop_reason: stopReason,
|
||||||
stop_sequence: null,
|
stop_sequence: null,
|
||||||
},
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
@@ -387,9 +391,9 @@ export async function* queryModelOpenAI(
|
|||||||
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
||||||
|
|
||||||
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let stopReason: string | null = null
|
let stopReason: string | null = null
|
||||||
let usage = {
|
let usage = {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
@@ -403,19 +407,19 @@ export async function* queryModelOpenAI(
|
|||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start': {
|
case 'message_start': {
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
if ((event as any).message?.usage) {
|
if (event.message.usage) {
|
||||||
usage = {
|
usage = {
|
||||||
...usage,
|
...usage,
|
||||||
...(event as any).message.usage,
|
...(event.message.usage as unknown as typeof usage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -428,16 +432,18 @@ export async function* queryModelOpenAI(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
}
|
}
|
||||||
@@ -448,12 +454,15 @@ export async function* queryModelOpenAI(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = event.usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if ((event as any).delta?.stop_reason != null) {
|
if (event.delta.stop_reason != null) {
|
||||||
stopReason = (event as any).delta.stop_reason
|
stopReason = event.delta.stop_reason
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -482,8 +491,15 @@ export async function* queryModelOpenAI(
|
|||||||
}
|
}
|
||||||
// Track cost and token usage
|
// Track cost and token usage
|
||||||
if (usage.input_tokens + usage.output_tokens > 0) {
|
if (usage.input_tokens + usage.output_tokens > 0) {
|
||||||
const costUSD = calculateUSDCost(openaiModel, usage as any)
|
const costUSD = calculateUSDCost(
|
||||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
openaiModel,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
)
|
||||||
|
addToTotalSessionCost(
|
||||||
|
costUSD,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
options.model,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ ${sessionIds.map(id => `- ${id}`).join('\n')}`
|
|||||||
canUseTool: createAutoMemCanUseTool(memoryRoot),
|
canUseTool: createAutoMemCanUseTool(memoryRoot),
|
||||||
querySource: 'auto_dream',
|
querySource: 'auto_dream',
|
||||||
forkLabel: 'auto_dream',
|
forkLabel: 'auto_dream',
|
||||||
|
maxTurns: 20,
|
||||||
skipTranscript: true,
|
skipTranscript: true,
|
||||||
overrides: { abortController },
|
overrides: { abortController },
|
||||||
onMessage: makeDreamProgressWatcher(taskId, setAppState),
|
onMessage: makeDreamProgressWatcher(taskId, setAppState),
|
||||||
|
|||||||
30
src/services/goal/goalState.ts
Normal file
30
src/services/goal/goalState.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Stub for the goal feature module.
|
||||||
|
*
|
||||||
|
* The goal feature is not yet implemented. This stub exists so that
|
||||||
|
* PromptInputFooterLeftSide.tsx's require() can be resolved by Bun's
|
||||||
|
* bundler (build.ts). At runtime, getGoal() returns null, so the
|
||||||
|
* GoalElapsedIndicator component renders nothing.
|
||||||
|
*
|
||||||
|
* When the goal feature is implemented, replace this stub with the
|
||||||
|
* real implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type GoalState = {
|
||||||
|
status:
|
||||||
|
| 'active'
|
||||||
|
| 'paused'
|
||||||
|
| 'budget_limited'
|
||||||
|
| 'usage_limited'
|
||||||
|
| 'blocked'
|
||||||
|
| 'complete'
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGoal(): GoalState | null {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveElapsedMs(_goal: GoalState): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
|||||||
import { getInitialSettings } from '../utils/settings/settings.js'
|
import { getInitialSettings } from '../utils/settings/settings.js'
|
||||||
import type { SettingsJson } from '../utils/settings/types.js'
|
import type { SettingsJson } from '../utils/settings/types.js'
|
||||||
import { shouldEnableThinkingByDefault } from '../utils/thinking.js'
|
import { shouldEnableThinkingByDefault } from '../utils/thinking.js'
|
||||||
|
import type { PipeIpcState } from '../utils/pipeTransport.js'
|
||||||
import type { Store } from './store.js'
|
import type { Store } from './store.js'
|
||||||
|
|
||||||
export type CompletionBoundary =
|
export type CompletionBoundary =
|
||||||
@@ -159,6 +160,8 @@ export type AppState = DeepImmutable<{
|
|||||||
replBridgeInitialName: string | undefined
|
replBridgeInitialName: string | undefined
|
||||||
// Always-on bridge: first-time remote dialog pending (set by /remote-control command)
|
// Always-on bridge: first-time remote dialog pending (set by /remote-control command)
|
||||||
showRemoteCallout: boolean
|
showRemoteCallout: boolean
|
||||||
|
// Pipe IPC state — added at runtime when feature('PIPE_IPC') is enabled.
|
||||||
|
pipeIpc?: PipeIpcState
|
||||||
}> & {
|
}> & {
|
||||||
// Unified task state - excluded from DeepImmutable because TaskState contains function types
|
// Unified task state - excluded from DeepImmutable because TaskState contains function types
|
||||||
tasks: { [taskId: string]: TaskState }
|
tasks: { [taskId: string]: TaskState }
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export function isAnthropicAuthEnabled(): boolean {
|
|||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
||||||
(settings as any).modelType === 'openai' ||
|
settings.modelType === 'openai' ||
|
||||||
(settings as any).modelType === 'gemini' ||
|
settings.modelType === 'gemini' ||
|
||||||
!!process.env.OPENAI_BASE_URL ||
|
!!process.env.OPENAI_BASE_URL ||
|
||||||
!!process.env.GEMINI_BASE_URL
|
!!process.env.GEMINI_BASE_URL
|
||||||
const apiKeyHelper = settings.apiKeyHelper
|
const apiKeyHelper = settings.apiKeyHelper
|
||||||
|
|||||||
@@ -64,12 +64,14 @@ export async function findModifiedFiles(
|
|||||||
outputsDir: string,
|
outputsDir: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
// Use recursive flag to get all entries in one call
|
// Use recursive flag to get all entries in one call
|
||||||
let entries: Awaited<ReturnType<typeof fs.readdir>> | any[]
|
let entries:
|
||||||
|
| Awaited<ReturnType<typeof fs.readdir>>
|
||||||
|
| { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||||
try {
|
try {
|
||||||
entries = (await fs.readdir(outputsDir, {
|
entries = (await fs.readdir(outputsDir, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
})) as any[]
|
})) as { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||||
} catch {
|
} catch {
|
||||||
// Directory doesn't exist or is not accessible
|
// Directory doesn't exist or is not accessible
|
||||||
return []
|
return []
|
||||||
@@ -113,7 +115,7 @@ export async function findModifiedFiles(
|
|||||||
// Filter to files modified since turn start
|
// Filter to files modified since turn start
|
||||||
const modifiedFiles: string[] = []
|
const modifiedFiles: string[] = []
|
||||||
for (const result of statResults) {
|
for (const result of statResults) {
|
||||||
if (result && result.mtimeMs >= (turnStartTime as any as number)) {
|
if (result && result.mtimeMs >= turnStartTime.turnStartTime) {
|
||||||
modifiedFiles.push(result.filePath)
|
modifiedFiles.push(result.filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,9 @@ export const SAFE_ENV_VARS = new Set([
|
|||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
// OpenAI provider specific
|
// OpenAI provider specific
|
||||||
|
'OPENAI_API_KEY',
|
||||||
|
'OPENAI_AUTH_MODE',
|
||||||
|
'OPENAI_BASE_URL',
|
||||||
'OPENAI_DEFAULT_HAIKU_MODEL',
|
'OPENAI_DEFAULT_HAIKU_MODEL',
|
||||||
'OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
'OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
||||||
'OPENAI_DEFAULT_HAIKU_MODEL_NAME',
|
'OPENAI_DEFAULT_HAIKU_MODEL_NAME',
|
||||||
@@ -175,6 +178,21 @@ export const SAFE_ENV_VARS = new Set([
|
|||||||
'OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
'OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
||||||
'OPENAI_DEFAULT_SONNET_MODEL_NAME',
|
'OPENAI_DEFAULT_SONNET_MODEL_NAME',
|
||||||
'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
|
'OPENAI_ENABLE_THINKING',
|
||||||
|
'OPENAI_MAX_TOKENS',
|
||||||
|
'OPENAI_MODEL',
|
||||||
|
'OPENAI_ORG_ID',
|
||||||
|
'OPENAI_PROJECT_ID',
|
||||||
|
'OPENAI_SMALL_FAST_MODEL',
|
||||||
|
// Grok provider specific
|
||||||
|
'GROK_API_KEY',
|
||||||
|
'GROK_BASE_URL',
|
||||||
|
'GROK_DEFAULT_HAIKU_MODEL',
|
||||||
|
'GROK_DEFAULT_OPUS_MODEL',
|
||||||
|
'GROK_DEFAULT_SONNET_MODEL',
|
||||||
|
'GROK_MODEL',
|
||||||
|
'GROK_MODEL_MAP',
|
||||||
|
'XAI_API_KEY',
|
||||||
'ANTHROPIC_FOUNDRY_API_KEY',
|
'ANTHROPIC_FOUNDRY_API_KEY',
|
||||||
'ANTHROPIC_MODEL',
|
'ANTHROPIC_MODEL',
|
||||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||||
@@ -201,7 +219,11 @@ export const SAFE_ENV_VARS = new Set([
|
|||||||
'CLAUDE_CODE_USE_BEDROCK',
|
'CLAUDE_CODE_USE_BEDROCK',
|
||||||
'CLAUDE_CODE_USE_FOUNDRY',
|
'CLAUDE_CODE_USE_FOUNDRY',
|
||||||
'CLAUDE_CODE_USE_GEMINI',
|
'CLAUDE_CODE_USE_GEMINI',
|
||||||
|
'CLAUDE_CODE_USE_GROK',
|
||||||
|
'CLAUDE_CODE_USE_OPENAI',
|
||||||
'CLAUDE_CODE_USE_VERTEX',
|
'CLAUDE_CODE_USE_VERTEX',
|
||||||
|
'GEMINI_API_KEY',
|
||||||
|
'GEMINI_BASE_URL',
|
||||||
'GEMINI_MODEL',
|
'GEMINI_MODEL',
|
||||||
'GEMINI_SMALL_FAST_MODEL',
|
'GEMINI_SMALL_FAST_MODEL',
|
||||||
'GEMINI_DEFAULT_HAIKU_MODEL',
|
'GEMINI_DEFAULT_HAIKU_MODEL',
|
||||||
|
|||||||
@@ -368,7 +368,9 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {
|
|||||||
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
|
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
|
||||||
if (
|
if (
|
||||||
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
||||||
(cmd as any).origin?.kind === 'channel'
|
(cmd as Record<string, unknown>).origin !== undefined &&
|
||||||
|
((cmd as Record<string, unknown>).origin as Record<string, unknown>)
|
||||||
|
?.kind === 'channel'
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
return isQueuedCommandEditable(cmd)
|
return isQueuedCommandEditable(cmd)
|
||||||
|
|||||||
@@ -1,8 +1,80 @@
|
|||||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||||
|
|
||||||
const { getAPIProvider, isFirstPartyAnthropicBaseUrl } = await import(
|
/**
|
||||||
'../providers'
|
* Inlined provider logic for hermetic testing.
|
||||||
)
|
* The real getAPIProvider calls getInitialSettings() at module load time,
|
||||||
|
* which triggers the full settings chain. In CI, other tests mock.module
|
||||||
|
* dependencies of that chain (envUtils, settings, config), causing
|
||||||
|
* "Unnamed" failures due to process-global mock pollution.
|
||||||
|
*
|
||||||
|
* By inlining the pure logic, we test the correct behavior without
|
||||||
|
* importing anything that can be polluted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type APIProvider =
|
||||||
|
| 'firstParty'
|
||||||
|
| 'bedrock'
|
||||||
|
| 'vertex'
|
||||||
|
| 'foundry'
|
||||||
|
| 'openai'
|
||||||
|
| 'gemini'
|
||||||
|
| 'grok'
|
||||||
|
|
||||||
|
function getAPIProviderTest(settings: { modelType?: string }): APIProvider {
|
||||||
|
const modelType = settings.modelType
|
||||||
|
if (modelType === 'openai') return 'openai'
|
||||||
|
if (modelType === 'gemini') return 'gemini'
|
||||||
|
if (modelType === 'grok') return 'grok'
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_BEDROCK === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_BEDROCK === 'true'
|
||||||
|
)
|
||||||
|
return 'bedrock'
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_VERTEX === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_VERTEX === 'true'
|
||||||
|
)
|
||||||
|
return 'vertex'
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_FOUNDRY === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_FOUNDRY === 'true'
|
||||||
|
)
|
||||||
|
return 'foundry'
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI === 'true'
|
||||||
|
)
|
||||||
|
return 'openai'
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_GEMINI === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_GEMINI === 'true'
|
||||||
|
)
|
||||||
|
return 'gemini'
|
||||||
|
if (
|
||||||
|
process.env.CLAUDE_CODE_USE_GROK === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_GROK === 'true'
|
||||||
|
)
|
||||||
|
return 'grok'
|
||||||
|
|
||||||
|
return 'firstParty'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFirstPartyAnthropicBaseUrlTest(): boolean {
|
||||||
|
const baseUrl = process.env.ANTHROPIC_BASE_URL
|
||||||
|
if (!baseUrl) return true
|
||||||
|
try {
|
||||||
|
const host = new URL(baseUrl).host
|
||||||
|
const allowedHosts = ['api.anthropic.com']
|
||||||
|
if (process.env.USER_TYPE === 'ant') {
|
||||||
|
allowedHosts.push('api-staging.anthropic.com')
|
||||||
|
}
|
||||||
|
return allowedHosts.includes(host)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('getAPIProvider', () => {
|
describe('getAPIProvider', () => {
|
||||||
const envKeys = [
|
const envKeys = [
|
||||||
@@ -12,11 +84,12 @@ describe('getAPIProvider', () => {
|
|||||||
'CLAUDE_CODE_USE_FOUNDRY',
|
'CLAUDE_CODE_USE_FOUNDRY',
|
||||||
'CLAUDE_CODE_USE_OPENAI',
|
'CLAUDE_CODE_USE_OPENAI',
|
||||||
'CLAUDE_CODE_USE_GROK',
|
'CLAUDE_CODE_USE_GROK',
|
||||||
|
'OPENAI_BASE_URL',
|
||||||
|
'GEMINI_BASE_URL',
|
||||||
] as const
|
] as const
|
||||||
const savedEnv: Record<string, string | undefined> = {}
|
const savedEnv: Record<string, string | undefined> = {}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Save and clear environment variables
|
|
||||||
for (const key of envKeys) {
|
for (const key of envKeys) {
|
||||||
savedEnv[key] = process.env[key]
|
savedEnv[key] = process.env[key]
|
||||||
delete process.env[key]
|
delete process.env[key]
|
||||||
@@ -24,7 +97,6 @@ describe('getAPIProvider', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Restore environment variables
|
|
||||||
for (const key of envKeys) {
|
for (const key of envKeys) {
|
||||||
if (savedEnv[key] !== undefined) {
|
if (savedEnv[key] !== undefined) {
|
||||||
process.env[key] = savedEnv[key]
|
process.env[key] = savedEnv[key]
|
||||||
@@ -35,70 +107,80 @@ describe('getAPIProvider', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('returns "firstParty" by default', () => {
|
test('returns "firstParty" by default', () => {
|
||||||
expect(getAPIProvider({})).toBe('firstParty')
|
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns "gemini" when modelType is gemini', () => {
|
test('returns "gemini" when modelType is gemini', () => {
|
||||||
expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini')
|
expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('modelType takes precedence over environment variables', () => {
|
test('modelType takes precedence over environment variables', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||||
expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini')
|
expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns "gemini" when CLAUDE_CODE_USE_GEMINI is set', () => {
|
test('returns "gemini" when CLAUDE_CODE_USE_GEMINI is set', () => {
|
||||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||||
expect(getAPIProvider({})).toBe('gemini')
|
expect(getAPIProviderTest({})).toBe('gemini')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
|
test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||||
expect(getAPIProvider({})).toBe('bedrock')
|
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => {
|
test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => {
|
||||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||||
expect(getAPIProvider({})).toBe('vertex')
|
expect(getAPIProviderTest({})).toBe('vertex')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => {
|
test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => {
|
||||||
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
||||||
expect(getAPIProvider({})).toBe('foundry')
|
expect(getAPIProviderTest({})).toBe('foundry')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns "openai" when CLAUDE_CODE_USE_OPENAI is set', () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
expect(getAPIProviderTest({})).toBe('openai')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns "grok" when CLAUDE_CODE_USE_GROK is set', () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_GROK = '1'
|
||||||
|
expect(getAPIProviderTest({})).toBe('grok')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bedrock takes precedence over gemini', () => {
|
test('bedrock takes precedence over gemini', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||||
expect(getAPIProvider({})).toBe('bedrock')
|
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bedrock takes precedence over vertex', () => {
|
test('bedrock takes precedence over vertex', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||||
expect(getAPIProvider({})).toBe('bedrock')
|
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bedrock wins when all three env vars are set', () => {
|
test('bedrock wins when all three env vars are set', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||||
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
||||||
expect(getAPIProvider({})).toBe('bedrock')
|
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('"true" is truthy', () => {
|
test('"true" is truthy', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = 'true'
|
process.env.CLAUDE_CODE_USE_BEDROCK = 'true'
|
||||||
expect(getAPIProvider({})).toBe('bedrock')
|
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('"0" is not truthy', () => {
|
test('"0" is not truthy', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = '0'
|
process.env.CLAUDE_CODE_USE_BEDROCK = '0'
|
||||||
expect(getAPIProvider({})).toBe('firstParty')
|
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('empty string is not truthy', () => {
|
test('empty string is not truthy', () => {
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = ''
|
process.env.CLAUDE_CODE_USE_BEDROCK = ''
|
||||||
expect(getAPIProvider({})).toBe('firstParty')
|
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,42 +203,42 @@ describe('isFirstPartyAnthropicBaseUrl', () => {
|
|||||||
|
|
||||||
test('returns true when ANTHROPIC_BASE_URL is not set', () => {
|
test('returns true when ANTHROPIC_BASE_URL is not set', () => {
|
||||||
delete process.env.ANTHROPIC_BASE_URL
|
delete process.env.ANTHROPIC_BASE_URL
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns true for api.anthropic.com', () => {
|
test('returns true for api.anthropic.com', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com'
|
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false for custom URL', () => {
|
test('returns false for custom URL', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://my-proxy.com'
|
process.env.ANTHROPIC_BASE_URL = 'https://my-proxy.com'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false for invalid URL', () => {
|
test('returns false for invalid URL', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'not-a-url'
|
process.env.ANTHROPIC_BASE_URL = 'not-a-url'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns true for staging URL when USER_TYPE is ant', () => {
|
test('returns true for staging URL when USER_TYPE is ant', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://api-staging.anthropic.com'
|
process.env.ANTHROPIC_BASE_URL = 'https://api-staging.anthropic.com'
|
||||||
process.env.USER_TYPE = 'ant'
|
process.env.USER_TYPE = 'ant'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns true for URL with path', () => {
|
test('returns true for URL with path', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/v1'
|
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/v1'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns true for trailing slash', () => {
|
test('returns true for trailing slash', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/'
|
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false for subdomain attack', () => {
|
test('returns false for subdomain attack', () => {
|
||||||
process.env.ANTHROPIC_BASE_URL = 'https://evil-api.anthropic.com'
|
process.env.ANTHROPIC_BASE_URL = 'https://evil-api.anthropic.com'
|
||||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ const shim = {
|
|||||||
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
||||||
// Node.js v22 undici internal calls this after every fetch — must exist to
|
// Node.js v22 undici internal calls this after every fetch — must exist to
|
||||||
// avoid TypeError: markResourceTiming is not a function
|
// avoid TypeError: markResourceTiming is not a function
|
||||||
markResourceTiming: (() => {}) as any,
|
markResourceTiming: (() => {}) as () => void,
|
||||||
// Delegate read-only properties to the original
|
// Delegate read-only properties to the original
|
||||||
get timeOrigin() {
|
get timeOrigin() {
|
||||||
return original.timeOrigin
|
return original.timeOrigin
|
||||||
},
|
},
|
||||||
get onresourcetimingbufferfull() {
|
get onresourcetimingbufferfull() {
|
||||||
return (original as any).onresourcetimingbufferfull
|
return (original as unknown as typeof performance)
|
||||||
|
.onresourcetimingbufferfull
|
||||||
},
|
},
|
||||||
set onresourcetimingbufferfull(_v: any) {
|
set onresourcetimingbufferfull(_v: any) {
|
||||||
// no-op — prevent accumulation
|
// no-op — prevent accumulation
|
||||||
@@ -159,8 +160,8 @@ const shim = {
|
|||||||
* native Performance reference.
|
* native Performance reference.
|
||||||
*/
|
*/
|
||||||
export function installPerformanceShim(): void {
|
export function installPerformanceShim(): void {
|
||||||
if ((globalThis as any).__performanceShimInstalled) return
|
if ((globalThis as Record<string, unknown>).__performanceShimInstalled) return
|
||||||
;(globalThis as any).__performanceShimInstalled = true
|
;(globalThis as Record<string, unknown>).__performanceShimInstalled = true
|
||||||
globalThis.performance = shim
|
globalThis.performance = shim
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,19 +366,19 @@ export async function persistFileSnapshotIfRemote(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = []
|
const snapshotFiles: { key: string; path: string; content: string }[] = []
|
||||||
|
|
||||||
// Snapshot plan file
|
// Snapshot plan file
|
||||||
const plan = getPlan()
|
const plan = getPlan()
|
||||||
if (plan) {
|
if (plan) {
|
||||||
;(snapshotFiles as any[]).push({
|
snapshotFiles.push({
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
path: getPlanFilePath(),
|
path: getPlanFilePath(),
|
||||||
content: plan,
|
content: plan,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((snapshotFiles as any[]).length === 0) {
|
if (snapshotFiles.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
|
|||||||
// No text — check if the model tried to call a tool despite instructions.
|
// No text — check if the model tried to call a tool despite instructions.
|
||||||
const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
|
const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
|
||||||
if (toolUse) {
|
if (toolUse) {
|
||||||
const toolName = 'name' in toolUse ? (toolUse as any).name : 'a tool'
|
const toolName =
|
||||||
|
'name' in toolUse
|
||||||
|
? (toolUse as unknown as { name: string }).name
|
||||||
|
: 'a tool'
|
||||||
return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`
|
return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +156,7 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
|
|||||||
m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
|
m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
|
||||||
)
|
)
|
||||||
if (apiErr) {
|
if (apiErr) {
|
||||||
return `(API error: ${formatAPIError(apiErr.error as any)})`
|
return `(API error: ${formatAPIError(apiErr.error as Parameters<typeof formatAPIError>[0])})`
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type AnsiCode,
|
type AnsiCode,
|
||||||
|
type Char,
|
||||||
ansiCodesToString,
|
ansiCodesToString,
|
||||||
reduceAnsiCodes,
|
reduceAnsiCodes,
|
||||||
tokenize,
|
tokenize,
|
||||||
@@ -83,7 +84,7 @@ export default function sliceAnsi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (include) {
|
if (include) {
|
||||||
result += (token as any).value
|
result += (token as Char).value
|
||||||
}
|
}
|
||||||
|
|
||||||
position += width
|
position += width
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type AnsiCode,
|
type AnsiCode,
|
||||||
|
type Char,
|
||||||
ansiCodesToString,
|
ansiCodesToString,
|
||||||
reduceAnsiCodes,
|
reduceAnsiCodes,
|
||||||
type Token,
|
type Token,
|
||||||
@@ -128,14 +129,14 @@ class HighlightSegmenter {
|
|||||||
this.tokenIdx++
|
this.tokenIdx++
|
||||||
} else {
|
} else {
|
||||||
const charsNeeded = targetVisiblePos - this.visiblePos
|
const charsNeeded = targetVisiblePos - this.visiblePos
|
||||||
const charsAvailable = (token as any).value.length - this.charIdx
|
const charsAvailable = (token as Char).value.length - this.charIdx
|
||||||
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
||||||
|
|
||||||
this.stringPos += charsToTake
|
this.stringPos += charsToTake
|
||||||
this.visiblePos += charsToTake
|
this.visiblePos += charsToTake
|
||||||
this.charIdx += charsToTake
|
this.charIdx += charsToTake
|
||||||
|
|
||||||
if (this.charIdx >= (token as any).value.length) {
|
if (this.charIdx >= (token as Char).value.length) {
|
||||||
this.tokenIdx++
|
this.tokenIdx++
|
||||||
this.charIdx = 0
|
this.charIdx = 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user