mirror of
https://github.com/Heretek-AI/ompweb.git
synced 2026-11-09 18:51:28 -10:00
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.
250 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|