mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
Render write_file/edit_file/patch as a reviewable diff instead of raw result JSON, closer to a Cursor/T3 per-edit review. - Unified diff via FileDiffPanel: strip git file-header + @@ hunk noise, drop the +/- gutter, color by line with a 2px gutter accent, full-bleed to the card, transparent context lines, compact scroll height. - Header shows filename + language icon + +N/-N stats; full path moves to a hover tooltip (no Edited verb, no ms). - Treat the three file-edit tools uniformly (isFileEditTool); read diff from inline_diff or patch's diff field; suppress raw-arg detail. - Reusable FileTypeIcon primitive sharing the code-block icon mapping (codiconForFilename), codicon fallback. - Per-row scaffolding fade (not the group wrapper, which trapped child opacity); expanded edits stay full, collapsed fade; keyboard-only focus lift. Hide diff-less rehydrated creates that read as dupes.
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildToolView,
|
|
countDiffLineStats,
|
|
inlineDiffFromResult,
|
|
type ToolPart
|
|
} from './tool-fallback-model'
|
|
|
|
const part = (overrides: Partial<ToolPart>): ToolPart => ({
|
|
args: {},
|
|
isError: false,
|
|
result: {},
|
|
toolCallId: 'call_1',
|
|
toolName: 'vision_analyze',
|
|
type: 'tool-call',
|
|
...overrides
|
|
})
|
|
|
|
describe('buildToolView image handling', () => {
|
|
// vision_analyze reports the input image as a local path; an <img> pointed at
|
|
// a bare path resolves against the renderer origin and 404s, so we render the
|
|
// tool codicon instead of a broken image.
|
|
it('drops bare filesystem paths', () => {
|
|
expect(buildToolView(part({ args: { path: '/Users/me/shot.png' } }), '').imageUrl).toBe('')
|
|
expect(buildToolView(part({ result: { image_path: '/tmp/out.jpg' } }), '').imageUrl).toBe('')
|
|
})
|
|
|
|
it('keeps fetchable data URLs', () => {
|
|
const dataUrl = 'data:image/png;base64,AAAA'
|
|
|
|
expect(buildToolView(part({ result: { image_url: dataUrl } }), '').imageUrl).toBe(dataUrl)
|
|
})
|
|
|
|
it('keeps remote http(s) image URLs', () => {
|
|
const url = 'https://example.com/pic.webp'
|
|
|
|
expect(buildToolView(part({ result: { url } }), '').imageUrl).toBe(url)
|
|
})
|
|
})
|
|
|
|
describe('buildToolView terminal exit-code status', () => {
|
|
const terminal = (result: Record<string, unknown>) =>
|
|
buildToolView(part({ result, toolName: 'terminal' }), '')
|
|
|
|
// A non-zero exit code with real output is not a failure (grep no-match,
|
|
// diff differences, piped commands surfacing the last stage's code, etc.) —
|
|
// it should render as success so the card isn't painted red.
|
|
it('treats non-zero exit with output as success', () => {
|
|
expect(terminal({ exit_code: 7, output: 'node ... 5174 (LISTEN)' }).status).toBe('success')
|
|
expect(terminal({ exit_code: 1, stdout: 'partial results' }).status).toBe('success')
|
|
})
|
|
|
|
// No output + non-zero exit is a genuine failure worth flagging.
|
|
it('treats non-zero exit with no output as error', () => {
|
|
expect(terminal({ exit_code: 127, output: '' }).status).toBe('error')
|
|
expect(terminal({ exit_code: 1 }).status).toBe('error')
|
|
})
|
|
|
|
it('treats zero exit as success', () => {
|
|
expect(terminal({ exit_code: 0, output: 'done' }).status).toBe('success')
|
|
})
|
|
|
|
// Explicit error signals still win regardless of output presence.
|
|
it('keeps explicit error signals red even with output', () => {
|
|
expect(terminal({ error: 'boom', exit_code: 0, output: 'partial' }).status).toBe('error')
|
|
expect(buildToolView(part({ isError: true, result: { output: 'x' }, toolName: 'terminal' }), '').status).toBe(
|
|
'error'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('buildToolView file edit diffs', () => {
|
|
const patchDiff = '--- a/src/demo.ts\n+++ b/src/demo.ts\n@@ -1 +1 @@\n-old\n+new'
|
|
|
|
it('reads inline_diff and diff fields from patch results', () => {
|
|
expect(inlineDiffFromResult({ inline_diff: patchDiff })).toBe(patchDiff)
|
|
expect(inlineDiffFromResult({ diff: patchDiff })).toBe(patchDiff)
|
|
})
|
|
|
|
it('suppresses raw patch args when a diff is available', () => {
|
|
const view = buildToolView(
|
|
part({
|
|
args: { context: 'src/demo.ts', mode: 'replace', new_string: 'new', path: 'src/demo.ts' },
|
|
result: { diff: patchDiff, success: true },
|
|
toolName: 'patch'
|
|
}),
|
|
patchDiff
|
|
)
|
|
|
|
expect(view.title).toBe('demo.ts')
|
|
expect(view.subtitle).toBe('src/demo.ts')
|
|
expect(view.detail).toBe('')
|
|
expect(view.inlineDiff).toBe(patchDiff)
|
|
})
|
|
|
|
it('shows path subtitle instead of patch args JSON while pending', () => {
|
|
const view = buildToolView(
|
|
part({
|
|
args: { context: 'src/demo.ts', mode: 'replace', new_string: 'new', path: 'src/demo.ts' },
|
|
result: undefined,
|
|
toolName: 'patch'
|
|
}),
|
|
''
|
|
)
|
|
|
|
expect(view.title).toBe('demo.ts')
|
|
expect(view.subtitle).toBe('src/demo.ts')
|
|
expect(view.detail).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('countDiffLineStats', () => {
|
|
it('counts added and removed lines', () => {
|
|
expect(
|
|
countDiffLineStats(`--- a/x\n+++ b/x\n@@\n-old\n+new\n context\n+another`)
|
|
).toEqual({ added: 2, removed: 1 })
|
|
})
|
|
})
|