Files
claude-code/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx
2026-04-06 23:56:45 +08:00

173 lines
5.0 KiB
TypeScript

import { relative } from 'path'
import * as React from 'react'
import { Suspense, use, useMemo } from 'react'
import { Box, NoSelect, Text } from '@anthropic/ink'
import type {
NotebookCellType,
NotebookContent,
} from '../../../types/notebook.js'
import { intersperse } from '../../../utils/array.js'
import { getCwd } from '../../../utils/cwd.js'
import { getPatchForDisplay } from '../../../utils/diff.js'
import { getFsImplementation } from '../../../utils/fsOperations.js'
import { safeParseJSON } from '../../../utils/json.js'
import { parseCellId } from '../../../utils/notebook.js'
import { HighlightedCode } from '../../HighlightedCode.js'
import { StructuredDiff } from '../../StructuredDiff.js'
type Props = {
notebook_path: string
cell_id: string | undefined
new_source: string
cell_type?: NotebookCellType
edit_mode?: string
verbose: boolean
width: number
}
type InnerProps = {
notebook_path: string
cell_id: string | undefined
new_source: string
cell_type?: NotebookCellType
edit_mode?: string
verbose: boolean
width: number
promise: Promise<NotebookContent | null>
}
export function NotebookEditToolDiff(props: Props): React.ReactNode {
// Create a promise that never rejects so we can handle errors inline.
// Memoized on notebook_path so we don't re-read on every render.
const notebookDataPromise = useMemo(
() =>
getFsImplementation()
.readFile(props.notebook_path, { encoding: 'utf-8' })
.then(content => safeParseJSON(content) as NotebookContent | null)
.catch(() => null),
[props.notebook_path],
)
return (
<Suspense fallback={null}>
<NotebookEditToolDiffInner {...props} promise={notebookDataPromise} />
</Suspense>
)
}
function NotebookEditToolDiffInner({
notebook_path,
cell_id,
new_source,
cell_type,
edit_mode = 'replace',
verbose,
width,
promise,
}: InnerProps): React.ReactNode {
const notebookData = use(promise)
const oldSource = useMemo(() => {
if (!notebookData || !cell_id) {
return ''
}
const cellIndex = parseCellId(cell_id)
if (cellIndex !== undefined) {
if (notebookData.cells[cellIndex]) {
const source = notebookData.cells[cellIndex].source
return Array.isArray(source) ? source.join('') : source
}
return ''
}
const cell = notebookData.cells.find(cell => cell.id === cell_id)
if (!cell) {
return ''
}
return Array.isArray(cell.source) ? cell.source.join('') : cell.source
}, [notebookData, cell_id])
const hunks = useMemo(() => {
if (!notebookData || edit_mode === 'insert' || edit_mode === 'delete') {
return null
}
// Create a "fake" file content with just the cell source
// This allows us to use the regular diff mechanism
return getPatchForDisplay({
filePath: notebook_path,
fileContents: oldSource,
edits: [
{
old_string: oldSource,
new_string: new_source,
replace_all: false,
},
],
ignoreWhitespace: false,
})
}, [notebookData, notebook_path, oldSource, new_source, edit_mode])
let editTypeDescription: string
switch (edit_mode) {
case 'insert':
editTypeDescription = 'Insert new cell'
break
case 'delete':
editTypeDescription = 'Delete cell'
break
default:
editTypeDescription = 'Replace cell contents'
}
return (
<Box flexDirection="column">
<Box borderStyle="round" flexDirection="column" paddingX={1}>
<Box paddingBottom={1} flexDirection="column">
<Text bold>
{verbose ? notebook_path : relative(getCwd(), notebook_path)}
</Text>
<Text dimColor>
{editTypeDescription} for cell {cell_id}
{cell_type ? ` (${cell_type})` : ''}
</Text>
</Box>
{edit_mode === 'delete' ? (
<Box flexDirection="column" paddingLeft={2}>
<HighlightedCode code={oldSource} filePath={notebook_path} />
</Box>
) : edit_mode === 'insert' ? (
<Box flexDirection="column" paddingLeft={2}>
<HighlightedCode
code={new_source}
filePath={cell_type === 'markdown' ? 'file.md' : notebook_path}
/>
</Box>
) : hunks ? (
intersperse(
hunks.map(_ => (
<StructuredDiff
key={_.newStart}
patch={_}
dim={false}
width={width}
filePath={notebook_path}
firstLine={new_source.split('\n')[0] ?? null}
fileContent={oldSource}
/>
)),
i => (
<NoSelect fromLeftEdge key={`ellipsis-${i}`}>
<Text dimColor>...</Text>
</NoSelect>
),
)
) : (
<HighlightedCode
code={new_source}
filePath={cell_type === 'markdown' ? 'file.md' : notebook_path}
/>
)}
</Box>
</Box>
)
}