mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -17,7 +17,8 @@ import {
|
||||
const _restores: (() => void)[] = []
|
||||
const originalCwd = process.cwd()
|
||||
const originalAcpPermissionMode = process.env.ACP_PERMISSION_MODE
|
||||
const originalAcpAllowBypass = process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS
|
||||
const originalAcpAllowBypass =
|
||||
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS
|
||||
|
||||
function mockModulePreservingExports(
|
||||
tsPath: string,
|
||||
@@ -35,10 +36,7 @@ afterAll(() => {
|
||||
}
|
||||
_restores.length = 0
|
||||
restoreEnv('ACP_PERMISSION_MODE', originalAcpPermissionMode)
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS',
|
||||
originalAcpAllowBypass,
|
||||
)
|
||||
restoreEnv('CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS', originalAcpAllowBypass)
|
||||
})
|
||||
|
||||
// ── Module mocks (must precede any import of the module under test) ──
|
||||
@@ -372,7 +370,9 @@ describe('AcpAgent', () => {
|
||||
mockGetSettings.mockImplementationOnce(() => ({
|
||||
permissions: { defaultMode: 'acceptEdits' },
|
||||
}))
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
|
||||
() => {},
|
||||
)
|
||||
const agent = new AcpAgent(makeConn())
|
||||
try {
|
||||
await expect(
|
||||
@@ -406,7 +406,9 @@ describe('AcpAgent', () => {
|
||||
mockGetSettings.mockImplementationOnce(() => ({
|
||||
permissions: { defaultMode: 'invalid-mode' },
|
||||
}))
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
|
||||
() => {},
|
||||
)
|
||||
const agent = new AcpAgent(makeConn())
|
||||
try {
|
||||
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||
@@ -422,7 +424,9 @@ describe('AcpAgent', () => {
|
||||
mockGetSettings.mockImplementationOnce(() => ({
|
||||
permissions: { defaultMode: 'acceptEdits' },
|
||||
}))
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
|
||||
() => {},
|
||||
)
|
||||
const agent = new AcpAgent(makeConn())
|
||||
try {
|
||||
await expect(
|
||||
@@ -976,7 +980,9 @@ describe('AcpAgent', () => {
|
||||
resolveFirst()
|
||||
const results = await Promise.all([first, ...queued])
|
||||
|
||||
expect(results.every(result => result.stopReason === 'end_turn')).toBe(true)
|
||||
expect(results.every(result => result.stopReason === 'end_turn')).toBe(
|
||||
true,
|
||||
)
|
||||
expect(mockSubmitMessage.mock.calls.map(call => call[0])).toEqual([
|
||||
'first',
|
||||
...Array.from({ length: 1000 }, (_, index) => `queued-${index}`),
|
||||
@@ -989,13 +995,17 @@ describe('AcpAgent', () => {
|
||||
|
||||
let resolveFirst!: () => void
|
||||
let resolveSecond!: () => void
|
||||
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(
|
||||
;(
|
||||
forwardSessionUpdates as ReturnType<typeof mock>
|
||||
).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<{ stopReason: string }>(resolve => {
|
||||
resolveFirst = () => resolve({ stopReason: 'end_turn' })
|
||||
}),
|
||||
)
|
||||
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(
|
||||
;(
|
||||
forwardSessionUpdates as ReturnType<typeof mock>
|
||||
).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<{ stopReason: string }>(resolve => {
|
||||
resolveSecond = () => resolve({ stopReason: 'end_turn' })
|
||||
|
||||
@@ -748,10 +748,14 @@ describe('forwardSessionUpdates', () => {
|
||||
ac.signal.addEventListener = addEventListener
|
||||
ac.signal.removeEventListener = removeEventListener
|
||||
|
||||
const msgs = Array.from({ length: 10_000 }, () => ({
|
||||
type: 'system',
|
||||
subtype: 'api_retry',
|
||||
}) as unknown as SDKMessage)
|
||||
const msgs = Array.from(
|
||||
{ length: 10_000 },
|
||||
() =>
|
||||
({
|
||||
type: 'system',
|
||||
subtype: 'api_retry',
|
||||
}) as unknown as SDKMessage,
|
||||
)
|
||||
|
||||
const result = await forwardSessionUpdates(
|
||||
's1',
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { afterAll, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
|
||||
import {
|
||||
afterAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
spyOn,
|
||||
test,
|
||||
} from 'bun:test'
|
||||
import type { AgentSideConnection } from '@agentclientprotocol/sdk'
|
||||
import type { Tool as ToolType, ToolUseContext } from '../../../Tool.js'
|
||||
import type { AssistantMessage } from '../../../types/message.js'
|
||||
@@ -29,7 +37,10 @@ const bridgeModuleSnapshot = {
|
||||
|
||||
afterAll(() => {
|
||||
mock.module('../bridge.js', () => bridgeModuleSnapshot)
|
||||
mock.module('../../../utils/permissions/permissions.js', () => permissionsModuleSnapshot)
|
||||
mock.module(
|
||||
'../../../utils/permissions/permissions.js',
|
||||
() => permissionsModuleSnapshot,
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('../../../utils/permissions/permissions.js', () => ({
|
||||
@@ -248,12 +259,20 @@ describe('createAcpCanUseTool', () => {
|
||||
() => false,
|
||||
)
|
||||
|
||||
await canUseTool(makeTool('ExitPlanMode'), {}, dummyContext, dummyMsg, 'tu_9')
|
||||
await canUseTool(
|
||||
makeTool('ExitPlanMode'),
|
||||
{},
|
||||
dummyContext,
|
||||
dummyMsg,
|
||||
'tu_9',
|
||||
)
|
||||
|
||||
const { options } = (conn.requestPermission as ReturnType<typeof mock>).mock
|
||||
.calls[0][0] as Record<string, unknown>
|
||||
const opts = options as Array<Record<string, unknown>>
|
||||
expect(opts.some(option => option.optionId === 'bypassPermissions')).toBe(false)
|
||||
expect(opts.some(option => option.optionId === 'bypassPermissions')).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('ExitPlanMode includes bypass option when the session exposes it', async () => {
|
||||
@@ -268,12 +287,20 @@ describe('createAcpCanUseTool', () => {
|
||||
() => true,
|
||||
)
|
||||
|
||||
await canUseTool(makeTool('ExitPlanMode'), {}, dummyContext, dummyMsg, 'tu_10')
|
||||
await canUseTool(
|
||||
makeTool('ExitPlanMode'),
|
||||
{},
|
||||
dummyContext,
|
||||
dummyMsg,
|
||||
'tu_10',
|
||||
)
|
||||
|
||||
const { options } = (conn.requestPermission as ReturnType<typeof mock>).mock
|
||||
.calls[0][0] as Record<string, unknown>
|
||||
const opts = options as Array<Record<string, unknown>>
|
||||
expect(opts.some(option => option.optionId === 'bypassPermissions')).toBe(true)
|
||||
expect(opts.some(option => option.optionId === 'bypassPermissions')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('ExitPlanMode rejects a bypass selection that was not offered', async () => {
|
||||
@@ -301,6 +328,8 @@ describe('createAcpCanUseTool', () => {
|
||||
|
||||
expect(result.behavior).toBe('deny')
|
||||
expect(onModeChange).not.toHaveBeenCalled()
|
||||
expect((conn.sessionUpdate as ReturnType<typeof mock>).mock.calls).toHaveLength(0)
|
||||
expect(
|
||||
(conn.sessionUpdate as ReturnType<typeof mock>).mock.calls,
|
||||
).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,7 +41,10 @@ import type {
|
||||
import { randomUUID, type UUID } from 'node:crypto'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { deserializeMessages } from '../../utils/conversationRecovery.js'
|
||||
import { getLastSessionLog, sessionIdExists } from '../../utils/sessionStorage.js'
|
||||
import {
|
||||
getLastSessionLog,
|
||||
sessionIdExists,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { QueryEngine } from '../../QueryEngine.js'
|
||||
import type { QueryEngineConfig } from '../../QueryEngine.js'
|
||||
import type { Tools } from '../../Tool.js'
|
||||
@@ -56,16 +59,18 @@ import { FileStateCache } from '../../utils/fileStateCache.js'
|
||||
import { getDefaultAppState } from '../../state/AppStateStore.js'
|
||||
import type { AppState } from '../../state/AppStateStore.js'
|
||||
import { createAcpCanUseTool } from './permissions.js'
|
||||
import { forwardSessionUpdates, replayHistoryMessages, type ToolUseCache } from './bridge.js'
|
||||
import {
|
||||
forwardSessionUpdates,
|
||||
replayHistoryMessages,
|
||||
type ToolUseCache,
|
||||
} from './bridge.js'
|
||||
import {
|
||||
resolvePermissionMode,
|
||||
computeSessionFingerprint,
|
||||
sanitizeTitle,
|
||||
} from './utils.js'
|
||||
import { promptToQueryInput } from './promptConversion.js'
|
||||
import {
|
||||
listSessionsImpl,
|
||||
} from '../../utils/listSessionsImpl.js'
|
||||
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
||||
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
||||
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
||||
@@ -123,8 +128,12 @@ export class AcpAgent implements Agent {
|
||||
.MACRO !== null
|
||||
? String(
|
||||
(
|
||||
(globalThis as unknown as Record<string, Record<string, unknown>>)
|
||||
.MACRO as Record<string, unknown>
|
||||
(
|
||||
globalThis as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
).MACRO as Record<string, unknown>
|
||||
).VERSION ?? '0.0.0',
|
||||
)
|
||||
: '0.0.0',
|
||||
@@ -156,7 +165,9 @@ export class AcpAgent implements Agent {
|
||||
|
||||
// ── authenticate ──────────────────────────────────────────────
|
||||
|
||||
async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse> {
|
||||
async authenticate(
|
||||
_params: AuthenticateRequest,
|
||||
): Promise<AuthenticateResponse> {
|
||||
// No authentication required — this is a self-hosted/custom deployment
|
||||
return {}
|
||||
}
|
||||
@@ -189,7 +200,9 @@ export class AcpAgent implements Agent {
|
||||
|
||||
// ── listSessions ───────────────────────────────────────────────
|
||||
|
||||
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
||||
async listSessions(
|
||||
params: ListSessionsRequest,
|
||||
): Promise<ListSessionsResponse> {
|
||||
const candidates = await listSessionsImpl({
|
||||
dir: params.cwd ?? undefined,
|
||||
limit: 100,
|
||||
@@ -214,13 +227,11 @@ export class AcpAgent implements Agent {
|
||||
async unstable_forkSession(
|
||||
params: ForkSessionRequest,
|
||||
): Promise<ForkSessionResponse> {
|
||||
const response = await this.createSession(
|
||||
{
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
_meta: params._meta,
|
||||
},
|
||||
)
|
||||
const response = await this.createSession({
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
_meta: params._meta,
|
||||
})
|
||||
this.scheduleAvailableCommandsUpdate(response.sessionId)
|
||||
return response
|
||||
}
|
||||
@@ -258,7 +269,7 @@ export class AcpAgent implements Agent {
|
||||
// Handle prompt queuing — if a prompt is already running, queue this one
|
||||
if (session.promptRunning) {
|
||||
const promptUuid = randomUUID()
|
||||
const cancelled = await new Promise<boolean>((resolve) => {
|
||||
const cancelled = await new Promise<boolean>(resolve => {
|
||||
session.pendingQueue.push(promptUuid)
|
||||
session.pendingMessages.set(promptUuid, { resolve })
|
||||
})
|
||||
@@ -414,7 +425,7 @@ export class AcpAgent implements Agent {
|
||||
)
|
||||
}
|
||||
|
||||
const option = session.configOptions.find((o) => o.id === params.configId)
|
||||
const option = session.configOptions.find(o => o.id === params.configId)
|
||||
if (!option) {
|
||||
throw new Error(`Unknown config option: ${params.configId}`)
|
||||
}
|
||||
@@ -436,7 +447,7 @@ export class AcpAgent implements Agent {
|
||||
|
||||
this.syncSessionConfigState(session, params.configId, value)
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
session.configOptions = session.configOptions.map(o =>
|
||||
o.id === params.configId && typeof o.currentValue === 'string'
|
||||
? { ...o, currentValue: value }
|
||||
: o,
|
||||
@@ -449,7 +460,11 @@ export class AcpAgent implements Agent {
|
||||
|
||||
private async createSession(
|
||||
params: NewSessionRequest,
|
||||
opts: { forceNewId?: boolean; sessionId?: string; initialMessages?: Message[] } = {},
|
||||
opts: {
|
||||
forceNewId?: boolean
|
||||
sessionId?: string
|
||||
initialMessages?: Message[]
|
||||
} = {},
|
||||
): Promise<NewSessionResponse> {
|
||||
enableConfigs()
|
||||
|
||||
@@ -468,140 +483,176 @@ export class AcpAgent implements Agent {
|
||||
}
|
||||
|
||||
try {
|
||||
// Build tools with a permissive permission context.
|
||||
const permissionContext = getEmptyToolPermissionContext()
|
||||
const tools: Tools = getTools(permissionContext)
|
||||
// Build tools with a permissive permission context.
|
||||
const permissionContext = getEmptyToolPermissionContext()
|
||||
const tools: Tools = getTools(permissionContext)
|
||||
|
||||
// Parse permission mode from _meta (passed by RCS/acp-link) or settings.
|
||||
const meta = params._meta as Record<string, unknown> | null | undefined
|
||||
const hasMetaPermissionMode = hasOwnField(meta, 'permissionMode')
|
||||
const metaPermissionMode = hasMetaPermissionMode
|
||||
? meta?.permissionMode
|
||||
: undefined
|
||||
const settingsPermissionMode = this.getSetting<string>('permissions.defaultMode')
|
||||
const permissionMode = resolveSessionPermissionMode(
|
||||
metaPermissionMode,
|
||||
hasMetaPermissionMode,
|
||||
settingsPermissionMode,
|
||||
)
|
||||
// Parse permission mode from _meta (passed by RCS/acp-link) or settings.
|
||||
const meta = params._meta as Record<string, unknown> | null | undefined
|
||||
const hasMetaPermissionMode = hasOwnField(meta, 'permissionMode')
|
||||
const metaPermissionMode = hasMetaPermissionMode
|
||||
? meta?.permissionMode
|
||||
: undefined
|
||||
const settingsPermissionMode = this.getSetting<string>(
|
||||
'permissions.defaultMode',
|
||||
)
|
||||
const permissionMode = resolveSessionPermissionMode(
|
||||
metaPermissionMode,
|
||||
hasMetaPermissionMode,
|
||||
settingsPermissionMode,
|
||||
)
|
||||
|
||||
// Create the permission bridge canUseTool function
|
||||
const canUseTool = createAcpCanUseTool(
|
||||
this.conn,
|
||||
sessionId,
|
||||
() => this.sessions.get(sessionId)?.modes.currentModeId ?? 'default',
|
||||
this.clientCapabilities,
|
||||
cwd,
|
||||
(modeId: string) => { this.applySessionMode(sessionId, modeId) },
|
||||
() => this.sessions.get(sessionId)?.appState
|
||||
.toolPermissionContext.isBypassPermissionsModeAvailable ?? false,
|
||||
)
|
||||
|
||||
// Parse MCP servers from ACP params
|
||||
// MCP server config is handled separately in the tools system
|
||||
|
||||
// ACP clients can expose bypass only when both the process and local config allow it.
|
||||
const isBypassAvailable = isAcpBypassPermissionModeAvailable(settingsPermissionMode)
|
||||
|
||||
// Create a mutable AppState for the session
|
||||
const appState: AppState = {
|
||||
...getDefaultAppState(),
|
||||
toolPermissionContext: {
|
||||
...permissionContext,
|
||||
mode: permissionMode as PermissionMode,
|
||||
isBypassPermissionsModeAvailable: isBypassAvailable,
|
||||
},
|
||||
}
|
||||
|
||||
// Load commands for slash command and skill support
|
||||
const commands = await getCommands(cwd)
|
||||
|
||||
// Build QueryEngine config
|
||||
const engineConfig: QueryEngineConfig = {
|
||||
cwd,
|
||||
tools,
|
||||
commands,
|
||||
mcpClients: [],
|
||||
agents: [],
|
||||
canUseTool,
|
||||
getAppState: () => appState,
|
||||
setAppState: (updater: (prev: AppState) => AppState) => {
|
||||
const updated = updater(appState)
|
||||
Object.assign(appState, updated)
|
||||
},
|
||||
readFileCache: new FileStateCache(500, 50 * 1024 * 1024),
|
||||
includePartialMessages: true,
|
||||
replayUserMessages: true,
|
||||
initialMessages: opts.initialMessages,
|
||||
}
|
||||
|
||||
const queryEngine = new QueryEngine(engineConfig)
|
||||
|
||||
// Build modes — bypassPermissions is opt-in for ACP clients.
|
||||
const availableModes = [
|
||||
{ id: 'default', name: 'Default', description: 'Standard behavior, prompts for dangerous operations' },
|
||||
{ id: 'acceptEdits', name: 'Accept Edits', description: 'Auto-accept file edit operations' },
|
||||
{ id: 'plan', name: 'Plan Mode', description: 'Planning mode, no actual tool execution' },
|
||||
{ id: 'auto', name: 'Auto', description: 'Use a model classifier to approve/deny permission prompts.' },
|
||||
...(isBypassAvailable
|
||||
? [{ id: 'bypassPermissions' as const, name: 'Bypass Permissions', description: 'Skip all permission checks' }]
|
||||
: []),
|
||||
{ id: 'dontAsk', name: "Don't Ask", description: "Don't prompt for permissions, deny if not pre-approved" },
|
||||
]
|
||||
|
||||
const modes: SessionModeState = {
|
||||
currentModeId: permissionMode,
|
||||
availableModes,
|
||||
}
|
||||
|
||||
// Build models
|
||||
const modelOptions = getModelOptions()
|
||||
const currentModel = getMainLoopModel()
|
||||
const models: SessionModelState = {
|
||||
availableModels: modelOptions.map((m) => ({
|
||||
modelId: String(m.value ?? ''),
|
||||
name: m.label ?? String(m.value ?? ''),
|
||||
description: m.description ?? undefined,
|
||||
})),
|
||||
currentModelId: currentModel,
|
||||
}
|
||||
|
||||
// Set the model on the engine
|
||||
queryEngine.setModel(currentModel)
|
||||
|
||||
// Build config options
|
||||
const configOptions = buildConfigOptions(modes, models)
|
||||
|
||||
const session: AcpSession = {
|
||||
queryEngine,
|
||||
cancelled: false,
|
||||
cancelGeneration: 0,
|
||||
cwd,
|
||||
modes,
|
||||
models,
|
||||
configOptions,
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
pendingQueue: [],
|
||||
pendingQueueHead: 0,
|
||||
toolUseCache: {},
|
||||
clientCapabilities: this.clientCapabilities,
|
||||
appState,
|
||||
commands,
|
||||
sessionFingerprint: computeSessionFingerprint({
|
||||
// Create the permission bridge canUseTool function
|
||||
const canUseTool = createAcpCanUseTool(
|
||||
this.conn,
|
||||
sessionId,
|
||||
() => this.sessions.get(sessionId)?.modes.currentModeId ?? 'default',
|
||||
this.clientCapabilities,
|
||||
cwd,
|
||||
mcpServers: params.mcpServers as Array<{ name: string; [key: string]: unknown }> | undefined,
|
||||
}),
|
||||
}
|
||||
(modeId: string) => {
|
||||
this.applySessionMode(sessionId, modeId)
|
||||
},
|
||||
() =>
|
||||
this.sessions.get(sessionId)?.appState.toolPermissionContext
|
||||
.isBypassPermissionsModeAvailable ?? false,
|
||||
)
|
||||
|
||||
this.sessions.set(sessionId, session)
|
||||
// Parse MCP servers from ACP params
|
||||
// MCP server config is handled separately in the tools system
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
models,
|
||||
modes,
|
||||
configOptions,
|
||||
}
|
||||
// ACP clients can expose bypass only when both the process and local config allow it.
|
||||
const isBypassAvailable = isAcpBypassPermissionModeAvailable(
|
||||
settingsPermissionMode,
|
||||
)
|
||||
|
||||
// Create a mutable AppState for the session
|
||||
const appState: AppState = {
|
||||
...getDefaultAppState(),
|
||||
toolPermissionContext: {
|
||||
...permissionContext,
|
||||
mode: permissionMode as PermissionMode,
|
||||
isBypassPermissionsModeAvailable: isBypassAvailable,
|
||||
},
|
||||
}
|
||||
|
||||
// Load commands for slash command and skill support
|
||||
const commands = await getCommands(cwd)
|
||||
|
||||
// Build QueryEngine config
|
||||
const engineConfig: QueryEngineConfig = {
|
||||
cwd,
|
||||
tools,
|
||||
commands,
|
||||
mcpClients: [],
|
||||
agents: [],
|
||||
canUseTool,
|
||||
getAppState: () => appState,
|
||||
setAppState: (updater: (prev: AppState) => AppState) => {
|
||||
const updated = updater(appState)
|
||||
Object.assign(appState, updated)
|
||||
},
|
||||
readFileCache: new FileStateCache(500, 50 * 1024 * 1024),
|
||||
includePartialMessages: true,
|
||||
replayUserMessages: true,
|
||||
initialMessages: opts.initialMessages,
|
||||
}
|
||||
|
||||
const queryEngine = new QueryEngine(engineConfig)
|
||||
|
||||
// Build modes — bypassPermissions is opt-in for ACP clients.
|
||||
const availableModes = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
description: 'Standard behavior, prompts for dangerous operations',
|
||||
},
|
||||
{
|
||||
id: 'acceptEdits',
|
||||
name: 'Accept Edits',
|
||||
description: 'Auto-accept file edit operations',
|
||||
},
|
||||
{
|
||||
id: 'plan',
|
||||
name: 'Plan Mode',
|
||||
description: 'Planning mode, no actual tool execution',
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
name: 'Auto',
|
||||
description:
|
||||
'Use a model classifier to approve/deny permission prompts.',
|
||||
},
|
||||
...(isBypassAvailable
|
||||
? [
|
||||
{
|
||||
id: 'bypassPermissions' as const,
|
||||
name: 'Bypass Permissions',
|
||||
description: 'Skip all permission checks',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'dontAsk',
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
]
|
||||
|
||||
const modes: SessionModeState = {
|
||||
currentModeId: permissionMode,
|
||||
availableModes,
|
||||
}
|
||||
|
||||
// Build models
|
||||
const modelOptions = getModelOptions()
|
||||
const currentModel = getMainLoopModel()
|
||||
const models: SessionModelState = {
|
||||
availableModels: modelOptions.map(m => ({
|
||||
modelId: String(m.value ?? ''),
|
||||
name: m.label ?? String(m.value ?? ''),
|
||||
description: m.description ?? undefined,
|
||||
})),
|
||||
currentModelId: currentModel,
|
||||
}
|
||||
|
||||
// Set the model on the engine
|
||||
queryEngine.setModel(currentModel)
|
||||
|
||||
// Build config options
|
||||
const configOptions = buildConfigOptions(modes, models)
|
||||
|
||||
const session: AcpSession = {
|
||||
queryEngine,
|
||||
cancelled: false,
|
||||
cancelGeneration: 0,
|
||||
cwd,
|
||||
modes,
|
||||
models,
|
||||
configOptions,
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
pendingQueue: [],
|
||||
pendingQueueHead: 0,
|
||||
toolUseCache: {},
|
||||
clientCapabilities: this.clientCapabilities,
|
||||
appState,
|
||||
commands,
|
||||
sessionFingerprint: computeSessionFingerprint({
|
||||
cwd,
|
||||
mcpServers: params.mcpServers as
|
||||
| Array<{ name: string; [key: string]: unknown }>
|
||||
| undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
this.sessions.set(sessionId, session)
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
models,
|
||||
modes,
|
||||
configOptions,
|
||||
}
|
||||
} finally {
|
||||
if (processCwdChanged) {
|
||||
process.chdir(previousProcessCwd)
|
||||
@@ -619,8 +670,9 @@ export class AcpAgent implements Agent {
|
||||
if (existingSession) {
|
||||
const fingerprint = computeSessionFingerprint({
|
||||
cwd: params.cwd,
|
||||
mcpServers:
|
||||
params.mcpServers as Array<{ name: string; [key: string]: unknown }> | undefined,
|
||||
mcpServers: params.mcpServers as
|
||||
| Array<{ name: string; [key: string]: unknown }>
|
||||
| undefined,
|
||||
})
|
||||
if (fingerprint === existingSession.sessionFingerprint) {
|
||||
return {
|
||||
@@ -703,7 +755,9 @@ export class AcpAgent implements Agent {
|
||||
) {
|
||||
throw new Error(`Mode not available: ${modeId}`)
|
||||
}
|
||||
const isAvailable = session.modes.availableModes.some(mode => mode.id === modeId)
|
||||
const isAvailable = session.modes.availableModes.some(
|
||||
mode => mode.id === modeId,
|
||||
)
|
||||
if (!isAvailable) {
|
||||
throw new Error(`Mode not available: ${modeId}`)
|
||||
}
|
||||
@@ -727,7 +781,7 @@ export class AcpAgent implements Agent {
|
||||
|
||||
this.syncSessionConfigState(session, configId, value)
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
session.configOptions = session.configOptions.map(o =>
|
||||
o.id === configId && typeof o.currentValue === 'string'
|
||||
? { ...o, currentValue: value }
|
||||
: o,
|
||||
@@ -761,9 +815,7 @@ export class AcpAgent implements Agent {
|
||||
const availableCommands = session.commands
|
||||
.filter(
|
||||
cmd =>
|
||||
cmd.type === 'prompt' &&
|
||||
!cmd.isHidden &&
|
||||
cmd.userInvocable !== false,
|
||||
cmd.type === 'prompt' && !cmd.isHidden && cmd.userInvocable !== false,
|
||||
)
|
||||
.map(cmd => ({
|
||||
name: cmd.name,
|
||||
@@ -851,14 +903,22 @@ function resolveRequiredPermissionMode(
|
||||
return resolvePermissionMode(mode, source) as PermissionMode
|
||||
}
|
||||
|
||||
function resolveConfiguredPermissionMode(mode: unknown): PermissionMode | undefined {
|
||||
function resolveConfiguredPermissionMode(
|
||||
mode: unknown,
|
||||
): PermissionMode | undefined {
|
||||
if (mode === undefined || mode === null) return undefined
|
||||
|
||||
try {
|
||||
return resolvePermissionMode(mode, 'permissions.defaultMode') as PermissionMode
|
||||
return resolvePermissionMode(
|
||||
mode,
|
||||
'permissions.defaultMode',
|
||||
) as PermissionMode
|
||||
} catch (err: unknown) {
|
||||
const reason = err instanceof Error ? err.message : String(err)
|
||||
console.error('[ACP] Invalid permissions.defaultMode, using default:', reason)
|
||||
console.error(
|
||||
'[ACP] Invalid permissions.defaultMode, using default:',
|
||||
reason,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -873,7 +933,8 @@ function hasOwnField(
|
||||
function isAcpBypassPermissionModeAvailable(settingsMode?: unknown): boolean {
|
||||
return (
|
||||
isProcessBypassPermissionModeAvailable() &&
|
||||
(isAcpBypassLocallyEnabled() || isSettingsBypassPermissionMode(settingsMode))
|
||||
(isAcpBypassLocallyEnabled() ||
|
||||
isSettingsBypassPermissionMode(settingsMode))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -948,11 +1009,13 @@ function buildConfigOptions(
|
||||
category: 'mode',
|
||||
type: 'select' as const,
|
||||
currentValue: modes.currentModeId,
|
||||
options: modes.availableModes.map((m: SessionModeState['availableModes'][number]) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
})),
|
||||
options: modes.availableModes.map(
|
||||
(m: SessionModeState['availableModes'][number]) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'model',
|
||||
@@ -961,11 +1024,13 @@ function buildConfigOptions(
|
||||
category: 'model',
|
||||
type: 'select' as const,
|
||||
currentValue: models.currentModelId,
|
||||
options: models.availableModels.map((m: SessionModelState['availableModels'][number]) => ({
|
||||
value: m.modelId,
|
||||
name: m.name,
|
||||
description: m.description ?? undefined,
|
||||
})),
|
||||
options: models.availableModels.map(
|
||||
(m: SessionModelState['availableModels'][number]) => ({
|
||||
value: m.modelId,
|
||||
name: m.name,
|
||||
description: m.description ?? undefined,
|
||||
}),
|
||||
),
|
||||
},
|
||||
] as SessionConfigOption[]
|
||||
}
|
||||
|
||||
@@ -72,7 +72,12 @@ export function toolInfoFromToolUse(
|
||||
title: description,
|
||||
kind: 'think',
|
||||
content: prompt
|
||||
? [{ type: 'content' as const, content: { type: 'text' as const, text: prompt } }]
|
||||
? [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: prompt },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
@@ -86,7 +91,12 @@ export function toolInfoFromToolUse(
|
||||
content: _supportsTerminalOutput
|
||||
? [{ type: 'terminal' as const, terminalId: toolUse.id }]
|
||||
: description
|
||||
? [{ type: 'content' as const, content: { type: 'text' as const, text: description } }]
|
||||
? [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: description },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
@@ -118,8 +128,20 @@ export function toolInfoFromToolUse(
|
||||
title: displayPath ? `Write ${displayPath}` : 'Write',
|
||||
kind: 'edit',
|
||||
content: filePath
|
||||
? [{ type: 'diff' as const, path: filePath, oldText: null, newText: content }]
|
||||
: [{ type: 'content' as const, content: { type: 'text' as const, text: content } }],
|
||||
? [
|
||||
{
|
||||
type: 'diff' as const,
|
||||
path: filePath,
|
||||
oldText: null,
|
||||
newText: content,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: content },
|
||||
},
|
||||
],
|
||||
locations: filePath ? [{ path: filePath }] : [],
|
||||
}
|
||||
}
|
||||
@@ -133,7 +155,14 @@ export function toolInfoFromToolUse(
|
||||
title: displayPath ? `Edit ${displayPath}` : 'Edit',
|
||||
kind: 'edit',
|
||||
content: filePath
|
||||
? [{ type: 'diff' as const, path: filePath, oldText: oldString || null, newText: newString }]
|
||||
? [
|
||||
{
|
||||
type: 'diff' as const,
|
||||
path: filePath,
|
||||
oldText: oldString || null,
|
||||
newText: newString,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
locations: filePath ? [{ path: filePath }] : [],
|
||||
}
|
||||
@@ -164,7 +193,8 @@ export function toolInfoFromToolUse(
|
||||
if (input?.['-C'] !== undefined) label += ` -C ${input['-C'] as number}`
|
||||
if (input?.output_mode === 'files_with_matches') label += ' -l'
|
||||
else if (input?.output_mode === 'count') label += ' -c'
|
||||
if (input?.head_limit !== undefined) label += ` | head -${input.head_limit as number}`
|
||||
if (input?.head_limit !== undefined)
|
||||
label += ` | head -${input.head_limit as number}`
|
||||
if (input?.glob) label += ` --include="${input.glob as string}"`
|
||||
if (input?.type) label += ` --type=${input.type as string}`
|
||||
if (input?.multiline) label += ' -P'
|
||||
@@ -184,7 +214,12 @@ export function toolInfoFromToolUse(
|
||||
title: url ? `Fetch ${url}` : 'Fetch',
|
||||
kind: 'fetch',
|
||||
content: fetchPrompt
|
||||
? [{ type: 'content' as const, content: { type: 'text' as const, text: fetchPrompt } }]
|
||||
? [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: fetchPrompt },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
@@ -194,8 +229,10 @@ export function toolInfoFromToolUse(
|
||||
let label = `"${query}"`
|
||||
const allowed = input?.allowed_domains as string[] | undefined
|
||||
const blocked = input?.blocked_domains as string[] | undefined
|
||||
if (allowed && allowed.length > 0) label += ` (allowed: ${allowed.join(', ')})`
|
||||
if (blocked && blocked.length > 0) label += ` (blocked: ${blocked.join(', ')})`
|
||||
if (allowed && allowed.length > 0)
|
||||
label += ` (allowed: ${allowed.join(', ')})`
|
||||
if (blocked && blocked.length > 0)
|
||||
label += ` (blocked: ${blocked.join(', ')})`
|
||||
return {
|
||||
title: label,
|
||||
kind: 'fetch',
|
||||
@@ -207,7 +244,7 @@ export function toolInfoFromToolUse(
|
||||
const todos = input?.todos as Array<{ content: string }> | undefined
|
||||
return {
|
||||
title: Array.isArray(todos)
|
||||
? `Update TODOs: ${todos.map((t) => t.content).join(', ')}`
|
||||
? `Update TODOs: ${todos.map(t => t.content).join(', ')}`
|
||||
: 'Update TODOs',
|
||||
kind: 'think',
|
||||
content: [],
|
||||
@@ -215,12 +252,19 @@ export function toolInfoFromToolUse(
|
||||
}
|
||||
|
||||
case 'ExitPlanMode': {
|
||||
const plan = (input as Record<string, unknown>)?.plan as string | undefined
|
||||
const plan = (input as Record<string, unknown>)?.plan as
|
||||
| string
|
||||
| undefined
|
||||
return {
|
||||
title: 'Ready to code?',
|
||||
kind: 'switch_mode',
|
||||
content: plan
|
||||
? [{ type: 'content' as const, content: { type: 'text' as const, text: plan } }]
|
||||
? [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: plan },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
@@ -240,7 +284,11 @@ export function toolUpdateFromToolResult(
|
||||
toolResult: Record<string, unknown>,
|
||||
toolUse: { name: string; id: string } | undefined,
|
||||
_supportsTerminalOutput: boolean = false,
|
||||
): { content?: ToolCallContent[]; title?: string; _meta?: Record<string, unknown> } {
|
||||
): {
|
||||
content?: ToolCallContent[]
|
||||
title?: string
|
||||
_meta?: Record<string, unknown>
|
||||
} {
|
||||
if (!toolUse) return {}
|
||||
|
||||
const isError = toolResult.is_error === true
|
||||
@@ -261,7 +309,10 @@ export function toolUpdateFromToolResult(
|
||||
content: [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: { type: 'text' as const, text: markdownEscape(resultContent) },
|
||||
content: {
|
||||
type: 'text' as const,
|
||||
text: markdownEscape(resultContent),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -272,7 +323,10 @@ export function toolUpdateFromToolResult(
|
||||
type: 'content' as const,
|
||||
content:
|
||||
c.type === 'text'
|
||||
? { type: 'text' as const, text: markdownEscape(c.text as string) }
|
||||
? {
|
||||
type: 'text' as const,
|
||||
text: markdownEscape(c.text as string),
|
||||
}
|
||||
: toAcpContentBlock(c, false),
|
||||
})),
|
||||
}
|
||||
@@ -290,10 +344,13 @@ export function toolUpdateFromToolResult(
|
||||
resultContent &&
|
||||
typeof resultContent === 'object' &&
|
||||
!Array.isArray(resultContent) &&
|
||||
(resultContent as Record<string, unknown>).type === 'bash_code_execution_result'
|
||||
(resultContent as Record<string, unknown>).type ===
|
||||
'bash_code_execution_result'
|
||||
) {
|
||||
const bashResult = resultContent as Record<string, unknown>
|
||||
output = [bashResult.stdout, bashResult.stderr].filter(Boolean).join('\n')
|
||||
output = [bashResult.stdout, bashResult.stderr]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
exitCode = (bashResult.return_code as number) ?? (isError ? 1 : 0)
|
||||
} else if (typeof resultContent === 'string') {
|
||||
output = resultContent
|
||||
@@ -311,7 +368,11 @@ export function toolUpdateFromToolResult(
|
||||
_meta: {
|
||||
terminal_info: { terminal_id: terminalId },
|
||||
terminal_output: { terminal_id: terminalId, data: output },
|
||||
terminal_exit: { terminal_id: terminalId, exit_code: exitCode, signal: null },
|
||||
terminal_exit: {
|
||||
terminal_id: terminalId,
|
||||
exit_code: exitCode,
|
||||
signal: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -342,10 +403,7 @@ export function toolUpdateFromToolResult(
|
||||
}
|
||||
|
||||
default: {
|
||||
return toAcpContentUpdate(
|
||||
resultContent ?? '',
|
||||
isError,
|
||||
)
|
||||
return toAcpContentUpdate(resultContent ?? '', isError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,8 +469,12 @@ function toAcpContentBlock(
|
||||
case 'tool_reference':
|
||||
return wrapText(`Tool: ${content.tool_name as string}`)
|
||||
case 'tool_search_tool_search_result': {
|
||||
const refs = content.tool_references as Array<{ tool_name: string }> | undefined
|
||||
return wrapText(`Tools found: ${refs?.map((r) => r.tool_name).join(', ') || 'none'}`)
|
||||
const refs = content.tool_references as
|
||||
| Array<{ tool_name: string }>
|
||||
| undefined
|
||||
return wrapText(
|
||||
`Tools found: ${refs?.map(r => r.tool_name).join(', ') || 'none'}`,
|
||||
)
|
||||
}
|
||||
case 'tool_search_tool_result_error':
|
||||
return wrapText(
|
||||
@@ -428,7 +490,9 @@ function toAcpContentBlock(
|
||||
return wrapText(`Error: ${content.error_code as string}`)
|
||||
case 'code_execution_result':
|
||||
case 'bash_code_execution_result':
|
||||
return wrapText(`Output: ${(content.stdout as string) || (content.stderr as string) || ''}`)
|
||||
return wrapText(
|
||||
`Output: ${(content.stdout as string) || (content.stderr as string) || ''}`,
|
||||
)
|
||||
case 'code_execution_tool_result_error':
|
||||
case 'bash_code_execution_tool_result_error':
|
||||
return wrapText(`Error: ${content.error_code as string}`)
|
||||
@@ -508,7 +572,10 @@ export function toolUpdateFromEditToolResponse(toolResponse: unknown): {
|
||||
}
|
||||
}
|
||||
|
||||
const result: { content?: ToolCallContent[]; locations?: ToolCallLocation[] } = {}
|
||||
const result: {
|
||||
content?: ToolCallContent[]
|
||||
locations?: ToolCallLocation[]
|
||||
} = {}
|
||||
if (content.length > 0) result.content = content
|
||||
if (locations.length > 0) result.locations = locations
|
||||
return result
|
||||
@@ -523,10 +590,12 @@ function nextSdkMessageOrAbort(
|
||||
}
|
||||
|
||||
let abortHandler: (() => void) | undefined
|
||||
const abortPromise = new Promise<IteratorResult<SDKMessage, void>>((resolve) => {
|
||||
abortHandler = () => resolve({ done: true, value: undefined })
|
||||
abortSignal.addEventListener('abort', abortHandler, { once: true })
|
||||
})
|
||||
const abortPromise = new Promise<IteratorResult<SDKMessage, void>>(
|
||||
resolve => {
|
||||
abortHandler = () => resolve({ done: true, value: undefined })
|
||||
abortSignal.addEventListener('abort', abortHandler, { once: true })
|
||||
},
|
||||
)
|
||||
|
||||
return Promise.race([sdkMessages.next(), abortPromise]).finally(() => {
|
||||
if (abortHandler) {
|
||||
@@ -622,7 +691,8 @@ export async function forwardSessionUpdates(
|
||||
accumulatedUsage.inputTokens += usage.input_tokens
|
||||
accumulatedUsage.outputTokens += usage.output_tokens
|
||||
accumulatedUsage.cachedReadTokens += usage.cache_read_input_tokens
|
||||
accumulatedUsage.cachedWriteTokens += usage.cache_creation_input_tokens
|
||||
accumulatedUsage.cachedWriteTokens +=
|
||||
usage.cache_creation_input_tokens
|
||||
}
|
||||
|
||||
// Resolve context window size from modelUsage via prefix matching
|
||||
@@ -638,12 +708,12 @@ export async function forwardSessionUpdates(
|
||||
|
||||
// Send usage_update — use lastAssistantTotalUsage if available
|
||||
// (more accurate than accumulatedUsage which may include background tasks)
|
||||
const usedTokens = lastAssistantTotalUsage ?? (
|
||||
const usedTokens =
|
||||
lastAssistantTotalUsage ??
|
||||
accumulatedUsage.inputTokens +
|
||||
accumulatedUsage.outputTokens +
|
||||
accumulatedUsage.cachedReadTokens +
|
||||
accumulatedUsage.cachedWriteTokens
|
||||
)
|
||||
accumulatedUsage.outputTokens +
|
||||
accumulatedUsage.cachedReadTokens +
|
||||
accumulatedUsage.cachedWriteTokens
|
||||
|
||||
const totalCostUsd = msg.total_cost_usd as number | undefined
|
||||
await conn.sessionUpdate({
|
||||
@@ -652,9 +722,10 @@ export async function forwardSessionUpdates(
|
||||
sessionUpdate: 'usage_update',
|
||||
used: usedTokens,
|
||||
size: lastContextWindowSize,
|
||||
cost: totalCostUsd != null
|
||||
? { amount: totalCostUsd, currency: 'USD' }
|
||||
: undefined,
|
||||
cost:
|
||||
totalCostUsd != null
|
||||
? { amount: totalCostUsd, currency: 'USD' }
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -724,8 +795,13 @@ export async function forwardSessionUpdates(
|
||||
case 'assistant': {
|
||||
// Track last assistant total usage for context window computation
|
||||
// (only for top-level messages, not subagents)
|
||||
const assistantMsg = msg.message as Record<string, unknown> | undefined
|
||||
const parentToolUseId = msg.parent_tool_use_id as string | null | undefined
|
||||
const assistantMsg = msg.message as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
const parentToolUseId = msg.parent_tool_use_id as
|
||||
| string
|
||||
| null
|
||||
| undefined
|
||||
if (assistantMsg?.usage && parentToolUseId === null) {
|
||||
const msgUsage = assistantMsg.usage as Record<string, unknown>
|
||||
lastAssistantTotalUsage =
|
||||
@@ -773,7 +849,10 @@ export async function forwardSessionUpdates(
|
||||
|
||||
// Handle agent/skill subagent progress
|
||||
const progressType = progressData.type as string | undefined
|
||||
if (progressType === 'agent_progress' || progressType === 'skill_progress') {
|
||||
if (
|
||||
progressType === 'agent_progress' ||
|
||||
progressType === 'skill_progress'
|
||||
) {
|
||||
const progressMessage = progressData.message as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
@@ -887,7 +966,15 @@ function assistantMessageToAcpNotifications(
|
||||
]
|
||||
}
|
||||
|
||||
return toAcpNotifications(content, 'assistant', sessionId, toolUseCache, conn, undefined, options)
|
||||
return toAcpNotifications(
|
||||
content,
|
||||
'assistant',
|
||||
sessionId,
|
||||
toolUseCache,
|
||||
conn,
|
||||
undefined,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Stream event conversion ───────────────────────────────────────
|
||||
@@ -907,7 +994,9 @@ function streamEventToAcpNotifications(
|
||||
|
||||
switch (event.type as string) {
|
||||
case 'content_block_start': {
|
||||
const contentBlock = event.content_block as Record<string, unknown> | undefined
|
||||
const contentBlock = event.content_block as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
if (!contentBlock) return []
|
||||
return toAcpNotifications(
|
||||
[contentBlock],
|
||||
@@ -1001,7 +1090,9 @@ function toAcpNotifications(
|
||||
if (source?.type === 'base64') {
|
||||
update = {
|
||||
sessionUpdate:
|
||||
role === 'assistant' ? 'agent_message_chunk' : 'user_message_chunk',
|
||||
role === 'assistant'
|
||||
? 'agent_message_chunk'
|
||||
: 'user_message_chunk',
|
||||
content: {
|
||||
type: 'image',
|
||||
data: source.data as string,
|
||||
@@ -1034,7 +1125,7 @@ function toAcpNotifications(
|
||||
| Array<{ content: string; status: string }>
|
||||
| undefined
|
||||
if (Array.isArray(todos)) {
|
||||
const entries: PlanEntry[] = todos.map((todo) => ({
|
||||
const entries: PlanEntry[] = todos.map(todo => ({
|
||||
content: todo.content,
|
||||
status: normalizePlanStatus(todo.status),
|
||||
priority: 'medium',
|
||||
@@ -1086,8 +1177,7 @@ function toAcpNotifications(
|
||||
|
||||
case 'tool_result':
|
||||
case 'mcp_tool_result': {
|
||||
const toolUseId =
|
||||
(chunk.tool_use_id as string | undefined) ?? ''
|
||||
const toolUseId = (chunk.tool_use_id as string | undefined) ?? ''
|
||||
const toolUse = toolUseCache[toolUseId]
|
||||
if (!toolUse) break
|
||||
|
||||
@@ -1105,7 +1195,9 @@ function toAcpNotifications(
|
||||
toolCallId: toolUseId,
|
||||
sessionUpdate: 'tool_call_update',
|
||||
status:
|
||||
(chunk.is_error as boolean | undefined) === true ? 'failed' : 'completed',
|
||||
(chunk.is_error as boolean | undefined) === true
|
||||
? 'failed'
|
||||
: 'completed',
|
||||
rawOutput: chunk.content,
|
||||
...toolUpdate,
|
||||
}
|
||||
@@ -1178,7 +1270,8 @@ export async function replayHistoryMessages(
|
||||
const content = messageData?.content
|
||||
if (!content) continue
|
||||
|
||||
const role: 'assistant' | 'user' = type === 'assistant' ? 'assistant' : 'user'
|
||||
const role: 'assistant' | 'user' =
|
||||
type === 'assistant' ? 'assistant' : 'user'
|
||||
|
||||
if (typeof content === 'string') {
|
||||
if (!content.trim()) continue
|
||||
@@ -1234,5 +1327,5 @@ function getMatchingModelUsage(
|
||||
}
|
||||
}
|
||||
|
||||
return bestKey ? modelUsage[bestKey] ?? null : null
|
||||
return bestKey ? (modelUsage[bestKey] ?? null) : null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AgentSideConnection,
|
||||
ndJsonStream,
|
||||
} from '@agentclientprotocol/sdk'
|
||||
import { AgentSideConnection, ndJsonStream } from '@agentclientprotocol/sdk'
|
||||
import type { Stream } from '@agentclientprotocol/sdk'
|
||||
import { Readable, Writable } from 'node:stream'
|
||||
import { AcpAgent } from './agent.js'
|
||||
@@ -39,7 +36,7 @@ export async function runAcpAgent(): Promise<void> {
|
||||
const stream = createAcpStream(process.stdin, process.stdout)
|
||||
|
||||
let agent!: AcpAgent
|
||||
const connection = new AgentSideConnection((conn) => {
|
||||
const connection = new AgentSideConnection(conn => {
|
||||
agent = new AcpAgent(conn)
|
||||
return agent
|
||||
}, stream)
|
||||
|
||||
@@ -44,14 +44,25 @@ export function createAcpCanUseTool(
|
||||
context: ToolUseContext,
|
||||
assistantMessage: AssistantMessage,
|
||||
toolUseID: string,
|
||||
forceDecision?: PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision,
|
||||
): Promise<PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision> => {
|
||||
forceDecision?:
|
||||
| PermissionAllowDecision
|
||||
| PermissionAskDecision
|
||||
| PermissionDenyDecision,
|
||||
): Promise<
|
||||
PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision
|
||||
> => {
|
||||
const supportsTerminalOutput = checkTerminalOutput(clientCapabilities)
|
||||
|
||||
// ── ExitPlanMode special handling ────────────────────────────
|
||||
if (tool.name === 'ExitPlanMode') {
|
||||
return handleExitPlanMode(
|
||||
conn, sessionId, toolUseID, input, supportsTerminalOutput, cwd, onModeChange,
|
||||
conn,
|
||||
sessionId,
|
||||
toolUseID,
|
||||
input,
|
||||
supportsTerminalOutput,
|
||||
cwd,
|
||||
onModeChange,
|
||||
isBypassModeAvailable,
|
||||
)
|
||||
}
|
||||
@@ -66,7 +77,11 @@ export function createAcpCanUseTool(
|
||||
// bypassPermissions mode, dontAsk mode, acceptEdits mode, auto mode classifier
|
||||
try {
|
||||
const pipelineResult = await hasPermissionsToUseTool(
|
||||
tool, input, context, assistantMessage, toolUseID,
|
||||
tool,
|
||||
input,
|
||||
context,
|
||||
assistantMessage,
|
||||
toolUseID,
|
||||
)
|
||||
|
||||
// If the pipeline resolved to allow or deny, return that
|
||||
@@ -168,9 +183,21 @@ async function handleExitPlanMode(
|
||||
isBypassModeAvailable?: () => boolean,
|
||||
): Promise<PermissionAllowDecision | PermissionDenyDecision> {
|
||||
const options: Array<PermissionOption> = [
|
||||
{ kind: 'allow_always', name: 'Yes, and use "auto" mode', optionId: 'auto' },
|
||||
{ kind: 'allow_always', name: 'Yes, and auto-accept edits', optionId: 'acceptEdits' },
|
||||
{ kind: 'allow_once', name: 'Yes, and manually approve edits', optionId: 'default' },
|
||||
{
|
||||
kind: 'allow_always',
|
||||
name: 'Yes, and use "auto" mode',
|
||||
optionId: 'auto',
|
||||
},
|
||||
{
|
||||
kind: 'allow_always',
|
||||
name: 'Yes, and auto-accept edits',
|
||||
optionId: 'acceptEdits',
|
||||
},
|
||||
{
|
||||
kind: 'allow_once',
|
||||
name: 'Yes, and manually approve edits',
|
||||
optionId: 'default',
|
||||
},
|
||||
{ kind: 'reject_once', name: 'No, keep planning', optionId: 'plan' },
|
||||
]
|
||||
if (isBypassModeAvailable?.() === true) {
|
||||
@@ -215,15 +242,15 @@ async function handleExitPlanMode(
|
||||
response.outcome.optionId !== undefined
|
||||
) {
|
||||
const selectedOption = response.outcome.optionId
|
||||
const isOfferedOption = options.some(option => option.optionId === selectedOption)
|
||||
const isOfferedOption = options.some(
|
||||
option => option.optionId === selectedOption,
|
||||
)
|
||||
if (
|
||||
isOfferedOption &&
|
||||
(
|
||||
selectedOption === 'default' ||
|
||||
(selectedOption === 'default' ||
|
||||
selectedOption === 'acceptEdits' ||
|
||||
selectedOption === 'auto' ||
|
||||
selectedOption === 'bypassPermissions'
|
||||
)
|
||||
selectedOption === 'bypassPermissions')
|
||||
) {
|
||||
// Sync mode to session state and appState
|
||||
onModeChange?.(selectedOption)
|
||||
|
||||
Reference in New Issue
Block a user