fix(tui): bound diff rendering work
This commit is contained in:
@@ -57,6 +57,7 @@ interface RenderedDiff {
|
||||
lines: string[]
|
||||
added: number
|
||||
removed: number
|
||||
approximate: boolean
|
||||
}
|
||||
|
||||
/** Split one diff change into display rows without counting its trailing line terminator. */
|
||||
@@ -66,8 +67,12 @@ function diffValueLines(value: string): string[] {
|
||||
return (safe.endsWith('\n') ? safe.slice(0, -1) : safe).split('\n')
|
||||
}
|
||||
|
||||
/** A file diff whose unchanged context stays neutral and does not affect change totals. */
|
||||
function renderDiff(diff: FileDiff, palette: Palette): RenderedDiff {
|
||||
/**
|
||||
* A file diff whose unchanged context stays neutral and does not affect exact
|
||||
* change totals. Comparisons beyond the edit-distance budget fall back to
|
||||
* whole-side rendering so a model-authored pending edit cannot stall the TUI.
|
||||
*/
|
||||
function renderDiff(diff: FileDiff, maxDiffEditLength: number, palette: Palette): RenderedDiff {
|
||||
// The card header is a fixed `Tool / <name>` frame that never names a file, so
|
||||
// each hunk always carries its own path header (no redundancy to suppress).
|
||||
const lines = [palette.bold(displayText(diff.path))]
|
||||
@@ -77,9 +82,20 @@ function renderDiff(diff: FileDiff, palette: Palette): RenderedDiff {
|
||||
const newLines = diffValueLines(diff.newText)
|
||||
added = newLines.length
|
||||
for (const line of newLines) lines.push(palette.success(`+ ${line}`))
|
||||
return { lines, added, removed }
|
||||
return { lines, added, removed, approximate: false }
|
||||
}
|
||||
for (const change of compareLines(diff.oldText, diff.newText)) {
|
||||
const changes = compareLines(diff.oldText, diff.newText, { maxEditLength: maxDiffEditLength })
|
||||
if (changes === undefined) {
|
||||
const oldLines = diffValueLines(diff.oldText)
|
||||
const newLines = diffValueLines(diff.newText)
|
||||
lines.push(palette.dim(`[exact line diff omitted: >${maxDiffEditLength} changed lines]`))
|
||||
removed = oldLines.length
|
||||
added = newLines.length
|
||||
for (const line of oldLines) lines.push(palette.error(`- ${line}`))
|
||||
for (const line of newLines) lines.push(palette.success(`+ ${line}`))
|
||||
return { lines, added, removed, approximate: true }
|
||||
}
|
||||
for (const change of changes) {
|
||||
const changedLines = diffValueLines(change.value)
|
||||
if (change.added) {
|
||||
added += changedLines.length
|
||||
@@ -91,7 +107,7 @@ function renderDiff(diff: FileDiff, palette: Palette): RenderedDiff {
|
||||
for (const line of changedLines) lines.push(palette.dim(` ${line}`))
|
||||
}
|
||||
}
|
||||
return { lines, added, removed }
|
||||
return { lines, added, removed, approximate: false }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,12 +370,14 @@ export class ToolCardComponent implements Component {
|
||||
private visibility: ToolCardVisibility = 'collapsed'
|
||||
private callView: ToolCallView
|
||||
private resultView: ToolResultView | undefined
|
||||
private diffBodyCache: { view: ToolCallView | ToolResultView; body: CardBody } | undefined
|
||||
|
||||
constructor(
|
||||
private readonly name: string,
|
||||
private readonly parsed: ParsedArguments,
|
||||
private readonly definition: ToolDefinition | undefined,
|
||||
private readonly maxOutputLines: number,
|
||||
private readonly maxDiffEditLength: number,
|
||||
private readonly palette: Palette,
|
||||
private readonly mdTheme: MarkdownTheme,
|
||||
) {
|
||||
@@ -530,21 +548,27 @@ export class ToolCardComponent implements Component {
|
||||
return { prelude: prelude.filter(Boolean), lines: lines.filter(Boolean) }
|
||||
}
|
||||
if (view.card === 'diff') {
|
||||
if (this.diffBodyCache?.view === view) return this.diffBodyCache.body
|
||||
// The header no longer names the file, so each diff keeps its own path
|
||||
// header. A trailing footer summarizes the change (`+A -R · N file(s)`).
|
||||
let added = 0
|
||||
let removed = 0
|
||||
const hunks = view.diffs.flatMap((diff, index) => {
|
||||
const rendered = renderDiff(diff, this.palette)
|
||||
added += rendered.added
|
||||
removed += rendered.removed
|
||||
const renderedDiffs = view.diffs.map(diff =>
|
||||
renderDiff(diff, this.maxDiffEditLength, this.palette),
|
||||
)
|
||||
const added = renderedDiffs.reduce((total, rendered) => total + rendered.added, 0)
|
||||
const removed = renderedDiffs.reduce((total, rendered) => total + rendered.removed, 0)
|
||||
const approximate = renderedDiffs.some(rendered => rendered.approximate)
|
||||
const hunks = renderedDiffs.flatMap((rendered, index) => {
|
||||
return [...index > 0 ? [''] : [], ...rendered.lines]
|
||||
})
|
||||
const files = view.diffs.length
|
||||
const footer = this.palette.dim(`└ +${added} -${removed} · ${files} file${files === 1 ? '' : 's'}`)
|
||||
const footer = this.palette.dim(
|
||||
`└ +${added} -${removed} · ${files} file${files === 1 ? '' : 's'}${approximate ? ' · approximate' : ''}`,
|
||||
)
|
||||
// A diff's own `+`/`-` colors carry its meaning, so it renders verbatim
|
||||
// rather than under the dim result-output color.
|
||||
return { prelude: [...hunks, footer], lines: [] }
|
||||
const body = { prelude: [...hunks, footer], lines: [] }
|
||||
this.diffBodyCache = { view, body }
|
||||
return body
|
||||
}
|
||||
// The web card carries no `content` copy, so a `web` result view falls back
|
||||
// to the raw result content here (`view.card === 'generic'` narrows the
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface TuiConfig {
|
||||
showReasoning?: boolean
|
||||
/** Maximum tool-card body lines retained in its collapsed head/tail preview. */
|
||||
maxToolOutputLines?: number
|
||||
/** Maximum added and removed lines explored while deriving an exact line diff. */
|
||||
maxDiffEditLength?: number
|
||||
/** Maximum options visible at once in a user-question panel. */
|
||||
maxQuestionOptions?: number
|
||||
/** Maximum models visible at once in the model selector. */
|
||||
@@ -64,6 +66,7 @@ export interface TuiConfig {
|
||||
|
||||
const showReasoningSchema = z.boolean().default(true)
|
||||
const maxToolOutputLinesSchema = z.number().step(1).min(1).default(6)
|
||||
const maxDiffEditLengthSchema = z.number().step(1).min(1).default(1000)
|
||||
const maxQuestionOptionsSchema = z.number().step(1).min(1).default(8)
|
||||
const maxModelOptionsSchema = z.number().step(1).min(1).default(8)
|
||||
const maxResumeOptionsSchema = z.number().step(1).min(1).default(8)
|
||||
@@ -95,6 +98,7 @@ const titleSchema = z.string().default('DeepSeek Harness')
|
||||
const tuiConfigSchemaFields = {
|
||||
showReasoning: showReasoningSchema,
|
||||
maxToolOutputLines: maxToolOutputLinesSchema,
|
||||
maxDiffEditLength: maxDiffEditLengthSchema,
|
||||
maxQuestionOptions: maxQuestionOptionsSchema,
|
||||
maxModelOptions: maxModelOptionsSchema,
|
||||
maxResumeOptions: maxResumeOptionsSchema,
|
||||
@@ -135,6 +139,7 @@ export const Config: z<Config> = z.object({
|
||||
initialSkill: z.string(),
|
||||
showReasoning: tuiConfigSchemaFields.showReasoning,
|
||||
maxToolOutputLines: tuiConfigSchemaFields.maxToolOutputLines,
|
||||
maxDiffEditLength: tuiConfigSchemaFields.maxDiffEditLength,
|
||||
maxQuestionOptions: tuiConfigSchemaFields.maxQuestionOptions,
|
||||
maxModelOptions: tuiConfigSchemaFields.maxModelOptions,
|
||||
maxResumeOptions: tuiConfigSchemaFields.maxResumeOptions,
|
||||
@@ -164,6 +169,7 @@ export interface ResolvedTuiThemeConfig {
|
||||
export interface ResolvedTuiConfig {
|
||||
showReasoning: boolean
|
||||
maxToolOutputLines: number
|
||||
maxDiffEditLength: number
|
||||
maxQuestionOptions: number
|
||||
maxModelOptions: number
|
||||
maxResumeOptions: number
|
||||
@@ -189,6 +195,7 @@ export function resolveTuiConfig(config: TuiConfig | undefined): ResolvedTuiConf
|
||||
return {
|
||||
showReasoning: config?.showReasoning ?? true,
|
||||
maxToolOutputLines: config?.maxToolOutputLines ?? 6,
|
||||
maxDiffEditLength: config?.maxDiffEditLength ?? 1000,
|
||||
maxQuestionOptions: config?.maxQuestionOptions ?? 8,
|
||||
maxModelOptions: config?.maxModelOptions ?? 8,
|
||||
maxResumeOptions: config?.maxResumeOptions ?? 8,
|
||||
|
||||
@@ -605,6 +605,7 @@ export function createTuiChat(
|
||||
parsed,
|
||||
ctx.tools.get(event.data.name, agent),
|
||||
resolved.maxToolOutputLines,
|
||||
resolved.maxDiffEditLength,
|
||||
palette,
|
||||
mdTheme,
|
||||
)
|
||||
@@ -748,7 +749,15 @@ export function createTuiChat(
|
||||
const callId = event.data.message.source.callId
|
||||
let card = toolCards.get(callId)
|
||||
if (card === undefined) {
|
||||
card = new ToolCardComponent('tool', { value: {}, valid: true }, undefined, resolved.maxToolOutputLines, palette, mdTheme)
|
||||
card = new ToolCardComponent(
|
||||
'tool',
|
||||
{ value: {}, valid: true },
|
||||
undefined,
|
||||
resolved.maxToolOutputLines,
|
||||
resolved.maxDiffEditLength,
|
||||
palette,
|
||||
mdTheme,
|
||||
)
|
||||
card.setVisibility(toolsVisibility)
|
||||
chat.addChild(card)
|
||||
allToolCards.add(card)
|
||||
|
||||
Reference in New Issue
Block a user