mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-20 15:25:50 +00:00
通过 4 阶段 workflow(分析 → 计划 → 重构 → 验证)将 3 个超大的 ACP 源文件拆分为 28 个模块化子文件,每个均严格小于 500 行,且完整保留 所有公共 API(barrel 模式重导出)。 变更概要: - packages/acp-link/src/server.ts: 1800 → 20 行(barrel),新增 11 个子模块 (server/types、payload-decode、permission-mode、runtime-state、dispatch、 handlers-agent、handlers-session、acp-client、client-send、start-server、 testing-internals) - src/services/acp/agent.ts: 1297 → 33 行(barrel),新增 9 个子模块 (agent/AcpAgent、sessionTypes、permissionMode、configOptions、promptQueue、 internalAccessors、createSessionMethod、sessionLifecycle、promptFlow) - src/services/acp/bridge.ts: 1516 → 29 行(barrel),新增 8 个子模块 (bridge/types、paths、contentBlocks、toolInfo、toolResults、modelUsage、 notifications、forwarding) 验证: - bun run precheck 全通过(typecheck + lint + 5851 tests) - ACP service tests: 176 pass / 0 fail - ACP link tests: 47 pass / 0 fail - 所有外部消费者(entry.ts、permissions.ts、__tests__/)的 import 路径不变 - 测试文件零修改 迁移计划详见 docs/acp-refactor-plan.md。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
240 lines
7.7 KiB
TypeScript
240 lines
7.7 KiB
TypeScript
// toolInfoFromToolUse — large switch mapping each known tool name to ACP ToolInfo.
|
|
import type { ToolInfo } from './types.js'
|
|
import { toAbsolutePath } from './paths.js'
|
|
import { toDisplayPath } from '../utils.js'
|
|
|
|
export function toolInfoFromToolUse(
|
|
toolUse: { name: string; id: string; input: Record<string, unknown> },
|
|
_supportsTerminalOutput: boolean = false,
|
|
cwd?: string,
|
|
): ToolInfo {
|
|
const name = toolUse.name
|
|
const input = toolUse.input
|
|
|
|
switch (name) {
|
|
case 'Agent':
|
|
case 'Task': {
|
|
const description = (input?.description as string | undefined) ?? 'Task'
|
|
const prompt = input?.prompt as string | undefined
|
|
return {
|
|
title: description,
|
|
kind: 'think',
|
|
content: prompt
|
|
? [
|
|
{
|
|
type: 'content' as const,
|
|
content: { type: 'text' as const, text: prompt },
|
|
},
|
|
]
|
|
: [],
|
|
}
|
|
}
|
|
|
|
case 'Bash': {
|
|
const command = (input?.command as string | undefined) ?? 'Terminal'
|
|
const description = input?.description as string | undefined
|
|
// Standard ACP terminal lifecycle (terminal/create → embed real terminalId →
|
|
// terminal/release) is not wired through BashTool yet. Embedding a fake
|
|
// terminalId here would cause compliant clients to fail terminal/output
|
|
// lookups, so we fall back to inline text content per audit doc §5.2.
|
|
// The _supportsTerminalOutput flag is retained for forward compatibility
|
|
// once terminal/create is actually plumbed through.
|
|
void _supportsTerminalOutput
|
|
return {
|
|
title: command,
|
|
kind: 'execute',
|
|
content: description
|
|
? [
|
|
{
|
|
type: 'content' as const,
|
|
content: { type: 'text' as const, text: description },
|
|
},
|
|
]
|
|
: [],
|
|
}
|
|
}
|
|
|
|
case 'Read': {
|
|
const inputFilePath = input?.file_path as string | undefined
|
|
const filePath = inputFilePath ?? 'File'
|
|
const offset = input?.offset as number | undefined
|
|
const limit = input?.limit as number | undefined
|
|
let suffix = ''
|
|
if (limit && limit > 0) {
|
|
suffix = ` (${offset ?? 1} - ${(offset ?? 1) + limit - 1})`
|
|
} else if (offset) {
|
|
suffix = ` (from line ${offset})`
|
|
}
|
|
const displayPath = filePath ? toDisplayPath(filePath, cwd) : 'File'
|
|
const absReadPath = toAbsolutePath(inputFilePath, cwd)
|
|
return {
|
|
title: `Read ${displayPath}${suffix}`,
|
|
kind: 'read',
|
|
locations: absReadPath
|
|
? [{ path: absReadPath, line: offset ?? 1 }]
|
|
: [],
|
|
content: [],
|
|
}
|
|
}
|
|
|
|
case 'Write': {
|
|
const filePath = (input?.file_path as string | undefined) ?? ''
|
|
const content = (input?.content as string | undefined) ?? ''
|
|
const displayPath = filePath ? toDisplayPath(filePath, cwd) : undefined
|
|
const absWritePath = toAbsolutePath(filePath, cwd)
|
|
return {
|
|
title: displayPath ? `Write ${displayPath}` : 'Write',
|
|
kind: 'edit',
|
|
content: absWritePath
|
|
? [
|
|
{
|
|
type: 'diff' as const,
|
|
path: absWritePath,
|
|
oldText: null,
|
|
newText: content,
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
type: 'content' as const,
|
|
content: { type: 'text' as const, text: content },
|
|
},
|
|
],
|
|
locations: absWritePath ? [{ path: absWritePath }] : [],
|
|
}
|
|
}
|
|
|
|
case 'Edit': {
|
|
const filePath = (input?.file_path as string | undefined) ?? ''
|
|
const oldString = (input?.old_string as string | undefined) ?? ''
|
|
const newString = (input?.new_string as string | undefined) ?? ''
|
|
const displayPath = filePath ? toDisplayPath(filePath, cwd) : undefined
|
|
const absEditPath = toAbsolutePath(filePath, cwd)
|
|
return {
|
|
title: displayPath ? `Edit ${displayPath}` : 'Edit',
|
|
kind: 'edit',
|
|
content: absEditPath
|
|
? [
|
|
{
|
|
type: 'diff' as const,
|
|
path: absEditPath,
|
|
oldText: oldString || null,
|
|
newText: newString,
|
|
},
|
|
]
|
|
: [],
|
|
locations: absEditPath ? [{ path: absEditPath }] : [],
|
|
}
|
|
}
|
|
|
|
case 'Glob': {
|
|
const globPath = (input?.path as string | undefined) ?? ''
|
|
const pattern = (input?.pattern as string | undefined) ?? ''
|
|
const absGlobPath = toAbsolutePath(globPath, cwd)
|
|
let label = 'Find'
|
|
if (globPath) label += ` \`${globPath}\``
|
|
if (pattern) label += ` \`${pattern}\``
|
|
return {
|
|
title: label,
|
|
kind: 'search',
|
|
content: [],
|
|
locations: absGlobPath ? [{ path: absGlobPath }] : [],
|
|
}
|
|
}
|
|
|
|
case 'Grep': {
|
|
const grepPattern = (input?.pattern as string | undefined) ?? ''
|
|
const grepPath = (input?.path as string | undefined) ?? ''
|
|
let label = 'grep'
|
|
if (input?.['-i']) label += ' -i'
|
|
if (input?.['-n']) label += ' -n'
|
|
if (input?.['-A'] !== undefined) label += ` -A ${input['-A'] as number}`
|
|
if (input?.['-B'] !== undefined) label += ` -B ${input['-B'] as number}`
|
|
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?.glob) label += ` --include="${input.glob as string}"`
|
|
if (input?.type) label += ` --type=${input.type as string}`
|
|
if (input?.multiline) label += ' -P'
|
|
if (grepPattern) label += ` "${grepPattern}"`
|
|
if (grepPath) label += ` ${grepPath}`
|
|
return {
|
|
title: label,
|
|
kind: 'search',
|
|
content: [],
|
|
}
|
|
}
|
|
|
|
case 'WebFetch': {
|
|
const url = (input?.url as string | undefined) ?? ''
|
|
const fetchPrompt = input?.prompt as string | undefined
|
|
return {
|
|
title: url ? `Fetch ${url}` : 'Fetch',
|
|
kind: 'fetch',
|
|
content: fetchPrompt
|
|
? [
|
|
{
|
|
type: 'content' as const,
|
|
content: { type: 'text' as const, text: fetchPrompt },
|
|
},
|
|
]
|
|
: [],
|
|
}
|
|
}
|
|
|
|
case 'WebSearch': {
|
|
const query = (input?.query as string | undefined) ?? 'Web search'
|
|
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(', ')})`
|
|
return {
|
|
title: label,
|
|
kind: 'fetch',
|
|
content: [],
|
|
}
|
|
}
|
|
|
|
case 'TodoWrite': {
|
|
const todos = input?.todos as Array<{ content: string }> | undefined
|
|
return {
|
|
title: Array.isArray(todos)
|
|
? `Update TODOs: ${todos.map(t => t.content).join(', ')}`
|
|
: 'Update TODOs',
|
|
kind: 'think',
|
|
content: [],
|
|
}
|
|
}
|
|
|
|
case 'ExitPlanMode': {
|
|
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 },
|
|
},
|
|
]
|
|
: [],
|
|
}
|
|
}
|
|
|
|
default:
|
|
return {
|
|
title: name || 'Unknown Tool',
|
|
kind: 'other',
|
|
content: [],
|
|
}
|
|
}
|
|
}
|