feat(desktop): PR-style file diffs in chat

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.
This commit is contained in:
Brooklyn Nicholson 2026-06-22 05:04:13 -05:00
parent fb3d31ba8b
commit a61baa9615
7 changed files with 451 additions and 50 deletions

View file

@ -108,6 +108,56 @@ export function codiconForLanguage(language: string | undefined): string {
return CODICON_BY_LANGUAGE[sanitizeLanguageTag(language || '')] || 'code'
}
// File extension → language tag, so a filename can resolve to the same icon a
// fenced code block of that language would get. Only extensions that map to a
// non-generic codicon need an entry; everything else falls through to `code`.
const LANGUAGE_BY_EXTENSION: Record<string, string> = {
bash: 'bash',
cfg: 'ini',
conf: 'ini',
css: 'css',
dockerfile: 'dockerfile',
env: 'env',
gql: 'graphql',
graphql: 'graphql',
ini: 'ini',
json: 'json',
json5: 'json',
less: 'less',
markdown: 'markdown',
md: 'markdown',
mdx: 'markdown',
mmd: 'mermaid',
ps1: 'powershell',
psql: 'sql',
sass: 'sass',
scss: 'scss',
sh: 'bash',
sql: 'sql',
svg: 'svg',
toml: 'toml',
yaml: 'yaml',
yml: 'yml',
zsh: 'zsh'
}
// Pick an icon for a file path by its extension (or bare name like
// `Dockerfile`), reusing the language→codicon map so file-edit rows and code
// blocks share one visual vocabulary. Unknown / generic code files get `code`.
export function codiconForFilename(path: string | undefined): string {
const base = (path || '').replace(/\\/g, '/').split('/').pop()?.trim().toLowerCase() || ''
if (!base) {
return 'code'
}
const dot = base.lastIndexOf('.')
const token = dot > 0 ? base.slice(dot + 1) : base
const language = LANGUAGE_BY_EXTENSION[token] || token
return codiconForLanguage(language)
}
function proseLineCount(body: string): number {
return body.split('\n').filter(line => {
const trimmed = line.trim()