Files
claude-code/src/services/acp/bridge/toolInfo.ts
claude-code-best 65f81de52b refactor: 拆分 3 个过大 ACP 文件为模块化子文件(每个 <500 行)
通过 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>
2026-06-20 12:38:43 +08:00

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: [],
}
}
}