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 } 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 ( ) } 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 ( {verbose ? notebook_path : relative(getCwd(), notebook_path)} {editTypeDescription} for cell {cell_id} {cell_type ? ` (${cell_type})` : ''} {edit_mode === 'delete' ? ( ) : edit_mode === 'insert' ? ( ) : hunks ? ( intersperse( hunks.map(_ => ( )), i => ( ... ), ) ) : ( )} ) }