Files
deepseek-harness/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts
T

114 lines
4.8 KiB
TypeScript

/** Pure diff-card derivation from raw file-mutation calls and result metadata. @module */
import type { DiffBlockProps, DiffHunk } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolCallBlock } from './tool-call-model.ts'
import { parsedToolCall } from './raw-tool-call.ts'
import { recordedAbsolutePath } from './recorded-path.ts'
/** Room for a path, one removed/added pair, and three context lines on each side. */
export const CHAT_DIFF_MAX_LINES = 9
/**
* The {@link DiffBlock} props this derivation owns. Picked off the primitive's
* props so the two stay in step; `maxLines`/`className` belong to each render
* site.
*/
export interface DiffCardModel {
/**
* The props {@link DiffBlock} draws. Held as a nested object so a render site
* spreads exactly the primitive's own surface and can never leak a
* neighbouring field into it.
*/
card: Pick<DiffBlockProps, 'diffs'>
}
/**
* Narrow opaque result metadata's `diffs` to well-formed hunks.
* @param diffs - the metadata field to validate.
* @returns the validated hunks, or null when the payload is not usable.
*/
function narrowDiffs(diffs: unknown): DiffHunk[] | null {
if (!Array.isArray(diffs) || diffs.length === 0) return null
const out: DiffHunk[] = []
for (const hunk of diffs) {
if (typeof hunk !== 'object' || hunk === null) return null
const { path, oldText, newText } = hunk as Record<string, unknown>
if (typeof path !== 'string') return null
if (oldText !== null && typeof oldText !== 'string') return null
if (typeof newText !== 'string') return null
out.push({ path, oldText, newText })
}
return out
}
type IntendedDiff = { tool: 'write' | 'edit' | 'str_replace_editor'; diff: DiffHunk }
function intendedDiff(block: ToolCallBlock): IntendedDiff | null {
const parsed = parsedToolCall(block)
if (parsed === null) return null
if (parsed.name === 'str_replace_editor') {
const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args
if (typeof path !== 'string' || path.trim() === '') return null
if (command === 'create') {
if (fileText !== undefined && typeof fileText !== 'string') return null
return {
tool: 'str_replace_editor',
diff: { path, oldText: null, newText: fileText ?? '' },
}
}
if (command === 'str_replace') {
if (oldText !== undefined && typeof oldText !== 'string') return null
if (newText !== undefined && typeof newText !== 'string') return null
return {
tool: 'str_replace_editor',
diff: { path, oldText: oldText ?? null, newText: newText ?? '' },
}
}
return null
}
const { file_path: path } = parsed.args
if (typeof path !== 'string' || path.trim() === '') return null
if (parsed.name === 'write') {
const { content } = parsed.args
return typeof content === 'string'
? { tool: 'write', diff: { path, oldText: null, newText: content } }
: null
}
if (parsed.name !== 'edit') return null
const { old_string: oldText, new_string: newText, replace_all: replaceAll } = parsed.args
if (typeof oldText !== 'string' || typeof newText !== 'string') return null
if (replaceAll !== undefined && typeof replaceAll !== 'boolean') return null
return { tool: 'edit', diff: { path, oldText: oldText || null, newText } }
}
function appliedDiffs(meta: unknown): DiffHunk[] | 'empty' | null {
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return null
const diffs = (meta as Record<string, unknown>).diffs
if (!Array.isArray(diffs)) return null
if (diffs.length === 0) return 'empty'
return narrowDiffs(diffs)
}
/**
* Derive running diffs for root write/edit and `str_replace_editor`
* create/replace calls, plus applied settled diffs for root write/edit calls.
* A successful write with valid empty metadata uses its argument-derived
* whole-file diff, matching create and identical-overwrite presentation;
* `str_replace_editor` settles through Generic because it has no result view.
* @param block - running or settled Tool block.
* @returns the diff-card props, or null for the generic path.
*/
export function diffCardModel(block: ToolCallBlock): DiffCardModel | null {
if (block.parentCallId !== undefined) return null
const intended = intendedDiff(block)
if (intended === null) return null
if (!('kind' in block)) return { card: { diffs: [intended.diff] } }
if (intended.tool === 'str_replace_editor') return null
if (block.isError) return null
const applied = appliedDiffs(block.meta)
if (applied === null || applied === 'empty') {
return intended.tool === 'write' ? { card: { diffs: [intended.diff] } } : null
}
const path = recordedAbsolutePath(block.meta, 'path')
return { card: { diffs: applied.map(diff => diff.path === path ? { ...diff, path: intended.diff.path } : diff) } }
}