openclaw/apps/web/app/components/syntax-block.tsx
kumarabhirup 0f6849a731
Web app: add syntax highlighting, diff viewer, rich chat editor, and file search
Syntax highlighting & code viewer:
- Add shiki for syntax-highlighted fenced code blocks in chat messages
- New SyntaxBlock component (lazy shiki, dual light/dark theme)
- New CodeViewer for workspace file panel (routes code files via isCodeFile())
- API routes (browse-file, virtual-file) now return "code" type for known extensions

Diff rendering:
- New DiffCard component for rendering unified diffs with add/remove colors
- diff-blocks.ts parser to extract fenced blocks from markdown
- Chain-of-thought tool steps show inline diffs for edit/write tools
  (synthetic from old_string/new_string or direct from tool output)
- Agent runner passes through diff/firstChangedLine from edit tool results
- Document view handles diff blocks alongside report blocks

Rich chat editor (Tiptap):
- Replace plain textarea with Tiptap-based ChatEditor
- File mention extension (@-mention files with autocomplete dropdown)
- File mention list with keyboard navigation and search via suggest-files API
- New suggest-files API endpoint for fuzzy file search

File search & navigation:
- FileSearch component in workspace sidebar (debounced search, keyboard nav)
- Search results navigate sidebar to file location and open in panel
- File picker modal for browsing/selecting workspace files

Drag & drop:
- File tree nodes support native HTML5 drag (application/x-file-mention)
  for cross-component drops (e.g. dragging files into chat editor)

Chat attachments reworked:
- Switch from browser File objects to path-based references (name + path)
- Simplified attachment strip (no media previews, shows shortened paths)

Also adds software-engineering skill and related CSS for code blocks/shiki.
2026-02-13 18:06:59 -08:00

90 lines
2.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { createHighlighter, type Highlighter } from "shiki";
// Singleton highlighter (shared with code-viewer)
let highlighterPromise: Promise<Highlighter> | null = null;
/** Languages to preload for chat code blocks. */
const CHAT_LANGS = [
"typescript", "tsx", "javascript", "jsx",
"python", "ruby", "go", "rust", "java",
"c", "cpp", "csharp", "swift", "kotlin",
"css", "scss", "html", "xml",
"json", "yaml", "toml",
"bash", "sql", "graphql",
"markdown", "diff", "php", "lua",
"vue", "svelte", "dart", "zig",
];
function getHighlighter(): Promise<Highlighter> {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ["github-dark", "github-light"],
langs: CHAT_LANGS,
});
}
return highlighterPromise;
}
type SyntaxBlockProps = {
code: string;
lang: string;
};
/**
* Renders a syntax-highlighted code block using shiki.
* Falls back to plain monospace while loading.
*/
export function SyntaxBlock({ code, lang }: SyntaxBlockProps) {
const [html, setHtml] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
getHighlighter().then((hl) => {
if (cancelled) {return;}
try {
const result = hl.codeToHtml(code, {
lang,
themes: {
dark: "github-dark",
light: "github-light",
},
});
setHtml(result);
} catch {
// If the language isn't loaded, fall back to plain text
try {
const result = hl.codeToHtml(code, {
lang: "text",
themes: {
dark: "github-dark",
light: "github-light",
},
});
setHtml(result);
} catch {
// Give up on highlighting
}
}
});
return () => { cancelled = true; };
}, [code, lang]);
if (html) {
return (
<div
className="syntax-block"
// biome-ignore lint/security/noDangerouslySetInnerHtml: shiki output is trusted
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
// Fallback: plain code while shiki loads
return (
<code className="block">{code}</code>
);
}