mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
fix(types): replace all as any with proper type assertions
Eliminate unsafe `as any` casts across 21 non-test source files, replacing them with specific type annotations: - Bridge transport: use StdoutMessage type for write/writeBatch calls - print.ts: type msg.request as Record<string, unknown> for unknown SDK control subtypes; use StdoutMessage for output.enqueue() - API providers (openai/grok/gemini): import ChatCompletion types, type streams as AsyncIterable<ChatCompletionChunk>, type request bodies as ChatCompletionCreateParamsStreaming - Computer use executor: use Partial<ResolvePrepareCaptureResult> for cross-platform screenshot result - Components: replace Ink color string casts with proper typing - Win32 bridge: type stdin as Writable after null check All 2453 tests pass with 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,8 +69,19 @@ import type {
|
|||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
SDKControlResponse,
|
SDKControlResponse,
|
||||||
} from '../entrypoints/sdk/controlTypes.js'
|
} from '../entrypoints/sdk/controlTypes.js'
|
||||||
|
import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
|
||||||
|
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
||||||
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StdoutMessage with session_id added. The transport layer adds session_id
|
||||||
|
* to messages at runtime, but the Zod schemas don't include it. This type
|
||||||
|
* makes it explicit that we're adding session_id to each message variant.
|
||||||
|
*/
|
||||||
|
type StdoutMessageWithSession = StdoutMessage extends infer T
|
||||||
|
? T & { session_id: string }
|
||||||
|
: never
|
||||||
|
|
||||||
const ANTHROPIC_VERSION = '2023-06-01'
|
const ANTHROPIC_VERSION = '2023-06-01'
|
||||||
|
|
||||||
// Telemetry discriminator for ws_connected. 'initial' is the default and
|
// Telemetry discriminator for ws_connected. 'initial' is the default and
|
||||||
@@ -608,7 +619,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
const msgs = flushGate.end()
|
const msgs = flushGate.end()
|
||||||
if (msgs.length === 0) return
|
if (msgs.length === 0) return
|
||||||
for (const msg of msgs) recentPostedUUIDs.add(msg.uuid)
|
for (const msg of msgs) recentPostedUUIDs.add(msg.uuid)
|
||||||
const events = toSDKMessages(msgs).map(m => ({
|
const events: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({
|
||||||
...m,
|
...m,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
}))
|
}))
|
||||||
@@ -636,7 +647,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
`[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`,
|
`[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const events = toSDKMessages(capped).map(m => ({
|
const events: StdoutMessageWithSession[] = toSDKMessages(capped).map(m => ({
|
||||||
...m,
|
...m,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
}))
|
}))
|
||||||
@@ -675,8 +686,11 @@ export async function initEnvLessBridgeCore(
|
|||||||
// explicit sleep. close() sets closed=true which interrupts drain at the
|
// explicit sleep. close() sets closed=true which interrupts drain at the
|
||||||
// next while-check, so close-before-archive drops the result.
|
// next while-check, so close-before-archive drops the result.
|
||||||
transport.reportState('idle')
|
transport.reportState('idle')
|
||||||
void transport.write(makeResultMessage(sessionId))
|
const resultMsg: StdoutMessageWithSession = {
|
||||||
|
...makeResultMessage(sessionId),
|
||||||
|
session_id: sessionId,
|
||||||
|
}
|
||||||
|
void transport.write(resultMsg)
|
||||||
let token = getAccessToken()
|
let token = getAccessToken()
|
||||||
let status = await archiveSession(
|
let status = await archiveSession(
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -795,7 +809,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const msg of filtered) recentPostedUUIDs.add(msg.uuid)
|
for (const msg of filtered) recentPostedUUIDs.add(msg.uuid)
|
||||||
const events = toSDKMessages(filtered).map(m => ({
|
const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({
|
||||||
...m,
|
...m,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
}))
|
}))
|
||||||
@@ -818,7 +832,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
for (const msg of filtered) {
|
for (const msg of filtered) {
|
||||||
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
||||||
}
|
}
|
||||||
const events = filtered.map(m => ({ ...m, session_id: sessionId }))
|
const events = filtered.map(m => ({ ...m, session_id: sessionId })) as StdoutMessage[]
|
||||||
void transport.writeBatch(events)
|
void transport.writeBatch(events)
|
||||||
},
|
},
|
||||||
sendControlRequest(request: SDKControlRequest) {
|
sendControlRequest(request: SDKControlRequest) {
|
||||||
@@ -828,7 +842,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = { ...request, session_id: sessionId }
|
const event: StdoutMessageWithSession = { ...request, session_id: sessionId }
|
||||||
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
|
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
|
||||||
transport.reportState('requires_action')
|
transport.reportState('requires_action')
|
||||||
}
|
}
|
||||||
@@ -844,7 +858,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = { ...response, session_id: sessionId }
|
const event: StdoutMessageWithSession = { ...response, session_id: sessionId }
|
||||||
transport.reportState('running')
|
transport.reportState('running')
|
||||||
void transport.write(event)
|
void transport.write(event)
|
||||||
logForDebugging('[remote-bridge] Sent control_response')
|
logForDebugging('[remote-bridge] Sent control_response')
|
||||||
@@ -856,7 +870,7 @@ export async function initEnvLessBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = {
|
const event: StdoutMessageWithSession = {
|
||||||
type: 'control_cancel_request' as const,
|
type: 'control_cancel_request' as const,
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
@@ -876,7 +890,11 @@ export async function initEnvLessBridgeCore(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
transport.reportState('idle')
|
transport.reportState('idle')
|
||||||
void transport.write(makeResultMessage(sessionId))
|
const resultMsg: StdoutMessageWithSession = {
|
||||||
|
...makeResultMessage(sessionId),
|
||||||
|
session_id: sessionId,
|
||||||
|
}
|
||||||
|
void transport.write(resultMsg)
|
||||||
logForDebugging(`[remote-bridge] Sent result`)
|
logForDebugging(`[remote-bridge] Sent result`)
|
||||||
},
|
},
|
||||||
async teardown() {
|
async teardown() {
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ import type {
|
|||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
SDKControlResponse,
|
SDKControlResponse,
|
||||||
} from '../entrypoints/sdk/controlTypes.js'
|
} from '../entrypoints/sdk/controlTypes.js'
|
||||||
|
import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
|
||||||
|
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StdoutMessage with session_id added. The transport layer adds session_id
|
||||||
|
* to messages at runtime, but the Zod schemas don't include it. This type
|
||||||
|
* makes it explicit that we're adding session_id to each message variant.
|
||||||
|
*/
|
||||||
|
type StdoutMessageWithSession = StdoutMessage extends infer T
|
||||||
|
? T & { session_id: string }
|
||||||
|
: never
|
||||||
import { createCapacityWake, type CapacitySignal } from './capacityWake.js'
|
import { createCapacityWake, type CapacitySignal } from './capacityWake.js'
|
||||||
import { FlushGate } from './flushGate.js'
|
import { FlushGate } from './flushGate.js'
|
||||||
import {
|
import {
|
||||||
@@ -865,7 +876,7 @@ export async function initBridgeCore(
|
|||||||
recentPostedUUIDs.add(msg.uuid)
|
recentPostedUUIDs.add(msg.uuid)
|
||||||
}
|
}
|
||||||
const sdkMessages = toSDKMessages(msgs)
|
const sdkMessages = toSDKMessages(msgs)
|
||||||
const events = sdkMessages.map(sdkMsg => ({
|
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
|
||||||
...sdkMsg,
|
...sdkMsg,
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
}))
|
}))
|
||||||
@@ -1285,7 +1296,7 @@ export async function initBridgeCore(
|
|||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`,
|
`[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`,
|
||||||
)
|
)
|
||||||
const events = sdkMessages.map(sdkMsg => ({
|
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
|
||||||
...sdkMsg,
|
...sdkMsg,
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
}))
|
}))
|
||||||
@@ -1655,7 +1666,11 @@ export async function initBridgeCore(
|
|||||||
transport = null
|
transport = null
|
||||||
flushGate.drop()
|
flushGate.drop()
|
||||||
if (teardownTransport) {
|
if (teardownTransport) {
|
||||||
void teardownTransport.write(makeResultMessage(currentSessionId))
|
const resultMsg: StdoutMessageWithSession = {
|
||||||
|
...makeResultMessage(currentSessionId),
|
||||||
|
session_id: currentSessionId,
|
||||||
|
}
|
||||||
|
void teardownTransport.write(resultMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopWorkP = currentWorkId
|
const stopWorkP = currentWorkId
|
||||||
@@ -1778,7 +1793,7 @@ export async function initBridgeCore(
|
|||||||
// Convert to SDK format and send via HTTP POST (HybridTransport).
|
// Convert to SDK format and send via HTTP POST (HybridTransport).
|
||||||
// The web UI receives them via the subscribe WebSocket.
|
// The web UI receives them via the subscribe WebSocket.
|
||||||
const sdkMessages = toSDKMessages(filtered)
|
const sdkMessages = toSDKMessages(filtered)
|
||||||
const events = sdkMessages.map(sdkMsg => ({
|
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
|
||||||
...sdkMsg,
|
...sdkMsg,
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
}))
|
}))
|
||||||
@@ -1803,7 +1818,7 @@ export async function initBridgeCore(
|
|||||||
for (const msg of filtered) {
|
for (const msg of filtered) {
|
||||||
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
||||||
}
|
}
|
||||||
const events = filtered.map(m => ({ ...m, session_id: currentSessionId }))
|
const events: StdoutMessageWithSession[] = filtered.map(m => ({ ...m, session_id: currentSessionId }))
|
||||||
void transport.writeBatch(events)
|
void transport.writeBatch(events)
|
||||||
},
|
},
|
||||||
sendControlRequest(request: SDKControlRequest) {
|
sendControlRequest(request: SDKControlRequest) {
|
||||||
@@ -1813,7 +1828,7 @@ export async function initBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = { ...request, session_id: currentSessionId }
|
const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId }
|
||||||
void transport.write(event)
|
void transport.write(event)
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
|
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
|
||||||
@@ -1826,7 +1841,7 @@ export async function initBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = { ...response, session_id: currentSessionId }
|
const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId }
|
||||||
void transport.write(event)
|
void transport.write(event)
|
||||||
logForDebugging('[bridge:repl] Sent control_response')
|
logForDebugging('[bridge:repl] Sent control_response')
|
||||||
},
|
},
|
||||||
@@ -1837,7 +1852,7 @@ export async function initBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const event = {
|
const event: StdoutMessageWithSession = {
|
||||||
type: 'control_cancel_request' as const,
|
type: 'control_cancel_request' as const,
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
@@ -1854,7 +1869,11 @@ export async function initBridgeCore(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void transport.write(makeResultMessage(currentSessionId))
|
const resultMsg: StdoutMessageWithSession = {
|
||||||
|
...makeResultMessage(currentSessionId),
|
||||||
|
session_id: currentSessionId,
|
||||||
|
}
|
||||||
|
void transport.write(resultMsg)
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[bridge:repl] Sent result for session=${currentSessionId}`,
|
`[bridge:repl] Sent result for session=${currentSessionId}`,
|
||||||
)
|
)
|
||||||
|
|||||||
332
src/cli/print.ts
332
src/cli/print.ts
@@ -1128,7 +1128,7 @@ function runHeadlessStreaming(
|
|||||||
rate_limit_info: rateLimitInfo,
|
rate_limit_info: rateLimitInfo,
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
session_id: getSessionId(),
|
session_id: getSessionId(),
|
||||||
})
|
} as unknown as Parameters<typeof output.enqueue>[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statusListeners.add(rateLimitListener)
|
statusListeners.add(rateLimitListener)
|
||||||
@@ -1237,7 +1237,7 @@ function runHeadlessStreaming(
|
|||||||
uuid: crumb.uuid,
|
uuid: crumb.uuid,
|
||||||
timestamp: crumb.timestamp,
|
timestamp: crumb.timestamp,
|
||||||
isReplay: true,
|
isReplay: true,
|
||||||
} as SDKUserMessageReplay)
|
} as SDKUserMessageReplay as StdoutMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1974,7 +1974,7 @@ function runHeadlessStreaming(
|
|||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
uuid: c.uuid as string,
|
uuid: c.uuid as string,
|
||||||
isReplay: true,
|
isReplay: true,
|
||||||
} as SDKUserMessageReplay)
|
} as SDKUserMessageReplay as StdoutMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2200,7 +2200,7 @@ function runHeadlessStreaming(
|
|||||||
output.enqueue({
|
output.enqueue({
|
||||||
type: 'system',
|
type: 'system',
|
||||||
subtype: 'status',
|
subtype: 'status',
|
||||||
status,
|
status: status as 'compacting' | null,
|
||||||
session_id: getSessionId(),
|
session_id: getSessionId(),
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
})
|
})
|
||||||
@@ -2227,10 +2227,10 @@ function runHeadlessStreaming(
|
|||||||
isBackgroundTask(t),
|
isBackgroundTask(t),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
heldBackResult = message
|
heldBackResult = message as StdoutMessage
|
||||||
} else {
|
} else {
|
||||||
heldBackResult = null
|
heldBackResult = null
|
||||||
output.enqueue(message)
|
output.enqueue(message as StdoutMessage)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Flush SDK events (task_started, task_progress) so background
|
// Flush SDK events (task_started, task_progress) so background
|
||||||
@@ -2238,7 +2238,7 @@ function runHeadlessStreaming(
|
|||||||
for (const event of drainSdkEvents()) {
|
for (const event of drainSdkEvents()) {
|
||||||
output.enqueue(event)
|
output.enqueue(event)
|
||||||
}
|
}
|
||||||
output.enqueue(message)
|
output.enqueue(message as StdoutMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) // end runWithWorkload
|
}) // end runWithWorkload
|
||||||
@@ -2256,11 +2256,12 @@ function runHeadlessStreaming(
|
|||||||
{ turnStartTime } as import('src/utils/filePersistence/types.js').TurnStartTime,
|
{ turnStartTime } as import('src/utils/filePersistence/types.js').TurnStartTime,
|
||||||
abortController.signal,
|
abortController.signal,
|
||||||
result => {
|
result => {
|
||||||
|
const filesResult = result as { persistedFiles: { filename: string; file_id: string }[]; failedFiles: { filename: string; error: string }[] }
|
||||||
output.enqueue({
|
output.enqueue({
|
||||||
type: 'system' as const,
|
type: 'system' as const,
|
||||||
subtype: 'files_persisted' as const,
|
subtype: 'files_persisted' as const,
|
||||||
files: (result as any).persistedFiles,
|
files: filesResult.persistedFiles,
|
||||||
failed: (result as any).failedFiles,
|
failed: filesResult.failedFiles,
|
||||||
processed_at: new Date().toISOString(),
|
processed_at: new Date().toISOString(),
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
session_id: getSessionId(),
|
session_id: getSessionId(),
|
||||||
@@ -2730,7 +2731,7 @@ function runHeadlessStreaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendControlResponseSuccess = function (
|
const sendControlResponseSuccess = function (
|
||||||
message: SDKControlRequest,
|
message: { request_id: string } | SDKControlRequest,
|
||||||
response?: Record<string, unknown>,
|
response?: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
output.enqueue({
|
output.enqueue({
|
||||||
@@ -2744,7 +2745,7 @@ function runHeadlessStreaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendControlResponseError = function (
|
const sendControlResponseError = function (
|
||||||
message: SDKControlRequest,
|
message: { request_id: string } | SDKControlRequest,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
) {
|
) {
|
||||||
output.enqueue({
|
output.enqueue({
|
||||||
@@ -2820,11 +2821,21 @@ function runHeadlessStreaming(
|
|||||||
message.type !== 'user' &&
|
message.type !== 'user' &&
|
||||||
message.type !== 'control_response'
|
message.type !== 'control_response'
|
||||||
) {
|
) {
|
||||||
notifyCommandLifecycle(eventId, 'completed')
|
notifyCommandLifecycle(eventId as string, 'completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'control_request') {
|
if (message.type === 'control_request') {
|
||||||
if (message.request.subtype === 'interrupt') {
|
// Type assertion: structuredInput yields StdinMessage | SDKMessage, but
|
||||||
|
// when type === 'control_request' the object has request_id and request.
|
||||||
|
// The union with SDKMessage (typed as `any`) causes request to be `unknown`.
|
||||||
|
// Cast to SDKControlRequest (via unknown) for type safety on known subtypes,
|
||||||
|
// and use Record<string, unknown> for subtypes not in the zod schema union.
|
||||||
|
const msg = message as unknown as SDKControlRequest
|
||||||
|
// Wider-typed alias for request properties on subtypes not in the zod schema.
|
||||||
|
// The schema union doesn't include end_session, channel_enable, mcp_authenticate,
|
||||||
|
// claude_authenticate, etc. so accessing their properties narrows to `never`.
|
||||||
|
const req = msg.request as Record<string, unknown>
|
||||||
|
if (msg.request.subtype === 'interrupt') {
|
||||||
// Track escapes for attribution (ant-only feature)
|
// Track escapes for attribution (ant-only feature)
|
||||||
if (feature('COMMIT_ATTRIBUTION')) {
|
if (feature('COMMIT_ATTRIBUTION')) {
|
||||||
setAppState(prev => ({
|
setAppState(prev => ({
|
||||||
@@ -2842,10 +2853,10 @@ function runHeadlessStreaming(
|
|||||||
suggestionState.abortController = null
|
suggestionState.abortController = null
|
||||||
suggestionState.lastEmitted = null
|
suggestionState.lastEmitted = null
|
||||||
suggestionState.pendingSuggestion = null
|
suggestionState.pendingSuggestion = null
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'end_session') {
|
} else if (req.subtype === 'end_session') {
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[print.ts] end_session received, reason=${message.request.reason ?? 'unspecified'}`,
|
`[print.ts] end_session received, reason=${req.reason ?? 'unspecified'}`,
|
||||||
)
|
)
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
@@ -2854,16 +2865,16 @@ function runHeadlessStreaming(
|
|||||||
suggestionState.abortController = null
|
suggestionState.abortController = null
|
||||||
suggestionState.lastEmitted = null
|
suggestionState.lastEmitted = null
|
||||||
suggestionState.pendingSuggestion = null
|
suggestionState.pendingSuggestion = null
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
break // exits for-await → falls through to inputClosed=true drain below
|
break // exits for-await → falls through to inputClosed=true drain below
|
||||||
} else if (message.request.subtype === 'initialize') {
|
} else if (msg.request.subtype === 'initialize') {
|
||||||
// SDK MCP server names from the initialize message
|
// SDK MCP server names from the initialize message
|
||||||
// Populated by both browser and ProcessTransport sessions
|
// Populated by both browser and ProcessTransport sessions
|
||||||
if (
|
if (
|
||||||
message.request.sdkMcpServers &&
|
msg.request.sdkMcpServers &&
|
||||||
message.request.sdkMcpServers.length > 0
|
msg.request.sdkMcpServers.length > 0
|
||||||
) {
|
) {
|
||||||
for (const serverName of message.request.sdkMcpServers) {
|
for (const serverName of msg.request.sdkMcpServers) {
|
||||||
// Create placeholder config for SDK MCP servers
|
// Create placeholder config for SDK MCP servers
|
||||||
// The actual server connection is managed by the SDK Query class
|
// The actual server connection is managed by the SDK Query class
|
||||||
sdkMcpConfigs[serverName] = {
|
sdkMcpConfigs[serverName] = {
|
||||||
@@ -2874,8 +2885,8 @@ function runHeadlessStreaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await handleInitializeRequest(
|
await handleInitializeRequest(
|
||||||
message.request,
|
msg.request,
|
||||||
message.request_id,
|
msg.request_id,
|
||||||
initialized,
|
initialized,
|
||||||
output,
|
output,
|
||||||
commands,
|
commands,
|
||||||
@@ -2890,7 +2901,7 @@ function runHeadlessStreaming(
|
|||||||
// Enable prompt suggestions in AppState when SDK consumer opts in.
|
// Enable prompt suggestions in AppState when SDK consumer opts in.
|
||||||
// shouldEnablePromptSuggestion() returns false for non-interactive
|
// shouldEnablePromptSuggestion() returns false for non-interactive
|
||||||
// sessions, but the SDK consumer explicitly requested suggestions.
|
// sessions, but the SDK consumer explicitly requested suggestions.
|
||||||
if (message.request.promptSuggestions) {
|
if (msg.request.promptSuggestions) {
|
||||||
setAppState(prev => {
|
setAppState(prev => {
|
||||||
if (prev.promptSuggestionEnabled) return prev
|
if (prev.promptSuggestionEnabled) return prev
|
||||||
return { ...prev, promptSuggestionEnabled: true }
|
return { ...prev, promptSuggestionEnabled: true }
|
||||||
@@ -2898,7 +2909,7 @@ function runHeadlessStreaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
message.request.agentProgressSummaries &&
|
msg.request.agentProgressSummaries &&
|
||||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', true)
|
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', true)
|
||||||
) {
|
) {
|
||||||
setSdkAgentProgressSummariesEnabled(true)
|
setSdkAgentProgressSummariesEnabled(true)
|
||||||
@@ -2911,13 +2922,13 @@ function runHeadlessStreaming(
|
|||||||
if (hasCommandsInQueue()) {
|
if (hasCommandsInQueue()) {
|
||||||
void run()
|
void run()
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'set_permission_mode') {
|
} else if (msg.request.subtype === 'set_permission_mode') {
|
||||||
const m = message.request // for typescript (TODO: use readonly types to avoid this)
|
const m = msg.request // for typescript (TODO: use readonly types to avoid this)
|
||||||
setAppState(prev => ({
|
setAppState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
toolPermissionContext: handleSetPermissionMode(
|
toolPermissionContext: handleSetPermissionMode(
|
||||||
m,
|
m,
|
||||||
message.request_id,
|
msg.request_id,
|
||||||
prev.toolPermissionContext,
|
prev.toolPermissionContext,
|
||||||
output,
|
output,
|
||||||
),
|
),
|
||||||
@@ -2926,8 +2937,8 @@ function runHeadlessStreaming(
|
|||||||
// handleSetPermissionMode sends the control_response; the
|
// handleSetPermissionMode sends the control_response; the
|
||||||
// notifySessionMetadataChanged that used to follow here is
|
// notifySessionMetadataChanged that used to follow here is
|
||||||
// now fired by onChangeAppState (with externalized mode name).
|
// now fired by onChangeAppState (with externalized mode name).
|
||||||
} else if (message.request.subtype === 'set_model') {
|
} else if (msg.request.subtype === 'set_model') {
|
||||||
const requestedModel = message.request.model ?? 'default'
|
const requestedModel = msg.request.model ?? 'default'
|
||||||
const model =
|
const model =
|
||||||
requestedModel === 'default'
|
requestedModel === 'default'
|
||||||
? getDefaultMainLoopModel()
|
? getDefaultMainLoopModel()
|
||||||
@@ -2937,24 +2948,24 @@ function runHeadlessStreaming(
|
|||||||
notifySessionMetadataChanged({ model })
|
notifySessionMetadataChanged({ model })
|
||||||
injectModelSwitchBreadcrumbs(requestedModel, model)
|
injectModelSwitchBreadcrumbs(requestedModel, model)
|
||||||
|
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'set_max_thinking_tokens') {
|
} else if (msg.request.subtype === 'set_max_thinking_tokens') {
|
||||||
if (message.request.max_thinking_tokens === null) {
|
if (msg.request.max_thinking_tokens === null) {
|
||||||
options.thinkingConfig = undefined
|
options.thinkingConfig = undefined
|
||||||
} else if (message.request.max_thinking_tokens === 0) {
|
} else if (msg.request.max_thinking_tokens === 0) {
|
||||||
options.thinkingConfig = { type: 'disabled' }
|
options.thinkingConfig = { type: 'disabled' }
|
||||||
} else {
|
} else {
|
||||||
options.thinkingConfig = {
|
options.thinkingConfig = {
|
||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
budgetTokens: message.request.max_thinking_tokens,
|
budgetTokens: msg.request.max_thinking_tokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'mcp_status') {
|
} else if (msg.request.subtype === 'mcp_status') {
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
mcpServers: buildMcpServerStatuses(),
|
mcpServers: buildMcpServerStatuses(),
|
||||||
})
|
})
|
||||||
} else if (message.request.subtype === 'get_context_usage') {
|
} else if (msg.request.subtype === 'get_context_usage') {
|
||||||
try {
|
try {
|
||||||
const appState = getAppState()
|
const appState = getAppState()
|
||||||
const data = await collectContextData({
|
const data = await collectContextData({
|
||||||
@@ -2968,13 +2979,13 @@ function runHeadlessStreaming(
|
|||||||
appendSystemPrompt: options.appendSystemPrompt,
|
appendSystemPrompt: options.appendSystemPrompt,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
sendControlResponseSuccess(message, { ...data })
|
sendControlResponseSuccess(msg, { ...data })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(message, errorMessage(error))
|
sendControlResponseError(msg, errorMessage(error))
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'mcp_message') {
|
} else if (msg.request.subtype === 'mcp_message') {
|
||||||
// Handle MCP notifications from SDK servers
|
// Handle MCP notifications from SDK servers
|
||||||
const mcpRequest = message.request
|
const mcpRequest = msg.request as Record<string, unknown>
|
||||||
const sdkClient = sdkClients.find(
|
const sdkClient = sdkClients.find(
|
||||||
client => client.name === mcpRequest.server_name,
|
client => client.name === mcpRequest.server_name,
|
||||||
)
|
)
|
||||||
@@ -2985,32 +2996,32 @@ function runHeadlessStreaming(
|
|||||||
sdkClient.type === 'connected' &&
|
sdkClient.type === 'connected' &&
|
||||||
sdkClient.client?.transport?.onmessage
|
sdkClient.client?.transport?.onmessage
|
||||||
) {
|
) {
|
||||||
sdkClient.client.transport.onmessage(mcpRequest.message)
|
sdkClient.client.transport.onmessage(mcpRequest.message as import('@modelcontextprotocol/sdk/types.js').JSONRPCMessage)
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'rewind_files') {
|
} else if (msg.request.subtype === 'rewind_files') {
|
||||||
const appState = getAppState()
|
const appState = getAppState()
|
||||||
const result = await handleRewindFiles(
|
const result = await handleRewindFiles(
|
||||||
message.request.user_message_id as UUID,
|
msg.request.user_message_id as UUID,
|
||||||
appState,
|
appState,
|
||||||
setAppState,
|
setAppState,
|
||||||
message.request.dry_run ?? false,
|
msg.request.dry_run ?? false,
|
||||||
)
|
)
|
||||||
if (result.canRewind || message.request.dry_run) {
|
if (result.canRewind || msg.request.dry_run) {
|
||||||
sendControlResponseSuccess(message, result)
|
sendControlResponseSuccess(msg, result)
|
||||||
} else {
|
} else {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
(result.error as string) ?? 'Unexpected error',
|
(result.error as string) ?? 'Unexpected error',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'cancel_async_message') {
|
} else if (msg.request.subtype === 'cancel_async_message') {
|
||||||
const targetUuid = message.request.message_uuid
|
const targetUuid = msg.request.message_uuid
|
||||||
const removed = dequeueAllMatching(cmd => cmd.uuid === targetUuid)
|
const removed = dequeueAllMatching(cmd => cmd.uuid === targetUuid)
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
cancelled: removed.length > 0,
|
cancelled: removed.length > 0,
|
||||||
})
|
})
|
||||||
} else if (message.request.subtype === 'seed_read_state') {
|
} else if (msg.request.subtype === 'seed_read_state') {
|
||||||
// Client observed a Read that was later removed from context (e.g.
|
// Client observed a Read that was later removed from context (e.g.
|
||||||
// by snip), so transcript-based seeding missed it. Queued into
|
// by snip), so transcript-based seeding missed it. Queued into
|
||||||
// pendingSeeds; applied at the next clone-replace boundary.
|
// pendingSeeds; applied at the next clone-replace boundary.
|
||||||
@@ -3018,7 +3029,7 @@ function runHeadlessStreaming(
|
|||||||
// expandPath: all other readFileState writers normalize (~, relative,
|
// expandPath: all other readFileState writers normalize (~, relative,
|
||||||
// session cwd vs process cwd). FileEditTool looks up by expandPath'd
|
// session cwd vs process cwd). FileEditTool looks up by expandPath'd
|
||||||
// key — a verbatim client path would miss.
|
// key — a verbatim client path would miss.
|
||||||
const normalizedPath = expandPath(message.request.path)
|
const normalizedPath = expandPath(msg.request.path)
|
||||||
// Check disk mtime before reading content. If the file changed
|
// Check disk mtime before reading content. If the file changed
|
||||||
// since the client's observation, readFile would return C_current
|
// since the client's observation, readFile would return C_current
|
||||||
// but we'd store it with the client's M_observed — getChangedFiles
|
// but we'd store it with the client's M_observed — getChangedFiles
|
||||||
@@ -3028,7 +3039,7 @@ function runHeadlessStreaming(
|
|||||||
// makes Edit fail "file not read yet" → forces a fresh Read.
|
// makes Edit fail "file not read yet" → forces a fresh Read.
|
||||||
// Math.floor matches FileReadTool and getFileModificationTime.
|
// Math.floor matches FileReadTool and getFileModificationTime.
|
||||||
const diskMtime = Math.floor((await stat(normalizedPath)).mtimeMs)
|
const diskMtime = Math.floor((await stat(normalizedPath)).mtimeMs)
|
||||||
if (diskMtime <= message.request.mtime) {
|
if (diskMtime <= msg.request.mtime) {
|
||||||
const raw = await readFile(normalizedPath, 'utf-8')
|
const raw = await readFile(normalizedPath, 'utf-8')
|
||||||
// Strip BOM + normalize CRLF→LF to match readFileInRange and
|
// Strip BOM + normalize CRLF→LF to match readFileInRange and
|
||||||
// readFileSyncWithMetadata. FileEditTool's content-compare
|
// readFileSyncWithMetadata. FileEditTool's content-compare
|
||||||
@@ -3047,18 +3058,18 @@ function runHeadlessStreaming(
|
|||||||
} catch {
|
} catch {
|
||||||
// ENOENT etc — skip seeding but still succeed
|
// ENOENT etc — skip seeding but still succeed
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'mcp_set_servers') {
|
} else if (msg.request.subtype === 'mcp_set_servers') {
|
||||||
const { response, sdkServersChanged } = await applyMcpServerChanges(
|
const { response, sdkServersChanged } = await applyMcpServerChanges(
|
||||||
message.request.servers,
|
msg.request.servers as Record<string, McpServerConfigForProcessTransport>,
|
||||||
)
|
)
|
||||||
sendControlResponseSuccess(message, response)
|
sendControlResponseSuccess(msg, response)
|
||||||
|
|
||||||
// Connect SDK servers AFTER response to avoid deadlock
|
// Connect SDK servers AFTER response to avoid deadlock
|
||||||
if (sdkServersChanged) {
|
if (sdkServersChanged) {
|
||||||
void updateSdkMcp()
|
void updateSdkMcp()
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'reload_plugins') {
|
} else if (msg.request.subtype === 'reload_plugins') {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
feature('DOWNLOAD_USER_SETTINGS') &&
|
feature('DOWNLOAD_USER_SETTINGS') &&
|
||||||
@@ -3106,7 +3117,7 @@ function runHeadlessStreaming(
|
|||||||
logError(pluginsR.reason)
|
logError(pluginsR.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
commands: currentCommands
|
commands: currentCommands
|
||||||
.filter(cmd => cmd.userInvocable !== false)
|
.filter(cmd => cmd.userInvocable !== false)
|
||||||
.map(cmd => ({
|
.map(cmd => ({
|
||||||
@@ -3120,15 +3131,15 @@ function runHeadlessStreaming(
|
|||||||
model: a.model === 'inherit' ? undefined : a.model,
|
model: a.model === 'inherit' ? undefined : a.model,
|
||||||
})),
|
})),
|
||||||
plugins,
|
plugins,
|
||||||
mcpServers: buildMcpServerStatuses(),
|
mcpServers: buildMcpServerStatuses() as SDKControlReloadPluginsResponse['mcpServers'],
|
||||||
error_count: r.error_count,
|
error_count: r.error_count,
|
||||||
} satisfies SDKControlReloadPluginsResponse)
|
} satisfies SDKControlReloadPluginsResponse)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(message, errorMessage(error))
|
sendControlResponseError(msg, errorMessage(error))
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'mcp_reconnect') {
|
} else if (msg.request.subtype === 'mcp_reconnect') {
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
const { serverName } = message.request
|
const { serverName } = msg.request
|
||||||
elicitationRegistered.delete(serverName)
|
elicitationRegistered.delete(serverName)
|
||||||
// Config-existence gate must cover the SAME sources as the
|
// Config-existence gate must cover the SAME sources as the
|
||||||
// operations below. SDK-injected servers (query({mcpServers:{...}}))
|
// operations below. SDK-injected servers (query({mcpServers:{...}}))
|
||||||
@@ -3144,7 +3155,7 @@ function runHeadlessStreaming(
|
|||||||
?.config ??
|
?.config ??
|
||||||
null
|
null
|
||||||
if (!config) {
|
if (!config) {
|
||||||
sendControlResponseError(message, `Server not found: ${serverName}`)
|
sendControlResponseError(msg, `Server not found: ${serverName}`)
|
||||||
} else {
|
} else {
|
||||||
const result = await reconnectMcpServerImpl(serverName, config)
|
const result = await reconnectMcpServerImpl(serverName, config)
|
||||||
// Update appState.mcp with the new client, tools, commands, and resources
|
// Update appState.mcp with the new client, tools, commands, and resources
|
||||||
@@ -3190,18 +3201,18 @@ function runHeadlessStreaming(
|
|||||||
if (result.client.type === 'connected') {
|
if (result.client.type === 'connected') {
|
||||||
registerElicitationHandlers([result.client])
|
registerElicitationHandlers([result.client])
|
||||||
reregisterChannelHandlerAfterReconnect(result.client)
|
reregisterChannelHandlerAfterReconnect(result.client)
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else {
|
} else {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
result.client.type === 'failed'
|
result.client.type === 'failed'
|
||||||
? (result.client.error ?? 'Connection failed')
|
? (result.client.error ?? 'Connection failed')
|
||||||
: `Server status: ${result.client.type}`
|
: `Server status: ${result.client.type}`
|
||||||
sendControlResponseError(message, errorMessage)
|
sendControlResponseError(msg, errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'mcp_toggle') {
|
} else if (msg.request.subtype === 'mcp_toggle') {
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
const { serverName, enabled } = message.request
|
const { serverName, enabled } = msg.request
|
||||||
elicitationRegistered.delete(serverName)
|
elicitationRegistered.delete(serverName)
|
||||||
// Gate must match the client-lookup spread below (which
|
// Gate must match the client-lookup spread below (which
|
||||||
// includes sdkClients and dynamicMcpState.clients). Same fix as
|
// includes sdkClients and dynamicMcpState.clients). Same fix as
|
||||||
@@ -3216,7 +3227,7 @@ function runHeadlessStreaming(
|
|||||||
null
|
null
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
sendControlResponseError(message, `Server not found: ${serverName}`)
|
sendControlResponseError(msg, `Server not found: ${serverName}`)
|
||||||
} else if (!enabled) {
|
} else if (!enabled) {
|
||||||
// Disabling: persist + disconnect (matches TUI toggleMcpServer behavior)
|
// Disabling: persist + disconnect (matches TUI toggleMcpServer behavior)
|
||||||
setMcpServerEnabled(serverName, false)
|
setMcpServerEnabled(serverName, false)
|
||||||
@@ -3247,7 +3258,7 @@ function runHeadlessStreaming(
|
|||||||
resources: omit(prev.mcp.resources, serverName),
|
resources: omit(prev.mcp.resources, serverName),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else {
|
} else {
|
||||||
// Enabling: persist + reconnect
|
// Enabling: persist + reconnect
|
||||||
setMcpServerEnabled(serverName, true)
|
setMcpServerEnabled(serverName, true)
|
||||||
@@ -3281,20 +3292,20 @@ function runHeadlessStreaming(
|
|||||||
if (result.client.type === 'connected') {
|
if (result.client.type === 'connected') {
|
||||||
registerElicitationHandlers([result.client])
|
registerElicitationHandlers([result.client])
|
||||||
reregisterChannelHandlerAfterReconnect(result.client)
|
reregisterChannelHandlerAfterReconnect(result.client)
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else {
|
} else {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
result.client.type === 'failed'
|
result.client.type === 'failed'
|
||||||
? (result.client.error ?? 'Connection failed')
|
? (result.client.error ?? 'Connection failed')
|
||||||
: `Server status: ${result.client.type}`
|
: `Server status: ${result.client.type}`
|
||||||
sendControlResponseError(message, errorMessage)
|
sendControlResponseError(msg, errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'channel_enable') {
|
} else if (req.subtype === 'channel_enable') {
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
handleChannelEnable(
|
handleChannelEnable(
|
||||||
message.request_id,
|
msg.request_id,
|
||||||
message.request.serverName,
|
req.serverName as string,
|
||||||
// Pool spread matches mcp_status — all three client sources.
|
// Pool spread matches mcp_status — all three client sources.
|
||||||
[
|
[
|
||||||
...currentAppState.mcp.clients,
|
...currentAppState.mcp.clients,
|
||||||
@@ -3303,8 +3314,8 @@ function runHeadlessStreaming(
|
|||||||
],
|
],
|
||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
} else if (message.request.subtype === 'mcp_authenticate') {
|
} else if (req.subtype === 'mcp_authenticate') {
|
||||||
const { serverName } = message.request
|
const { serverName } = req
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
const config =
|
const config =
|
||||||
getMcpConfigByName(serverName) ??
|
getMcpConfigByName(serverName) ??
|
||||||
@@ -3313,10 +3324,10 @@ function runHeadlessStreaming(
|
|||||||
?.config ??
|
?.config ??
|
||||||
null
|
null
|
||||||
if (!config) {
|
if (!config) {
|
||||||
sendControlResponseError(message, `Server not found: ${serverName}`)
|
sendControlResponseError(msg, `Server not found: ${serverName}`)
|
||||||
} else if (config.type !== 'sse' && config.type !== 'http') {
|
} else if (config.type !== 'sse' && config.type !== 'http') {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
`Server type "${config.type}" does not support OAuth authentication`,
|
`Server type "${config.type}" does not support OAuth authentication`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -3353,12 +3364,12 @@ function runHeadlessStreaming(
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
authUrl,
|
authUrl,
|
||||||
requiresUserAction: true,
|
requiresUserAction: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
requiresUserAction: false,
|
requiresUserAction: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -3453,11 +3464,11 @@ function runHeadlessStreaming(
|
|||||||
})
|
})
|
||||||
void fullFlowPromise
|
void fullFlowPromise
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(message, errorMessage(error))
|
sendControlResponseError(msg, errorMessage(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'mcp_oauth_callback_url') {
|
} else if (req.subtype === 'mcp_oauth_callback_url') {
|
||||||
const { serverName, callbackUrl } = message.request
|
const { serverName, callbackUrl } = req
|
||||||
const submit = oauthCallbackSubmitters.get(serverName)
|
const submit = oauthCallbackSubmitters.get(serverName)
|
||||||
if (submit) {
|
if (submit) {
|
||||||
// Validate the callback URL before submitting. The submit
|
// Validate the callback URL before submitting. The submit
|
||||||
@@ -3475,7 +3486,7 @@ function runHeadlessStreaming(
|
|||||||
}
|
}
|
||||||
if (!hasCodeOrError) {
|
if (!hasCodeOrError) {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
'Invalid callback URL: missing authorization code. Please paste the full redirect URL including the code parameter.',
|
'Invalid callback URL: missing authorization code. Please paste the full redirect URL including the code parameter.',
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -3488,32 +3499,32 @@ function runHeadlessStreaming(
|
|||||||
if (authPromise) {
|
if (authPromise) {
|
||||||
try {
|
try {
|
||||||
await authPromise
|
await authPromise
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'OAuth authentication failed',
|
: 'OAuth authentication failed',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
`No active OAuth flow for server: ${serverName}`,
|
`No active OAuth flow for server: ${serverName}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'claude_authenticate') {
|
} else if (req.subtype === 'claude_authenticate') {
|
||||||
// Anthropic OAuth over the control channel. The SDK client owns
|
// Anthropic OAuth over the control channel. The SDK client owns
|
||||||
// the user's browser (we're headless in -p mode); we hand back
|
// the user's browser (we're headless in -p mode); we hand back
|
||||||
// both URLs and wait. Automatic URL → localhost listener catches
|
// both URLs and wait. Automatic URL → localhost listener catches
|
||||||
// the redirect if the browser is on this host; manual URL → the
|
// the redirect if the browser is on this host; manual URL → the
|
||||||
// success page shows "code#state" for claude_oauth_callback.
|
// success page shows "code#state" for claude_oauth_callback.
|
||||||
const { loginWithClaudeAi } = message.request
|
const { loginWithClaudeAi } = req
|
||||||
|
|
||||||
// Clean up any prior flow. cleanup() closes the localhost listener
|
// Clean up any prior flow. cleanup() closes the localhost listener
|
||||||
// and nulls the manual resolver. The prior `flow` promise is left
|
// and nulls the manual resolver. The prior `flow` promise is left
|
||||||
@@ -3594,30 +3605,30 @@ function runHeadlessStreaming(
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
manualUrl,
|
manualUrl,
|
||||||
automaticUrl,
|
automaticUrl,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(message, errorMessage(error))
|
sendControlResponseError(msg, errorMessage(error))
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
message.request.subtype === 'claude_oauth_callback' ||
|
req.subtype === 'claude_oauth_callback' ||
|
||||||
message.request.subtype === 'claude_oauth_wait_for_completion'
|
req.subtype === 'claude_oauth_wait_for_completion'
|
||||||
) {
|
) {
|
||||||
if (!claudeOAuth) {
|
if (!claudeOAuth) {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
'No active claude_authenticate flow',
|
'No active claude_authenticate flow',
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Inject the manual code synchronously — must happen in stdin
|
// Inject the manual code synchronously — must happen in stdin
|
||||||
// message order so a subsequent claude_authenticate doesn't
|
// message order so a subsequent claude_authenticate doesn't
|
||||||
// replace the service before this code lands.
|
// replace the service before this code lands.
|
||||||
if (message.request.subtype === 'claude_oauth_callback') {
|
if (req.subtype === 'claude_oauth_callback') {
|
||||||
claudeOAuth.service.handleManualAuthCodeInput({
|
claudeOAuth.service.handleManualAuthCodeInput({
|
||||||
authorizationCode: message.request.authorizationCode,
|
authorizationCode: req.authorizationCode as string,
|
||||||
state: message.request.state,
|
state: req.state as string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Detach the await — the stdin reader is serial and blocking
|
// Detach the await — the stdin reader is serial and blocking
|
||||||
@@ -3629,7 +3640,7 @@ function runHeadlessStreaming(
|
|||||||
void flow.then(
|
void flow.then(
|
||||||
() => {
|
() => {
|
||||||
const accountInfo = getAccountInformation()
|
const accountInfo = getAccountInformation()
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
account: {
|
account: {
|
||||||
email: accountInfo?.email,
|
email: accountInfo?.email,
|
||||||
organization: accountInfo?.organization,
|
organization: accountInfo?.organization,
|
||||||
@@ -3641,11 +3652,11 @@ function runHeadlessStreaming(
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
(error: unknown) =>
|
(error: unknown) =>
|
||||||
sendControlResponseError(message, errorMessage(error)),
|
sendControlResponseError(msg, errorMessage(error)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'mcp_clear_auth') {
|
} else if (req.subtype === 'mcp_clear_auth') {
|
||||||
const { serverName } = message.request
|
const { serverName } = req
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
const config =
|
const config =
|
||||||
getMcpConfigByName(serverName) ??
|
getMcpConfigByName(serverName) ??
|
||||||
@@ -3654,10 +3665,10 @@ function runHeadlessStreaming(
|
|||||||
?.config ??
|
?.config ??
|
||||||
null
|
null
|
||||||
if (!config) {
|
if (!config) {
|
||||||
sendControlResponseError(message, `Server not found: ${serverName}`)
|
sendControlResponseError(msg, `Server not found: ${serverName}`)
|
||||||
} else if (config.type !== 'sse' && config.type !== 'http') {
|
} else if (config.type !== 'sse' && config.type !== 'http') {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
`Cannot clear auth for server type "${config.type}"`,
|
`Cannot clear auth for server type "${config.type}"`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -3690,16 +3701,16 @@ function runHeadlessStreaming(
|
|||||||
: omit(prev.mcp.resources, serverName),
|
: omit(prev.mcp.resources, serverName),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
sendControlResponseSuccess(message, {})
|
sendControlResponseSuccess(msg, {})
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'apply_flag_settings') {
|
} else if (msg.request.subtype === 'apply_flag_settings') {
|
||||||
// Snapshot the current model before applying — we need to detect
|
// Snapshot the current model before applying — we need to detect
|
||||||
// model switches so we can inject breadcrumbs and notify listeners.
|
// model switches so we can inject breadcrumbs and notify listeners.
|
||||||
const prevModel = getMainLoopModel()
|
const prevModel = getMainLoopModel()
|
||||||
|
|
||||||
// Merge the provided settings into the in-memory flag settings
|
// Merge the provided settings into the in-memory flag settings
|
||||||
const existing = getFlagSettingsInline() ?? {}
|
const existing = getFlagSettingsInline() ?? {}
|
||||||
const incoming = message.request.settings
|
const incoming = msg.request.settings
|
||||||
// Shallow-merge top-level keys; getSettingsForSource handles
|
// Shallow-merge top-level keys; getSettingsForSource handles
|
||||||
// the deep merge with file-based flag settings via mergeWith.
|
// the deep merge with file-based flag settings via mergeWith.
|
||||||
// JSON serialization drops `undefined`, so callers use `null`
|
// JSON serialization drops `undefined`, so callers use `null`
|
||||||
@@ -3748,8 +3759,8 @@ function runHeadlessStreaming(
|
|||||||
injectModelSwitchBreadcrumbs(modelArg, newModel)
|
injectModelSwitchBreadcrumbs(modelArg, newModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'get_settings') {
|
} else if (msg.request.subtype === 'get_settings') {
|
||||||
const currentAppState = getAppState()
|
const currentAppState = getAppState()
|
||||||
const model = getMainLoopModel()
|
const model = getMainLoopModel()
|
||||||
// modelSupportsEffort gate matches claude.ts — applied.effort must
|
// modelSupportsEffort gate matches claude.ts — applied.effort must
|
||||||
@@ -3757,7 +3768,7 @@ function runHeadlessStreaming(
|
|||||||
const effort = modelSupportsEffort(model)
|
const effort = modelSupportsEffort(model)
|
||||||
? resolveAppliedEffort(model, currentAppState.effortValue)
|
? resolveAppliedEffort(model, currentAppState.effortValue)
|
||||||
: undefined
|
: undefined
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
...getSettingsWithSources(),
|
...getSettingsWithSources(),
|
||||||
applied: {
|
applied: {
|
||||||
model,
|
model,
|
||||||
@@ -3765,22 +3776,22 @@ function runHeadlessStreaming(
|
|||||||
effort: typeof effort === 'string' ? effort : null,
|
effort: typeof effort === 'string' ? effort : null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (message.request.subtype === 'stop_task') {
|
} else if (msg.request.subtype === 'stop_task') {
|
||||||
const { task_id: taskId } = message.request
|
const { task_id: taskId } = msg.request
|
||||||
try {
|
try {
|
||||||
await stopTask(taskId, {
|
await stopTask(taskId, {
|
||||||
getAppState,
|
getAppState,
|
||||||
setAppState,
|
setAppState,
|
||||||
})
|
})
|
||||||
sendControlResponseSuccess(message, {})
|
sendControlResponseSuccess(msg, {})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendControlResponseError(message, errorMessage(error))
|
sendControlResponseError(msg, errorMessage(error))
|
||||||
}
|
}
|
||||||
} else if (message.request.subtype === 'generate_session_title') {
|
} else if (req.subtype === 'generate_session_title') {
|
||||||
// Fire-and-forget so the Haiku call does not block the stdin loop
|
// Fire-and-forget so the Haiku call does not block the stdin loop
|
||||||
// (which would delay processing of subsequent user messages /
|
// (which would delay processing of subsequent user messages /
|
||||||
// interrupts for the duration of the API roundtrip).
|
// interrupts for the duration of the API roundtrip).
|
||||||
const { description, persist } = message.request
|
const { description, persist } = req
|
||||||
// Reuse the live controller only if it has not already been aborted
|
// Reuse the live controller only if it has not already been aborted
|
||||||
// (e.g. by interrupt()); an aborted signal would cause queryHaiku to
|
// (e.g. by interrupt()); an aborted signal would cause queryHaiku to
|
||||||
// immediately throw APIUserAbortError → {title: null}.
|
// immediately throw APIUserAbortError → {title: null}.
|
||||||
@@ -3799,16 +3810,16 @@ function runHeadlessStreaming(
|
|||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message, { title })
|
sendControlResponseSuccess(msg, { title })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Unreachable in practice — generateSessionTitle wraps its
|
// Unreachable in practice — generateSessionTitle wraps its
|
||||||
// own body and returns null, saveAiGeneratedTitle is wrapped
|
// own body and returns null, saveAiGeneratedTitle is wrapped
|
||||||
// above. Propagate (not swallow) so unexpected failures are
|
// above. Propagate (not swallow) so unexpected failures are
|
||||||
// visible to the SDK caller (hostComms.ts catches and logs).
|
// visible to the SDK caller (hostComms.ts catches and logs).
|
||||||
sendControlResponseError(message, errorMessage(e))
|
sendControlResponseError(msg, errorMessage(e))
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
} else if (message.request.subtype === 'side_question') {
|
} else if (req.subtype === 'side_question') {
|
||||||
// Same fire-and-forget pattern as generate_session_title above —
|
// Same fire-and-forget pattern as generate_session_title above —
|
||||||
// the forked agent's API roundtrip must not block the stdin loop.
|
// the forked agent's API roundtrip must not block the stdin loop.
|
||||||
//
|
//
|
||||||
@@ -3824,7 +3835,7 @@ function runHeadlessStreaming(
|
|||||||
// matches in the common case. May still miss the cache for
|
// matches in the common case. May still miss the cache for
|
||||||
// coordinator mode or memory-mechanics extras — acceptable, the
|
// coordinator mode or memory-mechanics extras — acceptable, the
|
||||||
// alternative is the side question failing entirely.
|
// alternative is the side question failing entirely.
|
||||||
const { question } = message.request
|
const { question } = req
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const saved = getLastCacheSafeParams()
|
const saved = getLastCacheSafeParams()
|
||||||
@@ -3863,16 +3874,16 @@ function runHeadlessStreaming(
|
|||||||
question,
|
question,
|
||||||
cacheSafeParams,
|
cacheSafeParams,
|
||||||
})
|
})
|
||||||
sendControlResponseSuccess(message, { response: result.response })
|
sendControlResponseSuccess(msg, { response: result.response })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendControlResponseError(message, errorMessage(e))
|
sendControlResponseError(msg, errorMessage(e))
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
} else if (
|
} else if (
|
||||||
(feature('PROACTIVE') || feature('KAIROS')) &&
|
(feature('PROACTIVE') || feature('KAIROS')) &&
|
||||||
(message.request as { subtype: string }).subtype === 'set_proactive'
|
(msg.request as { subtype: string }).subtype === 'set_proactive'
|
||||||
) {
|
) {
|
||||||
const req = message.request as unknown as {
|
const req = msg.request as unknown as {
|
||||||
subtype: string
|
subtype: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
@@ -3884,12 +3895,12 @@ function runHeadlessStreaming(
|
|||||||
} else {
|
} else {
|
||||||
proactiveModule!.deactivateProactive()
|
proactiveModule!.deactivateProactive()
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
} else if (message.request.subtype === 'remote_control') {
|
} else if (req.subtype === 'remote_control') {
|
||||||
if (message.request.enabled) {
|
if (req.enabled as boolean) {
|
||||||
if (bridgeHandle) {
|
if (bridgeHandle) {
|
||||||
// Already connected
|
// Already connected
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
session_url: getRemoteSessionUrl(
|
session_url: getRemoteSessionUrl(
|
||||||
bridgeHandle.bridgeSessionId,
|
bridgeHandle.bridgeSessionId,
|
||||||
bridgeHandle.sessionIngressUrl,
|
bridgeHandle.sessionIngressUrl,
|
||||||
@@ -3972,7 +3983,7 @@ function runHeadlessStreaming(
|
|||||||
})
|
})
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
bridgeFailureDetail ??
|
bridgeFailureDetail ??
|
||||||
'Remote Control initialization failed',
|
'Remote Control initialization failed',
|
||||||
)
|
)
|
||||||
@@ -3988,7 +3999,7 @@ function runHeadlessStreaming(
|
|||||||
structuredIO.setOnControlRequestResolved(requestId => {
|
structuredIO.setOnControlRequestResolved(requestId => {
|
||||||
handle.sendControlCancelRequest(requestId)
|
handle.sendControlCancelRequest(requestId)
|
||||||
})
|
})
|
||||||
sendControlResponseSuccess(message, {
|
sendControlResponseSuccess(msg, {
|
||||||
session_url: getRemoteSessionUrl(
|
session_url: getRemoteSessionUrl(
|
||||||
handle.bridgeSessionId,
|
handle.bridgeSessionId,
|
||||||
handle.sessionIngressUrl,
|
handle.sessionIngressUrl,
|
||||||
@@ -4001,7 +4012,7 @@ function runHeadlessStreaming(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendControlResponseError(message, errorMessage(err))
|
sendControlResponseError(msg, errorMessage(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -4012,21 +4023,21 @@ function runHeadlessStreaming(
|
|||||||
await bridgeHandle.teardown()
|
await bridgeHandle.teardown()
|
||||||
bridgeHandle = null
|
bridgeHandle = null
|
||||||
}
|
}
|
||||||
sendControlResponseSuccess(message)
|
sendControlResponseSuccess(msg)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown control request subtype — send an error response so
|
// Unknown control request subtype — send an error response so
|
||||||
// the caller doesn't hang waiting for a reply that never comes.
|
// the caller doesn't hang waiting for a reply that never comes.
|
||||||
sendControlResponseError(
|
sendControlResponseError(
|
||||||
message,
|
msg,
|
||||||
`Unsupported control request subtype: ${(message.request as { subtype: string }).subtype}`,
|
`Unsupported control request subtype: ${(msg.request as { subtype: string }).subtype}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
} else if (message.type === 'control_response') {
|
} else if (message.type === 'control_response') {
|
||||||
// Replay control_response messages when replay mode is enabled
|
// Replay control_response messages when replay mode is enabled
|
||||||
if (options.replayUserMessages) {
|
if (options.replayUserMessages) {
|
||||||
output.enqueue(message)
|
output.enqueue(message as StdoutMessage)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
} else if (message.type === 'keep_alive') {
|
} else if (message.type === 'keep_alive') {
|
||||||
@@ -4038,11 +4049,11 @@ function runHeadlessStreaming(
|
|||||||
} else if (message.type === 'assistant' || message.type === 'system') {
|
} else if (message.type === 'assistant' || message.type === 'system') {
|
||||||
// History replay from bridge: inject into mutableMessages as
|
// History replay from bridge: inject into mutableMessages as
|
||||||
// conversation context so the model sees prior turns.
|
// conversation context so the model sees prior turns.
|
||||||
const internalMsgs = toInternalMessages([message])
|
const internalMsgs = toInternalMessages([message as SDKMessage])
|
||||||
mutableMessages.push(...internalMsgs)
|
mutableMessages.push(...internalMsgs)
|
||||||
// Echo assistant messages back so CCR displays them
|
// Echo assistant messages back so CCR displays them
|
||||||
if (message.type === 'assistant' && options.replayUserMessages) {
|
if (message.type === 'assistant' && options.replayUserMessages) {
|
||||||
output.enqueue(message)
|
output.enqueue(message as StdoutMessage)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -4051,58 +4062,61 @@ function runHeadlessStreaming(
|
|||||||
if (message.type !== 'user') {
|
if (message.type !== 'user') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Type assertion: after the type guard, message is a user message.
|
||||||
|
// The union with SDKMessage (any) prevents proper narrowing.
|
||||||
|
const userMsg = message as SDKUserMessage
|
||||||
|
|
||||||
// First prompt message implicitly initializes if not already done.
|
// First prompt message implicitly initializes if not already done.
|
||||||
initialized = true
|
initialized = true
|
||||||
|
|
||||||
// Check for duplicate user message - skip if already processed
|
// Check for duplicate user message - skip if already processed
|
||||||
if (message.uuid) {
|
if (userMsg.uuid) {
|
||||||
const sessionId = getSessionId() as UUID
|
const sessionId = getSessionId() as UUID
|
||||||
const existsInSession = await doesMessageExistInSession(
|
const existsInSession = await doesMessageExistInSession(
|
||||||
sessionId,
|
sessionId,
|
||||||
message.uuid,
|
userMsg.uuid as UUID,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check both historical duplicates (from file) and runtime duplicates (this session)
|
// Check both historical duplicates (from file) and runtime duplicates (this session)
|
||||||
if (existsInSession || receivedMessageUuids.has(message.uuid)) {
|
if (existsInSession || receivedMessageUuids.has(userMsg.uuid as UUID)) {
|
||||||
logForDebugging(`Skipping duplicate user message: ${message.uuid}`)
|
logForDebugging(`Skipping duplicate user message: ${userMsg.uuid}`)
|
||||||
// Send acknowledgment for duplicate message if replay mode is enabled
|
// Send acknowledgment for duplicate message if replay mode is enabled
|
||||||
if (options.replayUserMessages) {
|
if (options.replayUserMessages) {
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`Sending acknowledgment for duplicate user message: ${message.uuid}`,
|
`Sending acknowledgment for duplicate user message: ${userMsg.uuid}`,
|
||||||
)
|
)
|
||||||
output.enqueue({
|
output.enqueue({
|
||||||
type: 'user',
|
type: 'user',
|
||||||
content: message.message?.content ?? '',
|
content: (userMsg.message as { content?: string })?.content ?? '',
|
||||||
message: message.message,
|
message: userMsg.message as { role: string; content: unknown },
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
uuid: message.uuid,
|
uuid: userMsg.uuid as string,
|
||||||
timestamp: message.timestamp,
|
timestamp: (userMsg as { timestamp?: string }).timestamp,
|
||||||
isReplay: true,
|
isReplay: true,
|
||||||
} as unknown as SDKUserMessageReplay)
|
} as unknown as SDKUserMessageReplay as StdoutMessage)
|
||||||
}
|
}
|
||||||
// Historical dup = transcript already has this turn's output, so it
|
// Historical dup = transcript already has this turn's output, so it
|
||||||
// ran but its lifecycle was never closed (interrupted before ack).
|
// ran but its lifecycle was never closed (interrupted before ack).
|
||||||
// Runtime dups don't need this — the original enqueue path closes them.
|
// Runtime dups don't need this — the original enqueue path closes them.
|
||||||
if (existsInSession) {
|
if (existsInSession) {
|
||||||
notifyCommandLifecycle(message.uuid, 'completed')
|
notifyCommandLifecycle(userMsg.uuid as string, 'completed')
|
||||||
}
|
}
|
||||||
// Don't enqueue duplicate messages for execution
|
// Don't enqueue duplicate messages for execution
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track this UUID to prevent runtime duplicates
|
// Track this UUID to prevent runtime duplicates
|
||||||
trackReceivedMessageUuid(message.uuid)
|
trackReceivedMessageUuid(userMsg.uuid as UUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue({
|
enqueue({
|
||||||
mode: 'prompt' as const,
|
mode: 'prompt' as const,
|
||||||
// file_attachments rides the protobuf catchall from the web composer.
|
// file_attachments rides the protobuf catchall from the web composer.
|
||||||
// Same-ref no-op when absent (no 'file_attachments' key).
|
// Same-ref no-op when absent (no 'file_attachments' key).
|
||||||
value: await resolveAndPrepend(message, message.message.content),
|
value: await resolveAndPrepend(userMsg, (userMsg.message as { content: ContentBlockParam[] }).content),
|
||||||
uuid: message.uuid,
|
uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`,
|
||||||
priority: message.priority,
|
priority: (userMsg as { priority?: string }).priority as import('src/types/textInputTypes.js').QueuePriority,
|
||||||
})
|
})
|
||||||
// Increment prompt count for attribution tracking and save snapshot
|
// Increment prompt count for attribution tracking and save snapshot
|
||||||
// The snapshot persists promptCount so it survives compaction
|
// The snapshot persists promptCount so it survives compaction
|
||||||
@@ -4463,7 +4477,7 @@ async function handleInitializeRequest(
|
|||||||
})),
|
})),
|
||||||
output_style: outputStyle,
|
output_style: outputStyle,
|
||||||
available_output_styles: Object.keys(availableOutputStyles),
|
available_output_styles: Object.keys(availableOutputStyles),
|
||||||
models: modelInfos,
|
models: modelInfos as unknown as SDKControlInitializeResponse['models'],
|
||||||
account: {
|
account: {
|
||||||
email: accountInfo?.email,
|
email: accountInfo?.email,
|
||||||
organization: accountInfo?.organization,
|
organization: accountInfo?.organization,
|
||||||
@@ -4473,7 +4487,7 @@ async function handleInitializeRequest(
|
|||||||
// getAccountInformation() returns undefined under 3P providers, so the
|
// getAccountInformation() returns undefined under 3P providers, so the
|
||||||
// other fields are all absent. apiProvider disambiguates "not logged
|
// other fields are all absent. apiProvider disambiguates "not logged
|
||||||
// in" (firstParty + tokenSource:none) from "3P, login not applicable".
|
// in" (firstParty + tokenSource:none) from "3P, login not applicable".
|
||||||
apiProvider: getAPIProvider(),
|
apiProvider: getAPIProvider() as 'firstParty' | 'bedrock' | 'vertex' | 'foundry',
|
||||||
},
|
},
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export class StructuredIO {
|
|||||||
// Used by bridge session runner for auth token refresh
|
// Used by bridge session runner for auth token refresh
|
||||||
// (CLAUDE_CODE_SESSION_ACCESS_TOKEN) which must be readable
|
// (CLAUDE_CODE_SESSION_ACCESS_TOKEN) which must be readable
|
||||||
// by the REPL process itself, not just child Bash commands.
|
// by the REPL process itself, not just child Bash commands.
|
||||||
const variables = message.variables as Record<string, string>
|
const variables = message.variables ?? {}
|
||||||
const keys = Object.keys(variables)
|
const keys = Object.keys(variables)
|
||||||
for (const [key, value] of Object.entries(variables)) {
|
for (const [key, value] of Object.entries(variables)) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
@@ -377,7 +377,8 @@ export class StructuredIO {
|
|||||||
if (uuid) {
|
if (uuid) {
|
||||||
notifyCommandLifecycle(uuid, 'completed')
|
notifyCommandLifecycle(uuid, 'completed')
|
||||||
}
|
}
|
||||||
const request = this.pendingRequests.get(message.response.request_id)
|
const resp = message.response as { request_id: string; subtype: string; response?: Record<string, unknown>; error?: string }
|
||||||
|
const request = this.pendingRequests.get(resp.request_id)
|
||||||
if (!request) {
|
if (!request) {
|
||||||
// Check if this tool_use was already resolved through the normal
|
// Check if this tool_use was already resolved through the normal
|
||||||
// permission flow. Duplicate control_response deliveries (e.g. from
|
// permission flow. Duplicate control_response deliveries (e.g. from
|
||||||
@@ -385,8 +386,8 @@ export class StructuredIO {
|
|||||||
// re-processing them would push duplicate assistant messages into
|
// re-processing them would push duplicate assistant messages into
|
||||||
// the conversation, causing API 400 errors.
|
// the conversation, causing API 400 errors.
|
||||||
const responsePayload =
|
const responsePayload =
|
||||||
message.response.subtype === 'success'
|
resp.subtype === 'success'
|
||||||
? message.response.response
|
? resp.response
|
||||||
: undefined
|
: undefined
|
||||||
const toolUseID = responsePayload?.toolUseID
|
const toolUseID = responsePayload?.toolUseID
|
||||||
if (
|
if (
|
||||||
@@ -394,31 +395,31 @@ export class StructuredIO {
|
|||||||
this.resolvedToolUseIds.has(toolUseID)
|
this.resolvedToolUseIds.has(toolUseID)
|
||||||
) {
|
) {
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`Ignoring duplicate control_response for already-resolved toolUseID=${toolUseID} request_id=${message.response.request_id}`,
|
`Ignoring duplicate control_response for already-resolved toolUseID=${toolUseID} request_id=${resp.request_id}`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if (this.unexpectedResponseCallback) {
|
if (this.unexpectedResponseCallback) {
|
||||||
await this.unexpectedResponseCallback(message)
|
await this.unexpectedResponseCallback(message as SDKControlResponse & { uuid?: string })
|
||||||
}
|
}
|
||||||
return undefined // Ignore responses for requests we don't know about
|
return undefined // Ignore responses for requests we don't know about
|
||||||
}
|
}
|
||||||
this.trackResolvedToolUseId(request.request)
|
this.trackResolvedToolUseId(request.request)
|
||||||
this.pendingRequests.delete(message.response.request_id)
|
this.pendingRequests.delete(resp.request_id)
|
||||||
// Notify the bridge when the SDK consumer resolves a can_use_tool
|
// Notify the bridge when the SDK consumer resolves a can_use_tool
|
||||||
// request, so it can cancel the stale permission prompt on claude.ai.
|
// request, so it can cancel the stale permission prompt on claude.ai.
|
||||||
if (
|
if (
|
||||||
(request.request.request as { subtype?: string }).subtype === 'can_use_tool' &&
|
(request.request.request as { subtype?: string }).subtype === 'can_use_tool' &&
|
||||||
this.onControlRequestResolved
|
this.onControlRequestResolved
|
||||||
) {
|
) {
|
||||||
this.onControlRequestResolved(message.response.request_id)
|
this.onControlRequestResolved(resp.request_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.response.subtype === 'error') {
|
if (resp.subtype === 'error') {
|
||||||
request.reject(new Error(message.response.error))
|
request.reject(new Error(resp.error ?? 'Unknown error'))
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const result = message.response.response
|
const result = resp.response
|
||||||
if (request.schema) {
|
if (request.schema) {
|
||||||
try {
|
try {
|
||||||
request.resolve(request.schema.parse(result))
|
request.resolve(request.schema.parse(result))
|
||||||
@@ -454,9 +455,9 @@ export class StructuredIO {
|
|||||||
if (message.type === 'assistant' || message.type === 'system') {
|
if (message.type === 'assistant' || message.type === 'system') {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
if (message.message.role !== 'user') {
|
if ((message as { message?: { role?: string } }).message?.role !== 'user') {
|
||||||
exitWithMessage(
|
exitWithMessage(
|
||||||
`Error: Expected message role 'user', got '${message.message.role}'`,
|
`Error: Expected message role 'user', got '${(message as { message?: { role?: string } }).message?.role}'`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
@@ -678,7 +679,7 @@ export class StructuredIO {
|
|||||||
{
|
{
|
||||||
subtype: 'hook_callback',
|
subtype: 'hook_callback',
|
||||||
callback_id: callbackId,
|
callback_id: callbackId,
|
||||||
input,
|
input: input as Parameters<HookCallback['callback']>[0],
|
||||||
tool_use_id: toolUseID || undefined,
|
tool_use_id: toolUseID || undefined,
|
||||||
},
|
},
|
||||||
hookJSONOutputSchema(),
|
hookJSONOutputSchema(),
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function ServerManagementDialog({ onDone }: Props): React.ReactNode {
|
|||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text>
|
<Text>
|
||||||
Remote Control Server is{' '}
|
Remote Control Server is{' '}
|
||||||
<Text bold color="green">
|
<Text bold color="success">
|
||||||
running
|
running
|
||||||
</Text>
|
</Text>
|
||||||
{daemonProcess ? ` (PID: ${daemonProcess.pid})` : ''}
|
{daemonProcess ? ` (PID: ${daemonProcess.pid})` : ''}
|
||||||
@@ -233,10 +233,10 @@ function startDaemon(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
|
||||||
daemonProcess = null;
|
daemonProcess = null;
|
||||||
daemonStatus = 'stopped';
|
daemonStatus = 'stopped';
|
||||||
daemonLogs.push(`[daemon] exited (code=${code}, signal=${signal})`);
|
daemonLogs.push(`[daemon] exited (code=${code ?? 'unknown'}, signal=${signal})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (err: Error) => {
|
child.on('error', (err: Error) => {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function BuiltinStatusLineInner({
|
|||||||
// Force re-render every 60s so countdowns stay current
|
// Force re-render every 60s so countdowns stay current
|
||||||
const [tick, setTick] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasResetTime = rateLimits.five_hour?.resets_at || rateLimits.seven_day?.resets_at;
|
const hasResetTime = (rateLimits.five_hour?.resets_at ?? 0) || (rateLimits.seven_day?.resets_at ?? 0);
|
||||||
if (!hasResetTime) return;
|
if (!hasResetTime) return;
|
||||||
const id = setInterval(() => setTick(t => t + 1), 60_000);
|
const id = setInterval(() => setTick(t => t + 1), 60_000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ type Props = {
|
|||||||
export function CompactSummary({ message, screen }: Props): React.ReactNode {
|
export function CompactSummary({ message, screen }: Props): React.ReactNode {
|
||||||
const isTranscriptMode = screen === 'transcript'
|
const isTranscriptMode = screen === 'transcript'
|
||||||
const textContent = getUserMessageText(message) || ''
|
const textContent = getUserMessageText(message) || ''
|
||||||
const metadata = message.summarizeMetadata
|
const metadata = message.summarizeMetadata as {
|
||||||
|
messagesSummarized?: number
|
||||||
|
direction?: string
|
||||||
|
userContext?: string
|
||||||
|
} | undefined
|
||||||
|
|
||||||
// "Summarize from here" with metadata
|
// "Summarize from here" with metadata
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ const SuggestionItemRow = memo(function SuggestionItemRow({
|
|||||||
{paddedDisplayText}
|
{paddedDisplayText}
|
||||||
</Text>
|
</Text>
|
||||||
{tagText ? (
|
{tagText ? (
|
||||||
<Text color={item.tag === 'local' ? 'yellow' : undefined} dimColor={item.tag !== 'local'}>
|
<Text color={item.tag === 'local' ? ('yellow' as const) : undefined} dimColor={item.tag !== 'local'}>
|
||||||
{tagText}
|
{tagText}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -162,13 +162,13 @@ export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
|
|||||||
skill.source === 'plugin'
|
skill.source === 'plugin'
|
||||||
? skill.pluginInfo?.pluginManifest.name
|
? skill.pluginInfo?.pluginManifest.name
|
||||||
: undefined
|
: undefined
|
||||||
const scopeTag = getScopeTag(skill.source as SkillSource)
|
const scopeTag = getScopeTag(skill.source)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`${skill.name}-${skill.source}`}>
|
<Box key={`${skill.name}-${skill.source}`}>
|
||||||
<Text>{getCommandName(skill)}</Text>
|
<Text>{getCommandName(skill)}</Text>
|
||||||
{scopeTag && (
|
{scopeTag && (
|
||||||
<Text color={scopeTag.color}> [{scopeTag.label}]</Text>
|
<Text color={scopeTag.color as keyof Theme}> [{scopeTag.label}]</Text>
|
||||||
)}
|
)}
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
|
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ function UltraplanSessionDetail({
|
|||||||
let lastBlock: { name: string; input: unknown } | null = null
|
let lastBlock: { name: string; input: unknown } | null = null
|
||||||
for (const msg of session.log) {
|
for (const msg of session.log) {
|
||||||
if (msg.type !== 'assistant') continue
|
if (msg.type !== 'assistant') continue
|
||||||
for (const block of msg.message.content) {
|
const content = msg.message?.content ?? []
|
||||||
|
for (const block of content as Array<{type: string; name: string; input: unknown}>) {
|
||||||
if (block.type !== 'tool_use') continue
|
if (block.type !== 'tool_use') continue
|
||||||
calls++
|
calls++
|
||||||
lastBlock = block
|
lastBlock = block
|
||||||
|
|||||||
@@ -225,13 +225,16 @@ export function useReplBridge(
|
|||||||
const { resolveAndPrepend } = await import(
|
const { resolveAndPrepend } = await import(
|
||||||
'../bridge/inboundAttachments.js'
|
'../bridge/inboundAttachments.js'
|
||||||
)
|
)
|
||||||
let sanitized = fields.content
|
const rawContent = fields.content
|
||||||
|
let sanitized: string | Array<{ type: string; [key: string]: unknown }> = typeof rawContent === 'string' ? rawContent : rawContent as Array<{ type: string; [key: string]: unknown }>
|
||||||
if (feature('KAIROS_GITHUB_WEBHOOKS')) {
|
if (feature('KAIROS_GITHUB_WEBHOOKS')) {
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const { sanitizeInboundWebhookContent } =
|
const { sanitizeInboundWebhookContent } =
|
||||||
require('../bridge/webhookSanitizer.js') as typeof import('../bridge/webhookSanitizer.js')
|
require('../bridge/webhookSanitizer.js') as typeof import('../bridge/webhookSanitizer.js')
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
sanitized = sanitizeInboundWebhookContent(fields.content)
|
if (typeof sanitized === 'string') {
|
||||||
|
sanitized = sanitizeInboundWebhookContent(sanitized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const content = await resolveAndPrepend(msg, sanitized)
|
const content = await resolveAndPrepend(msg, sanitized)
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function* queryModelGemini(
|
|||||||
|
|
||||||
const standardTools = toolSchemas.filter(
|
const standardTools = toolSchemas.filter(
|
||||||
(t): t is BetaToolUnion & { type: string } => {
|
(t): t is BetaToolUnion & { type: string } => {
|
||||||
const anyTool = t as Record<string, unknown>
|
const anyTool = t as unknown as Record<string, unknown>
|
||||||
return (
|
return (
|
||||||
anyTool.type !== 'advisor_20260301' &&
|
anyTool.type !== 'advisor_20260301' &&
|
||||||
anyTool.type !== 'computer_20250124'
|
anyTool.type !== 'computer_20250124'
|
||||||
@@ -186,7 +186,7 @@ export async function* queryModelGemini(
|
|||||||
yield createAssistantAPIErrorMessage({
|
yield createAssistantAPIErrorMessage({
|
||||||
content: `API Error: ${errorMessage}`,
|
content: `API Error: ${errorMessage}`,
|
||||||
apiError: 'api_error',
|
apiError: 'api_error',
|
||||||
error: error instanceof Error ? error : new Error(String(error)),
|
error: (error instanceof Error ? error : new Error(String(error))) as Error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/me
|
|||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||||
import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js'
|
import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js'
|
||||||
import type { Tools } from '../../../Tool.js'
|
import type { Tools } from '../../../Tool.js'
|
||||||
|
import type {
|
||||||
|
ChatCompletionChunk,
|
||||||
|
ChatCompletionCreateParamsStreaming,
|
||||||
|
} from 'openai/resources/chat/completions/completions.mjs'
|
||||||
import { getGrokClient } from './client.js'
|
import { getGrokClient } from './client.js'
|
||||||
import { anthropicMessagesToOpenAI } from '../openai/convertMessages.js'
|
import { anthropicMessagesToOpenAI } from '../openai/convertMessages.js'
|
||||||
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openai/convertTools.js'
|
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openai/convertTools.js'
|
||||||
@@ -51,7 +55,7 @@ export async function* queryModelGrok(
|
|||||||
)
|
)
|
||||||
const standardTools = toolSchemas.filter(
|
const standardTools = toolSchemas.filter(
|
||||||
(t): t is BetaToolUnion & { type: string } => {
|
(t): t is BetaToolUnion & { type: string } => {
|
||||||
const anyT = t as Record<string, unknown>
|
const anyT = t as unknown as Record<string, unknown>
|
||||||
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -62,7 +66,7 @@ export async function* queryModelGrok(
|
|||||||
|
|
||||||
const client = getGrokClient({
|
const client = getGrokClient({
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
fetchOverride: options.fetchOverride,
|
fetchOverride: options.fetchOverride as typeof fetch | undefined,
|
||||||
source: options.querySource,
|
source: options.querySource,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,13 +85,13 @@ export async function* queryModelGrok(
|
|||||||
...(options.temperatureOverride !== undefined && {
|
...(options.temperatureOverride !== undefined && {
|
||||||
temperature: options.temperatureOverride,
|
temperature: options.temperatureOverride,
|
||||||
}),
|
}),
|
||||||
},
|
} as ChatCompletionCreateParamsStreaming,
|
||||||
{
|
{
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, grokModel)
|
const adaptedStream = adaptOpenAIStreamToAnthropic(stream as AsyncIterable<ChatCompletionChunk>, grokModel)
|
||||||
|
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, any> = {}
|
||||||
let partialMessage: any = undefined
|
let partialMessage: any = undefined
|
||||||
@@ -186,7 +190,7 @@ export async function* queryModelGrok(
|
|||||||
yield createAssistantAPIErrorMessage({
|
yield createAssistantAPIErrorMessage({
|
||||||
content: `API Error: ${errorMessage}`,
|
content: `API Error: ${errorMessage}`,
|
||||||
apiError: 'api_error',
|
apiError: 'api_error',
|
||||||
error: error instanceof Error ? error : new Error(String(error)),
|
error: (error instanceof Error ? error : new Error(String(error))) as Error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
import { getProxyFetchOptions } from 'src/utils/proxy.js'
|
import { getProxyFetchOptions } from 'src/utils/proxy.js'
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variables:
|
* Environment variables:
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import type {
|
|||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
} from '../../../types/message.js'
|
} from '../../../types/message.js'
|
||||||
import type { Tools } from '../../../Tool.js'
|
import type { Tools } from '../../../Tool.js'
|
||||||
|
import type { Stream } from 'openai/streaming.mjs'
|
||||||
|
import type {
|
||||||
|
ChatCompletionChunk,
|
||||||
|
ChatCompletionCreateParamsStreaming,
|
||||||
|
} from 'openai/resources/chat/completions/completions.mjs'
|
||||||
import { getOpenAIClient } from './client.js'
|
import { getOpenAIClient } from './client.js'
|
||||||
import { anthropicMessagesToOpenAI } from './convertMessages.js'
|
import { anthropicMessagesToOpenAI } from './convertMessages.js'
|
||||||
import {
|
import {
|
||||||
@@ -82,7 +87,7 @@ export function buildOpenAIRequestBody(params: {
|
|||||||
toolChoice: any
|
toolChoice: any
|
||||||
enableThinking: boolean
|
enableThinking: boolean
|
||||||
temperatureOverride?: number
|
temperatureOverride?: number
|
||||||
}): Record<string, any> {
|
}): ChatCompletionCreateParamsStreaming {
|
||||||
const { model, messages, tools, toolChoice, enableThinking, temperatureOverride } = params
|
const { model, messages, tools, toolChoice, enableThinking, temperatureOverride } = params
|
||||||
return {
|
return {
|
||||||
model,
|
model,
|
||||||
@@ -183,7 +188,7 @@ export async function* queryModelOpenAI(
|
|||||||
// 7. Filter out non-standard tools (server tools like advisor)
|
// 7. Filter out non-standard tools (server tools like advisor)
|
||||||
const standardTools = toolSchemas.filter(
|
const standardTools = toolSchemas.filter(
|
||||||
(t): t is BetaToolUnion & { type: string } => {
|
(t): t is BetaToolUnion & { type: string } => {
|
||||||
const anyT = t as Record<string, unknown>
|
const anyT = t as unknown as Record<string, unknown>
|
||||||
return (
|
return (
|
||||||
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
||||||
)
|
)
|
||||||
@@ -349,7 +354,7 @@ export async function* queryModelOpenAI(
|
|||||||
yield createAssistantAPIErrorMessage({
|
yield createAssistantAPIErrorMessage({
|
||||||
content: `API Error: ${errorMessage}`,
|
content: `API Error: ${errorMessage}`,
|
||||||
apiError: 'api_error',
|
apiError: 'api_error',
|
||||||
error: error instanceof Error ? error : new Error(String(error)),
|
error: (error instanceof Error ? error : new Error(String(error))) as Error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ function processProgressMessages(
|
|||||||
isAgentRunning: boolean,
|
isAgentRunning: boolean,
|
||||||
): ProcessedMessage[] {
|
): ProcessedMessage[] {
|
||||||
// Only process for ants
|
// Only process for ants
|
||||||
if ("external" !== 'ant') {
|
if (process.env.USER_TYPE !== 'ant') {
|
||||||
return messages
|
return messages
|
||||||
.filter(
|
.filter(
|
||||||
(m): m is ProgressMessage<AgentToolProgress> =>
|
(m): m is ProgressMessage<AgentToolProgress> =>
|
||||||
@@ -411,7 +411,7 @@ export function renderToolResultMessage(
|
|||||||
|
|
||||||
const finalAssistantMessage = createAssistantMessage({
|
const finalAssistantMessage = createAssistantMessage({
|
||||||
content: completionMessage,
|
content: completionMessage,
|
||||||
usage: { ...usage, inference_geo: null, iterations: null, speed: null },
|
usage: { ...usage, inference_geo: null, iterations: null, speed: null } as typeof usage,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -866,7 +866,7 @@ export function renderGroupedAgentToolUse(
|
|||||||
taskDescription = parsedInput.data.description
|
taskDescription = parsedInput.data.description
|
||||||
// Use the custom agent definition's color on the type, not the name
|
// Use the custom agent definition's color on the type, not the name
|
||||||
descriptionColor = isCustomSubagentType(subagentType)
|
descriptionColor = isCustomSubagentType(subagentType)
|
||||||
? (getAgentColor(subagentType) as keyof Theme | undefined)
|
? getAgentColor(subagentType)
|
||||||
: undefined
|
: undefined
|
||||||
} else {
|
} else {
|
||||||
agentType = parsedInput.success
|
agentType = parsedInput.success
|
||||||
@@ -1019,7 +1019,7 @@ export function userFacingNameBackgroundColor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the color for this agent
|
// Get the color for this agent
|
||||||
return getAgentColor(input.subagent_type) as keyof Theme | undefined
|
return getAgentColor(input.subagent_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractLastToolInfo(
|
export function extractLastToolInfo(
|
||||||
|
|||||||
@@ -423,18 +423,21 @@ export function createCliExecutor(opts: {
|
|||||||
targetW,
|
targetW,
|
||||||
targetH,
|
targetH,
|
||||||
opts.preferredDisplayId,
|
opts.preferredDisplayId,
|
||||||
opts.autoResolve,
|
|
||||||
opts.doHide,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Ensure the result has fields expected by toolCalls.ts (hidden, displayId).
|
// Ensure the result has fields expected by toolCalls.ts (hidden, displayId).
|
||||||
// macOS native returns these from Swift; our cross-platform ComputerUseAPI
|
// macOS native returns these from Swift; our cross-platform ComputerUseAPI
|
||||||
// returns {base64, width, height} — fill in the missing fields.
|
// returns {base64, width, height} — fill in the missing fields.
|
||||||
|
const baseResult = raw as Partial<ResolvePrepareCaptureResult> & { width?: number; height?: number }
|
||||||
return {
|
return {
|
||||||
...raw,
|
...raw,
|
||||||
hidden: (raw as any).hidden ?? [],
|
displayWidth: baseResult.displayWidth ?? baseResult.width,
|
||||||
displayId: (raw as any).displayId ?? opts.preferredDisplayId ?? d.displayId,
|
displayHeight: baseResult.displayHeight ?? baseResult.height,
|
||||||
}
|
originX: baseResult.originX ?? 0,
|
||||||
|
originY: baseResult.originY ?? 0,
|
||||||
|
hidden: baseResult.hidden ?? [],
|
||||||
|
displayId: baseResult.displayId ?? opts.preferredDisplayId ?? d.displayId,
|
||||||
|
} as ResolvePrepareCaptureResult
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import type { Writable } from 'stream'
|
||||||
|
|
||||||
interface BridgeRequest {
|
interface BridgeRequest {
|
||||||
id: number
|
id: number
|
||||||
@@ -48,7 +49,7 @@ export function ensureBridge(): boolean {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Read stdout lines asynchronously
|
// Read stdout lines asynchronously
|
||||||
const reader = bridgeProc.stdout.getReader()
|
const reader = (bridgeProc.stdout as ReadableStream<Uint8Array>).getReader()
|
||||||
const readLoop = async () => {
|
const readLoop = async () => {
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -114,12 +115,12 @@ export async function call<T = unknown>(
|
|||||||
}, timeoutMs)
|
}, timeoutMs)
|
||||||
|
|
||||||
// Clear timeout on resolve/reject
|
// Clear timeout on resolve/reject
|
||||||
const origResolve = resolve
|
const origResolve = resolve as (v: unknown) => void
|
||||||
const origReject = reject
|
const origReject = reject
|
||||||
pendingRequests.set(id, {
|
pendingRequests.set(id, {
|
||||||
resolve: v => {
|
resolve: v => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
;(origResolve as any)(v)
|
origResolve(v)
|
||||||
},
|
},
|
||||||
reject: e => {
|
reject: e => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
@@ -128,8 +129,14 @@ export async function call<T = unknown>(
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bridgeProc!.stdin.write(JSON.stringify(req) + '\n')
|
const stdin = bridgeProc!.stdin
|
||||||
bridgeProc!.stdin.flush()
|
if (stdin) {
|
||||||
|
const writable = stdin as Writable
|
||||||
|
writable.write(JSON.stringify(req) + '\n')
|
||||||
|
if (typeof writable.flush === 'function') {
|
||||||
|
writable.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
pendingRequests.delete(id)
|
pendingRequests.delete(id)
|
||||||
@@ -176,7 +183,13 @@ export function callSync<T = unknown>(
|
|||||||
export function stopBridge(): void {
|
export function stopBridge(): void {
|
||||||
if (bridgeProc) {
|
if (bridgeProc) {
|
||||||
try {
|
try {
|
||||||
bridgeProc.stdin.end()
|
const stdin = bridgeProc.stdin
|
||||||
|
if (stdin) {
|
||||||
|
const writable = stdin as Writable
|
||||||
|
if (typeof writable.end === 'function') {
|
||||||
|
writable.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
bridgeProc.kill()
|
bridgeProc.kill()
|
||||||
} catch {}
|
} catch {}
|
||||||
bridgeProc = null
|
bridgeProc = null
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ export function createStreamlinedTransformer(): (
|
|||||||
): StdoutMessage | null {
|
): StdoutMessage | null {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
const content = message.message.content
|
const messageContent = (message as SDKAssistantMessage).message
|
||||||
|
const content = messageContent?.content
|
||||||
const text = Array.isArray(content)
|
const text = Array.isArray(content)
|
||||||
? extractTextContent(content, '\n').trim()
|
? extractTextContent(content, '\n').trim()
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// Accumulate tool counts from this message
|
// Accumulate tool counts from this message
|
||||||
accumulateToolUses(message, cumulativeCounts)
|
accumulateToolUses(message as SDKAssistantMessage, cumulativeCounts)
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
// Text message: emit text only, reset counts
|
// Text message: emit text only, reset counts
|
||||||
|
|||||||
Reference in New Issue
Block a user