Files
ompweb/components/MessageView-diff-view.tsx
Khaled Ahmed 13df407e99 refactor: split god modules into focused sibling modules
Verbatim extractions, no behavior change:
- hooks/useAgentSession (3442->2998) + stream/queue/notices helpers
- SessionSidebar (3112->1599) + helpers/chrome/rows
- ChatInput (2814->2560) + model-options/slash-commands/draft-attachments/banners
- AppShell (2436->2283) + app-update/provider-usage/layout
- ModelsConfig (2385->1736) + types/panels
- MessageView (1827->1409) + tool-format/task-panel/diff-view
Contract test points the archive flow at SessionSidebar-rows.
2026-09-03 00:32:32 +03:00

250 lines
7.5 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { useI18n } from "@/lib/i18n";
import { parseUnifiedPatch, type SplitDiffCell } from "@/lib/patch";
import type { ToolResultMessage } from "@/lib/types";
export interface ResultDiff {
text: string;
}
export function getResultDiff(result: ToolResultMessage): ResultDiff | null {
const details = (result as ToolResultMessage & { details?: unknown }).details;
if (typeof details !== "object" || details === null || Array.isArray(details)) return null;
const record = details as Record<string, unknown>;
const patch = typeof record.patch === "string" ? record.patch : null;
if (patch) return { text: patch };
const diff = typeof record.diff === "string" ? record.diff : null;
if (diff) return { text: diff };
return null;
}
export function PairedDiffResult({ diff }: { diff: ResultDiff }) {
return (
<div
style={{
borderTop: "1px solid color-mix(in srgb, var(--status-success) 15%, transparent)",
background: "var(--bg)",
}}
>
<SplitPatchView text={diff.text} />
</div>
);
}
function SplitPatchView({ text }: { text: string }) {
const { t } = useI18n();
const files = useMemo(() => parseUnifiedPatch(text), [text]);
if (!files) return <PatchTextView text={text} />;
const showFileHeaders = files.length > 1;
return (
<div style={{ maxHeight: 560, overflowY: "auto", overflowX: "hidden", background: "var(--bg)" }}>
{files.map((file, fileIndex) => (
<div
key={fileIndex}
style={{
minWidth: 0,
borderTop: fileIndex === 0 ? "none" : "1px solid var(--border)",
fontFamily: "var(--font-mono)",
fontSize: 12,
lineHeight: 1.55,
}}
>
{showFileHeaders && (
<div
style={{
display: "grid",
gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
position: "sticky",
top: 0,
zIndex: 1,
background: "var(--bg-panel)",
borderBottom: "1px solid var(--border)",
}}
>
<SplitDiffHeader title={file.oldPath || t("messageView.diffBefore")} side="left" />
<SplitDiffHeader title={file.newPath || t("messageView.diffAfter")} side="right" />
</div>
)}
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)" }}>
{file.rows.map((row, rowIndex) => {
if (row.type === "hunk") {
return null;
}
return (
<div key={rowIndex} style={{ display: "contents" }}>
<SplitDiffCellView cell={row.left} side="left" />
<SplitDiffCellView cell={row.right} side="right" />
</div>
);
})}
</div>
</div>
))}
</div>
);
}
function SplitDiffHeader({ title, side }: { title: string; side: "left" | "right" }) {
return (
<div
title={title}
style={{
padding: "5px 10px",
color: "var(--text-dim)",
borderRight: side === "left" ? "1px solid var(--border)" : "none",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{title}
</div>
);
}
function SplitDiffCellView({ cell, side }: { cell: SplitDiffCell; side: "left" | "right" }) {
const bg =
cell.type === "added"
? "color-mix(in srgb, var(--status-success) 12%, transparent)"
: cell.type === "removed"
? "color-mix(in srgb, var(--status-error) 13%, transparent)"
: cell.type === "empty"
? "var(--bg-subtle)"
: "transparent";
const marker =
cell.type === "added" ? "+" : cell.type === "removed" ? "-" : " ";
const markerColor =
cell.type === "added" ? "var(--status-success)" : cell.type === "removed" ? "var(--status-error)" : "var(--text-dim)";
return (
<div
style={{
display: "flex",
minWidth: 0,
background: bg,
borderRight: side === "left" ? "1px solid var(--border)" : "none",
}}
>
<span
style={{
width: 42,
padding: "0 6px",
textAlign: "right",
color: "var(--text-dim)",
userSelect: "none",
background: "var(--bg-panel)",
borderRight: "1px solid var(--border)",
flexShrink: 0,
}}
>
{cell.lineNo ?? ""}
</span>
<span
style={{
width: 18,
padding: "0 5px",
color: markerColor,
userSelect: "none",
fontWeight: cell.type === "context" || cell.type === "empty" ? 400 : 700,
flexShrink: 0,
}}
>
{marker}
</span>
<span
style={{
flex: 1,
minWidth: 0,
padding: "0 10px 0 0",
color: cell.type === "empty" ? "var(--text-dim)" : "var(--text)",
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
}}
>
{cell.text || "\u00a0"}
</span>
</div>
);
}
function PatchTextView({ text }: { text: string }) {
const lines = text.split(/\r?\n/);
return (
<div style={{ maxHeight: 520, overflowY: "auto", overflowX: "hidden", fontFamily: "var(--font-mono)", fontSize: 12, lineHeight: 1.55, minWidth: 0 }}>
{lines.map((line, i) => {
const kind =
line.startsWith("@@") ? "hunk" :
line.startsWith("+") && !line.startsWith("+++") ? "added" :
line.startsWith("-") && !line.startsWith("---") ? "removed" :
"context";
const bg =
kind === "added" ? "color-mix(in srgb, var(--status-success) 12%, transparent)" :
kind === "removed" ? "color-mix(in srgb, var(--status-error) 13%, transparent)" :
kind === "hunk" ? "color-mix(in srgb, var(--accent) 12%, transparent)" :
"transparent";
const color =
kind === "added" ? "var(--status-success)" :
kind === "removed" ? "var(--status-error)" :
kind === "hunk" ? "var(--accent)" :
"var(--text)";
return (
<div
key={i}
style={{
display: "flex",
background: bg,
borderLeft: kind === "added"
? "3px solid var(--status-success)"
: kind === "removed"
? "3px solid var(--status-error)"
: kind === "hunk"
? "3px solid var(--accent)"
: "3px solid transparent",
}}
>
<span
style={{
width: 48,
padding: "0 8px",
color: "var(--text-dim)",
background: "var(--bg-panel)",
borderRight: "1px solid var(--border)",
textAlign: "right",
userSelect: "none",
flexShrink: 0,
}}
>
{i + 1}
</span>
<span style={{ padding: "0 10px", whiteSpace: "pre-wrap", overflowWrap: "anywhere", color }}>
{line || "\u00a0"}
</span>
</div>
);
})}
</div>
);
}
export function PairedResult({ text, isEmpty, isError }: {
text: string;
isEmpty: boolean;
isError: boolean;
}) {
const { t } = useI18n();
return (
<div className={`tool-call-output${isError ? " tool-call-output-error" : ""}`}>
<pre className="tool-call-output-text" data-tool-output="true">
{isEmpty ? t("messageView.noOutput") : text}
</pre>
</div>
);
}