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.
90 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|