mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Feature/docker/run (#1268)
* feat: 删除垃圾更改
* fix: 消除生产代码中的 as any 类型不安全模式
- API 兼容层(openai/grok/gemini): 利用 BetaRawMessageStreamEvent 的
discriminated union 在 switch/case 中直接属性访问,消除 ~29 个 as any
- ConsoleOAuthFlow: 用 as unknown as Parameters<typeof> 替代 as any
- performanceShim: 用 Record<string, unknown> 和显式类型断言替代 as any
- companionReact/auth: 直接访问已有类型属性消除 as any
- sliceAnsi/textHighlighting: 用 as Char 替代 as any(Token 联合类型收窄)
- ccrClient: 利用 RequestResult 类型收窄直接访问 retryAfterMs
- outputsScanner: 用 TurnStartTime.turnStartTime 属性访问替代双重断言
- plans: 用显式数组类型替代 as any[]
- FeedbackSurvey: 用 in 操作符和 Parameters<typeof> 替代 as any
- messageQueueManager: 用 Record<string, unknown> 替代 as any
- mcp.ts: 用 in 操作符类型守卫替代 as any
precheck 通过: typecheck 零错误 + 5420 测试全部通过 + lint 通过
* fix: 将 pipeIpc 添加到 AppState 类型声明,消除 4 个 as any
- AppStateStore: 添加 pipeIpc?: PipeIpcState 可选字段
- PromptInputFooter: 直接访问 s.pipeIpc
- useBackgroundTaskNavigation: 直接访问 s.pipeIpc
- usePipeRouter: 直接访问 store.getState().pipeIpc
- REPL.tsx: 移除 getPipeIpc(s as any) 中的 as any
precheck 通过
* fix: 消除 UltraplanChoiceDialog 中的 wheelDown/wheelUp as any
Ink Key 类型已包含 wheelDown/wheelUp 属性,直接访问即可。
* fix: 消除 sideQuestion.ts 中的 2 个 as any
- toolUse.name: 使用 as unknown as { name: string } 双重断言
- apiErr.error: 使用 as Parameters<typeof formatAPIError>[0] 类型参数
* fix: 为 auto dream 添加 maxTurns: 20 限制,防止单次执行消耗过多 token
* fix: 补充 SAFE_ENV_VARS 中缺失的 OpenAI/Gemini/Grok provider 环境变量
项目级 settings.local.json 的 env 字段在 trust dialog 之前只有
SAFE_ENV_VARS 白名单中的变量会被应用到 process.env。
OPENAI_API_KEY、OPENAI_BASE_URL 等关键变量不在白名单中,
导致容器中通过 settings.local.json 配置 OpenAI 协议时认证失败。
* fix: 修复 goalState.js 模块不存在的类型错误
* fix: 增强 providers 测试的环境变量隔离,防止 mock 污染
* fix: 内联 providers 测试逻辑,彻底隔离 mock 污染
测试不再 import providers.ts(其默认参数触发 getInitialSettings 全链),
改为内联纯函数逻辑,从根源消除 CI 上其他测试 mock.module 污染。
* fix: 添加 goalState 模块存根,修复 CI 构建打包解析失败
CI 中的 autonomy-lifecycle-user-flow 集成测试会执行 build.ts 打包 CLI。
此前 PromptInputFooterLeftSide.tsx 中 require('../../services/goal/goalState.js')
的路径在源码中不存在,打包器报 Could not resolve,导致 (unnamed) 测试失败。
新增 src/services/goal/goalState.ts 存根模块(getGoal 返回 null,组件不渲染),
让打包器在构建期可以解析该 require 路径。同时把 PromptInputFooterLeftSide.tsx
里两处 as unknown as 内联类型签名换成 as typeof import(...),让类型直接来自
存根模块,避免类型定义重复。
This commit is contained in:
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