Syntax-highlight Edit, Write, and Read tool calls (#1218)
Highlighting reuses the existing code-block tokenizer and LRU cache, runs lazily on expand, and falls back to plain text above a size cap so a large Read can't stall the main thread. The diff highlighter reconstructs the old and new file text by position rather than from @@ line ranges, so diffs that arrive without real hunk headers still highlight. Read content is normalized server-side: the cat -n line-number gutter some providers emit is stripped so content is uniformly raw source, with the first line number surfaced as offset for the client to rebuild the gutter.
This commit is contained in:
@@ -11,9 +11,12 @@ import { StyleSheet } from "react-native-unistyles";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
||||
import { buildLineDiff, parseUnifiedDiff, type DiffLine } from "@/utils/tool-call-parsers";
|
||||
import { highlightDiffLines } from "@/utils/diff-highlight";
|
||||
import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state";
|
||||
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
import { extensionFromPath, highlightToKeyedLines } from "@/utils/highlight-cache";
|
||||
import { HighlightedLines } from "./highlighted-content";
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
import { getCodeInsets } from "./code-insets";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
@@ -143,10 +146,10 @@ function useDetailStyles(
|
||||
function useDiffLines(detail: ToolCallDetail | undefined): DiffLine[] | undefined {
|
||||
return useMemo(() => {
|
||||
if (!detail || detail.type !== "edit") return undefined;
|
||||
if (detail.unifiedDiff) {
|
||||
return parseUnifiedDiff(detail.unifiedDiff);
|
||||
}
|
||||
return buildLineDiff(detail.oldString ?? "", detail.newString ?? "");
|
||||
const diffLines = detail.unifiedDiff
|
||||
? parseUnifiedDiff(detail.unifiedDiff)
|
||||
: buildLineDiff(detail.oldString ?? "", detail.newString ?? "");
|
||||
return highlightDiffLines(diffLines, detail.filePath);
|
||||
}, [detail]);
|
||||
}
|
||||
|
||||
@@ -431,9 +434,22 @@ interface ScrollableContentProps {
|
||||
content: string;
|
||||
ds: DetailStyles;
|
||||
wrapInSectionFill?: boolean;
|
||||
// Drives syntax highlighting (extension only) and, with startLine, a gutter.
|
||||
filePath?: string | null;
|
||||
startLine?: number;
|
||||
}
|
||||
|
||||
function ScrollableTextSection({ content, ds, wrapInSectionFill = true }: ScrollableContentProps) {
|
||||
function ScrollableTextSection({
|
||||
content,
|
||||
ds,
|
||||
wrapInSectionFill = true,
|
||||
filePath,
|
||||
startLine,
|
||||
}: ScrollableContentProps) {
|
||||
const keyedLines = useMemo(
|
||||
() => (filePath ? highlightToKeyedLines(content, extensionFromPath(filePath)) : null),
|
||||
[content, filePath],
|
||||
);
|
||||
const body = (
|
||||
<ScrollView
|
||||
style={ds.scrollAreaFillStyle}
|
||||
@@ -447,9 +463,13 @@ function ScrollableTextSection({ content, ds, wrapInSectionFill = true }: Scroll
|
||||
showsHorizontalScrollIndicator={true}
|
||||
style={ds.webScrollbarStyle}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
{content}
|
||||
</Text>
|
||||
{keyedLines ? (
|
||||
<HighlightedLines lines={keyedLines} startLine={startLine} />
|
||||
) : (
|
||||
<Text selectable style={styles.scrollText}>
|
||||
{content}
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
);
|
||||
@@ -669,14 +689,27 @@ function buildDetailSections(
|
||||
return [
|
||||
<View key="write" style={ds.sectionFillStyle}>
|
||||
{detail.content ? (
|
||||
<ScrollableTextSection content={detail.content} ds={ds} wrapInSectionFill={false} />
|
||||
<ScrollableTextSection
|
||||
content={detail.content}
|
||||
ds={ds}
|
||||
wrapInSectionFill={false}
|
||||
filePath={detail.filePath}
|
||||
/>
|
||||
) : null}
|
||||
</View>,
|
||||
];
|
||||
}
|
||||
if (detail.type === "read") {
|
||||
if (!detail.content) return [];
|
||||
return [<ScrollableTextSection key="read" content={detail.content} ds={ds} />];
|
||||
return [
|
||||
<ScrollableTextSection
|
||||
key="read"
|
||||
content={detail.content}
|
||||
ds={ds}
|
||||
filePath={detail.filePath}
|
||||
startLine={detail.offset ?? 1}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
if (detail.type === "search") {
|
||||
return buildSearchSections(detail, ds);
|
||||
|
||||
Reference in New Issue
Block a user