* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
import type { StructuredPatchHunk } from 'diff'
|
|
import * as React from 'react'
|
|
import { Suspense, use, useState } from 'react'
|
|
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
|
import { Box, Text } from '@anthropic/ink'
|
|
import type { FileEdit } from '@claude-code-best/builtin-tools/tools/FileEditTool/types.js'
|
|
import {
|
|
findActualString,
|
|
preserveQuoteStyle,
|
|
} from '@claude-code-best/builtin-tools/tools/FileEditTool/utils.js'
|
|
import {
|
|
adjustHunkLineNumbers,
|
|
CONTEXT_LINES,
|
|
getPatchForDisplay,
|
|
} from '../utils/diff.js'
|
|
import { logError } from '../utils/log.js'
|
|
import {
|
|
CHUNK_SIZE,
|
|
openForScan,
|
|
readCapped,
|
|
scanForContext,
|
|
} from '../utils/readEditContext.js'
|
|
import { firstLineOf } from '../utils/stringUtils.js'
|
|
import { StructuredDiffList } from './StructuredDiffList.js'
|
|
|
|
type Props = {
|
|
file_path: string
|
|
edits: FileEdit[]
|
|
}
|
|
|
|
type DiffData = {
|
|
patch: StructuredPatchHunk[]
|
|
firstLine: string | null
|
|
fileContent: string | undefined
|
|
}
|
|
|
|
export function FileEditToolDiff(props: Props): React.ReactNode {
|
|
// Snapshot on mount — the diff must stay consistent even if the file changes
|
|
// while the dialog is open. useMemo on props.edits would re-read the file on
|
|
// every render because callers pass fresh array literals.
|
|
const [dataPromise] = useState(() =>
|
|
loadDiffData(props.file_path, props.edits),
|
|
)
|
|
return (
|
|
<Suspense fallback={<DiffFrame placeholder />}>
|
|
<DiffBody promise={dataPromise} file_path={props.file_path} />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
function DiffBody({
|
|
promise,
|
|
file_path,
|
|
}: {
|
|
promise: Promise<DiffData>
|
|
file_path: string
|
|
}): React.ReactNode {
|
|
const { patch, firstLine, fileContent } = use(promise)
|
|
const { columns } = useTerminalSize()
|
|
return (
|
|
<DiffFrame>
|
|
<StructuredDiffList
|
|
hunks={patch}
|
|
dim={false}
|
|
width={columns}
|
|
filePath={file_path}
|
|
firstLine={firstLine}
|
|
fileContent={fileContent}
|
|
/>
|
|
</DiffFrame>
|
|
)
|
|
}
|
|
|
|
function DiffFrame({
|
|
children,
|
|
placeholder,
|
|
}: {
|
|
children?: React.ReactNode
|
|
placeholder?: boolean
|
|
}): React.ReactNode {
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Box
|
|
borderColor="subtle"
|
|
borderStyle="dashed"
|
|
flexDirection="column"
|
|
borderLeft={false}
|
|
borderRight={false}
|
|
>
|
|
{placeholder ? <Text dimColor>…</Text> : children}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
async function loadDiffData(
|
|
file_path: string,
|
|
edits: FileEdit[],
|
|
): Promise<DiffData> {
|
|
const valid = edits.filter(e => e.old_string != null && e.new_string != null)
|
|
const single = valid.length === 1 ? valid[0]! : undefined
|
|
|
|
// SedEditPermissionRequest passes the entire file as old_string. Scanning for
|
|
// a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the
|
|
// file read entirely and diff the inputs we already have.
|
|
if (single && single.old_string.length >= CHUNK_SIZE) {
|
|
return diffToolInputsOnly(file_path, [single])
|
|
}
|
|
|
|
try {
|
|
const handle = await openForScan(file_path)
|
|
if (handle === null) return diffToolInputsOnly(file_path, valid)
|
|
try {
|
|
// Multi-edit and empty old_string genuinely need full-file for sequential
|
|
// replacements — structuredPatch needs before/after strings. replace_all
|
|
// routes through the chunked path below (shows first-occurrence window;
|
|
// matches within the slice still replace via edit.replace_all).
|
|
if (!single || single.old_string === '') {
|
|
const file = await readCapped(handle)
|
|
if (file === null) return diffToolInputsOnly(file_path, valid)
|
|
const normalized = valid.map(e => normalizeEdit(file, e))
|
|
return {
|
|
patch: getPatchForDisplay({
|
|
filePath: file_path,
|
|
fileContents: file,
|
|
edits: normalized,
|
|
}),
|
|
firstLine: firstLineOf(file),
|
|
fileContent: file,
|
|
}
|
|
}
|
|
|
|
const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES)
|
|
if (ctx.truncated || ctx.content === '') {
|
|
return diffToolInputsOnly(file_path, [single])
|
|
}
|
|
const normalized = normalizeEdit(ctx.content, single)
|
|
const hunks = getPatchForDisplay({
|
|
filePath: file_path,
|
|
fileContents: ctx.content,
|
|
edits: [normalized],
|
|
})
|
|
return {
|
|
patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1),
|
|
firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null,
|
|
fileContent: ctx.content,
|
|
}
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
} catch (e) {
|
|
logError(e as Error)
|
|
return diffToolInputsOnly(file_path, valid)
|
|
}
|
|
}
|
|
|
|
function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData {
|
|
return {
|
|
patch: edits.flatMap(e =>
|
|
getPatchForDisplay({
|
|
filePath,
|
|
fileContents: e.old_string,
|
|
edits: [e],
|
|
}),
|
|
),
|
|
firstLine: null,
|
|
fileContent: undefined,
|
|
}
|
|
}
|
|
|
|
function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit {
|
|
const actualOld =
|
|
findActualString(fileContent, edit.old_string) || edit.old_string
|
|
const actualNew = preserveQuoteStyle(
|
|
edit.old_string,
|
|
actualOld,
|
|
edit.new_string,
|
|
)
|
|
return { ...edit, old_string: actualOld, new_string: actualNew }
|
|
}
|