v1.2.1: edit toolview uses DSH's native ToolRow/DiffBlock chrome (DisclosureRow, collapsed by default, file link, hover diff stat, Output card on error, code-card header with icon actions, 9-row fold); keeps line-number gutter; client render smoke test
This commit is contained in:
1 parent
26dabbba88
commit
ee78e6d3a8
4 files changed
+389
-105
No files matched your search
@@ -1,70 +1,126 @@
|
||||
// omp-edit Web client: render hashline / patch edits as a DSH-native diff card with line numbers.
|
||||
// omp-edit Web client: render hashline / patch edits with DSH's own tool-row chrome.
|
||||
//
|
||||
// The stock `edit` row derives its diff from `file_path` + `old_string`/`new_string` arguments.
|
||||
// Hashline, sloppy and apply_patch calls carry one `input` string instead, so the stock row falls
|
||||
// back to raw text. This keyed toolview (priority -1 shadows the stock one) renders the applied
|
||||
// back to raw JSON. This keyed toolview (priority -1 shadows the stock one) renders the applied
|
||||
// hunks the host persists in `meta.diffs`.
|
||||
//
|
||||
// Styling uses only DSH theme tokens (the same ones DiffBlock.module.css uses: --dsw-alias-code-
|
||||
// diff-added/-deleted, --dsw-alias-state-success-/error-primary, --dsw-font-markdown-code-block…)
|
||||
// so it follows every theme. Each row shows its old/new file line numbers in a gutter, from the
|
||||
// `oldStart`/`newStart`/`rows` the host attaches to every hunk (older results without them fall
|
||||
// back to un-numbered rows).
|
||||
// It is assembled from the same parts as the stock ToolRow + DiffBlock so it lines up with every
|
||||
// other tool row: the DisclosureRow / TextShimmer / Tooltip primitives and icons, the turn-scoped
|
||||
// `useDisclosure` open state (collapsed by default), the dotted file link that opens the file,
|
||||
// the small `+N -M` stat that only takes color on hover/expand, the red first-line error summary
|
||||
// with an Output card, and a code card whose rules are copied from CodeCard/DiffBlock.module.css
|
||||
// (language header with icon actions, 22px rows, inset +/- bar, head/tail fold at 9 rows).
|
||||
// The one addition is the old/new line-number gutter, from the `oldStart`/`newStart`/`rows` the
|
||||
// host attaches to every hunk (older results without them render un-numbered, like stock).
|
||||
window.__ModuleLoader__.load({
|
||||
id: '@local/omp-edit',
|
||||
factory(require) {
|
||||
const React = require('react');
|
||||
const P = require('@deepseek-ai/dsh-client-ui-primitives');
|
||||
const h = React.createElement;
|
||||
const Fragment = React.Fragment;
|
||||
|
||||
/** Rows shown before the fold, as in the stock chat diff card (CHAT_DIFF_MAX_LINES). */
|
||||
const MAX_LINES = 9;
|
||||
|
||||
// Rules mirror ToolRow.module.css, CodeCard.module.css and DiffBlock.module.css of DSH.
|
||||
const CSS = `
|
||||
.ompe-row{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||
.ompe-head{display:flex;align-items:center;gap:6px;min-width:0;min-height:24px;padding:0 4px;margin:0 -4px;border:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;border-radius:var(--dsw-radius-sm);text-align:left}
|
||||
.ompe-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.ompe-head:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}
|
||||
.ompe-icon{display:inline-flex;flex:none;color:var(--dsw-alias-label-tertiary)}
|
||||
.ompe-title{color:var(--dsw-alias-label-primary);flex:none}
|
||||
.ompe-sum{color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.ompe-add{color:var(--dsw-alias-state-success-primary);flex:none;font-variant-numeric:tabular-nums}
|
||||
.ompe-del{color:var(--dsw-alias-state-error-primary);flex:none;font-variant-numeric:tabular-nums}
|
||||
.ompe-err{color:var(--dsw-alias-state-error-primary)}
|
||||
.ompe-card{border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-alias-bg-l2,var(--dsw-alias-fill-l1));border:1px solid var(--dsw-alias-border-l1,transparent);overflow:hidden}
|
||||
.ompe-tools{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px 0 12px;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
||||
.ompe-tools-path{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.ompe-tbtn{border:0;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;height:24px;padding:0 6px;border-radius:var(--dsw-radius-sm);cursor:pointer}
|
||||
.ompe-tbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
||||
.ompe-body{display:grid;grid-template-columns:auto auto minmax(max-content,1fr);padding:2px 0 8px;font:var(--dsw-font-markdown-code-block);overflow-x:auto;max-height:480px;overflow-y:auto}
|
||||
.ompe-body[data-wrap]{grid-template-columns:auto auto minmax(0,1fr)}
|
||||
.ompe-ln{padding:0 8px 0 10px;text-align:right;color:var(--dsw-alias-label-tertiary);opacity:.7;user-select:none;font-variant-numeric:tabular-nums;white-space:pre}
|
||||
.ompe-ln+.ompe-ln{padding-left:0}
|
||||
.ompe-code{padding:0 16px 0 6px;white-space:pre;min-height:1lh}
|
||||
.ompe-body[data-wrap] .ompe-code{white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
.ompe-code::before{display:inline-block;width:1.5ch;content:' '}
|
||||
.ompe-r-add>.ompe-code::before{content:'+'}
|
||||
.ompe-r-del>.ompe-code::before{content:'-'}
|
||||
.ompe-r{display:contents}
|
||||
.ompe-r-ctx>*{color:var(--dsw-alias-label-secondary)}
|
||||
.ompe-r-add>*{background:var(--dsw-alias-code-diff-added)}
|
||||
.ompe-r-add>.ompe-code{color:var(--dsw-alias-state-success-primary)}
|
||||
.ompe-r-add>.ompe-ln:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-success-primary)}
|
||||
.ompe-r-del>*{background:var(--dsw-alias-code-diff-deleted)}
|
||||
.ompe-r-del>.ompe-code{color:var(--dsw-alias-state-error-primary)}
|
||||
.ompe-r-del>.ompe-ln:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-error-primary)}
|
||||
.ompe-r-path>*{color:var(--dsw-alias-label-secondary);font-weight:500;padding-top:6px}
|
||||
.ompe-r-gap>*{color:var(--dsw-alias-label-tertiary)}
|
||||
.ompe-text{margin:0;padding:8px 12px;font:var(--dsw-font-markdown-code-block);white-space:pre-wrap;color:var(--dsw-alias-label-secondary);max-height:320px;overflow:auto}
|
||||
.ompe-root{display:flex;flex-direction:column}
|
||||
.ompe-leading{flex-shrink:0}
|
||||
.ompe-title{font-weight:400}
|
||||
.ompe-sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}
|
||||
.ompe-summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));flex:auto;overflow:hidden}
|
||||
.ompe-error-summary{color:var(--dsw-alias-state-error-primary)}
|
||||
.ompe-stopped-summary{color:var(--dsw-alias-state-warn-label)}
|
||||
.ompe-file{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:inherit;text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:0 auto;margin:0;padding:0;text-decoration:underline 1px dotted;overflow:hidden}
|
||||
.ompe-suffix{white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));flex:none;margin-left:4px}
|
||||
.ompe-stat{font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);margin-left:10px;transform:translateY(.5px)}
|
||||
.ompe-row:is(:hover,[aria-expanded=true]) .ompe-added{color:var(--dsw-alias-state-success-primary)}
|
||||
.ompe-row:is(:hover,[aria-expanded=true]) .ompe-removed{color:var(--dsw-alias-state-error-primary)}
|
||||
.ompe-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}
|
||||
.ompe-body-wrap{flex-direction:column;display:flex}
|
||||
.ompe-inspect{border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}
|
||||
.ompe-root:hover .ompe-inspect,.ompe-inspect:focus-visible{opacity:1}
|
||||
.ompe-inspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}
|
||||
.ompe-io-card{border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);flex-direction:column;margin:4px 0 4px 4px;display:flex}
|
||||
.ompe-io-section{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}
|
||||
.ompe-io-label{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}
|
||||
.ompe-io-text{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}
|
||||
.ompe-io-text[data-error]{color:var(--dsw-alias-state-error-primary)}
|
||||
.ompe-card{position:relative;margin:4px 0 4px 4px;overflow:hidden;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-markdown-code-block);border-radius:var(--dsw-radius-lg);font:var(--dsw-font-markdown-code-block)}
|
||||
.ompe-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 18px 8px 22px;background:var(--dsl-code-block-background,var(--dsw-alias-markdown-code-block));border-top-left-radius:inherit;border-top-right-radius:inherit;color:var(--dsw-alias-label-secondary);font:11px/18px var(--dsw-font-family)}
|
||||
.ompe-card-heading{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.ompe-card-lang{color:var(--dsw-alias-label-tertiary);flex:none;font-family:var(--ds-font-family-code)}
|
||||
.ompe-card-actions{display:flex;align-items:center;gap:4px;flex:none}
|
||||
.ompe-card-action{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-secondary);background:transparent;cursor:pointer}
|
||||
.ompe-card-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.ompe-card-action:focus-visible{outline:1px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
||||
.ompe-body{display:grid;grid-template-columns:minmax(max-content,1fr);padding:6px 0 20px;font:var(--dsw-font-markdown-code-block);overflow-x:auto}
|
||||
.ompe-body[data-numbered]{grid-template-columns:auto auto minmax(max-content,1fr)}
|
||||
.ompe-card[data-code-wrap=true] .ompe-body{grid-template-columns:minmax(0,1fr)}
|
||||
.ompe-card[data-code-wrap=true] .ompe-body[data-numbered]{grid-template-columns:auto auto minmax(0,1fr)}
|
||||
.ompe-l{display:contents}
|
||||
.ompe-full,.ompe-ln,.ompe-code{box-sizing:border-box;min-height:1lh;white-space:pre}
|
||||
.ompe-full{grid-column:1/-1;padding:0 22px}
|
||||
.ompe-code{padding:0 22px}
|
||||
.ompe-body[data-numbered] .ompe-code{padding-left:12px}
|
||||
.ompe-ln{padding:0 0 0 12px;text-align:right;font-variant-numeric:tabular-nums;user-select:none}
|
||||
.ompe-ln:first-child{padding-left:22px}
|
||||
.ompe-card[data-code-wrap=true] .ompe-code,.ompe-card[data-code-wrap=true] .ompe-full{white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
.ompe-path{color:var(--dsw-alias-label-secondary);font-weight:500}
|
||||
.ompe-gap{color:var(--dsw-alias-label-tertiary)}
|
||||
.ompe-del>*{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-code-diff-deleted)}
|
||||
.ompe-del>:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-error-primary)}
|
||||
.ompe-del>.ompe-code::before{content:'- '}
|
||||
.ompe-add>*{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-code-diff-added)}
|
||||
.ompe-add>:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-success-primary)}
|
||||
.ompe-add>.ompe-code::before{content:'+ '}
|
||||
.ompe-ctx>*{color:var(--dsw-alias-label-secondary)}
|
||||
.ompe-ctx>.ompe-code::before{content:' '}
|
||||
.ompe-l>.ompe-ln{color:var(--dsw-alias-label-tertiary)}
|
||||
.ompe-expand{grid-column:1/-1;display:block;width:100%;padding:0 22px;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;text-align:left}
|
||||
.ompe-expand:hover{color:var(--dsw-alias-label-secondary)}
|
||||
`;
|
||||
let cssDone = false;
|
||||
const installCss = () => {
|
||||
if (cssDone || typeof document === 'undefined') return;
|
||||
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="@local/omp-edit"]')) {
|
||||
const s = document.createElement('style');
|
||||
s.dataset.ompEdit = '';
|
||||
s.dataset.plugin = '@local/omp-edit';
|
||||
s.dataset.pluginCss = '@local/omp-edit';
|
||||
s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
cssDone = true;
|
||||
};
|
||||
}
|
||||
|
||||
const Icon = P.IconEditOutlineRegular || (() => null);
|
||||
const none = () => null;
|
||||
const IconEdit = P.IconEditOutlineRegular || none;
|
||||
const IconWrap = P.IconWrapFillRegular || none;
|
||||
const IconNowrap = P.IconNowrapFillRegular || none;
|
||||
const IconCopy = P.IconCopyOutlineRegular || none;
|
||||
const IconCheck = P.IconCheckOutlineRegular || none;
|
||||
const IconInspect = P.IconInspectOutlineRegular || null;
|
||||
const Tooltip = P.Tooltip || (({ children }) => children);
|
||||
const Shimmer = P.TextShimmer || (({ children, className }) => h('span', { className }, children));
|
||||
const EDIT_ICON = h(IconEdit, { size: 14 });
|
||||
|
||||
const cls = (...xs) => xs.filter(Boolean).join(' ');
|
||||
/** Locale lookup with a fallback for keys a DSH build may lack. */
|
||||
const tr = (t, key, fallback, params) => {
|
||||
const v = typeof t === 'function' ? t(key, params) : undefined;
|
||||
return typeof v === 'string' && v !== '' && v !== key ? v : fallback;
|
||||
};
|
||||
const firstLine = (s) => { const i = s.indexOf('\n'); return i === -1 ? s : s.slice(0, i); };
|
||||
|
||||
function displayPath(p, cwd, home) {
|
||||
let out = p;
|
||||
if (cwd) {
|
||||
const root = cwd.replace(/[/\\]+$/, '');
|
||||
if (out.startsWith(`${root}/`)) out = out.slice(root.length + 1);
|
||||
}
|
||||
if (home) {
|
||||
const root = home.replace(/\/+$/, '');
|
||||
if (root && root !== '/' && out.startsWith(`${root}/`)) out = `~${out.slice(root.length)}`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readDiffs(meta) {
|
||||
const d = meta && typeof meta === 'object' && Array.isArray(meta.diffs) ? meta.diffs : [];
|
||||
@@ -89,82 +145,199 @@ window.__ModuleLoader__.load({
|
||||
});
|
||||
}
|
||||
|
||||
/** Flatten hunks like DiffBlock: a path header per new file, `⋯` between same-file hunks. */
|
||||
function buildRows(diffs) {
|
||||
const out = [];
|
||||
let prev;
|
||||
for (const d of diffs) {
|
||||
if (d.path !== prev) out.push({ kind: 'path', text: d.path, old: '', neu: '' });
|
||||
else out.push({ kind: 'gap', text: '⋯', old: '', neu: '' });
|
||||
out.push(d.path !== prev ? { kind: 'path', text: d.path } : { kind: 'gap', text: '⋯' });
|
||||
prev = d.path;
|
||||
out.push(...hunkRows(d));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const totals = (rows) => rows.reduce((a, r) => ({ add: a.add + (r.kind === 'add'), del: a.del + (r.kind === 'del') }), { add: 0, del: 0 });
|
||||
const totals = (rows) => rows.reduce((a, r) => ({ added: a.added + (r.kind === 'add'), removed: a.removed + (r.kind === 'del') }), { added: 0, removed: 0 });
|
||||
|
||||
function argsSummary(argsRaw) {
|
||||
/** Files named by the call arguments (hashline headers, apply_patch markers, or file_path). */
|
||||
function argFiles(argsRaw) {
|
||||
try {
|
||||
const a = JSON.parse(argsRaw || '{}');
|
||||
if (typeof a.file_path === 'string') return a.file_path;
|
||||
if (typeof a.file_path === 'string') return [a.file_path];
|
||||
if (typeof a.path === 'string') return [a.path];
|
||||
if (typeof a.input === 'string') {
|
||||
const files = [...a.input.matchAll(/^\[(.+?)#[0-9A-Fa-f]{4}\]\s*$/gm)].map((m) => m[1]);
|
||||
const more = [...a.input.matchAll(/^\*\*\* (?:Edit|Update|Add|Delete) File:\s*(.+?)(?:\s+all)?\s*$/gm)].map((m) => m[1]);
|
||||
return [...new Set([...files, ...more])].join(', ');
|
||||
return [...new Set([...files, ...more])];
|
||||
}
|
||||
} catch { /* partial arguments */ }
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
|
||||
function copyText(rows) {
|
||||
return rows.map((r) => (r.kind === 'add' ? `+ ${r.text}` : r.kind === 'del' ? `- ${r.text}` : r.kind === 'ctx' ? ` ${r.text}` : r.text)).join('\n');
|
||||
/** Mirrors the stock resultText: text parts, other blocks as JSON, else the error name/code. */
|
||||
function resultText(block) {
|
||||
const parts = [];
|
||||
for (const c of Array.isArray(block.content) ? block.content : []) parts.push(c.type === 'text' ? c.text : JSON.stringify(c, null, 2));
|
||||
if (!parts.length && block.error) parts.push(`${block.error.name}: ${block.error.code}`);
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function DiffCard({ rows, files, t }) {
|
||||
const [wrap, setWrap] = React.useState(false);
|
||||
const copyText = (rows) => rows.map((r) => (r.kind === 'add' ? `+ ${r.text}` : r.kind === 'del' ? `- ${r.text}` : r.kind === 'ctx' ? ` ${r.text}` : r.text)).join('\n');
|
||||
|
||||
function DiffCard({ rows, paths, t }) {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const onCopy = () => {
|
||||
navigator.clipboard?.writeText(copyText(rows)).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1000); }).catch(() => {});
|
||||
};
|
||||
const tr = (k, fb) => (typeof t === 'function' && t(k)) || fb;
|
||||
return h('div', { className: 'ompe-card', 'data-diff': '' },
|
||||
h('div', { className: 'ompe-tools' },
|
||||
h('span', { className: 'ompe-tools-path' }, files),
|
||||
h('button', { type: 'button', className: 'ompe-tbtn', onClick: () => setWrap((v) => !v) }, wrap ? tr('code.unwrap', 'No wrap') : tr('code.wrap', 'Wrap')),
|
||||
h('button', { type: 'button', className: 'ompe-tbtn', onClick: onCopy }, copied ? tr('copied', 'Copied') : tr('copy', 'Copy'))),
|
||||
h('div', { className: 'ompe-body', 'data-wrap': wrap ? '' : undefined },
|
||||
rows.map((r, i) => h('div', { key: i, className: `ompe-r ompe-r-${r.kind}` },
|
||||
h('span', { className: 'ompe-ln' }, r.old),
|
||||
h('span', { className: 'ompe-ln' }, r.neu),
|
||||
h('span', { className: 'ompe-code' }, r.text)))));
|
||||
const [wrapped, setWrapped] = React.useState(false);
|
||||
const langs = paths.map((p) => (typeof P.languageForPath === 'function' ? P.languageForPath(p) : undefined));
|
||||
const lang = langs.every((l) => l === langs[0]) ? langs[0] : undefined;
|
||||
const numbered = rows.some((r) => r.old || r.neu);
|
||||
const onCopy = React.useCallback(() => {
|
||||
if (copied) return;
|
||||
const text = copyText(rows);
|
||||
const write = typeof P.writeClipboard === 'function'
|
||||
? P.writeClipboard(text)
|
||||
: (navigator.clipboard?.writeText(text) ?? Promise.reject()).then(() => true);
|
||||
Promise.resolve(write).then((ok) => {
|
||||
if (!ok) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1000);
|
||||
}).catch(() => {});
|
||||
}, [copied, rows]);
|
||||
|
||||
const hidden = rows.length - MAX_LINES;
|
||||
const capped = hidden > 0 && !expanded;
|
||||
const headN = Math.ceil(MAX_LINES / 2);
|
||||
const head = capped ? rows.slice(0, headN) : rows;
|
||||
const tail = capped ? rows.slice(rows.length - (MAX_LINES - headN)) : [];
|
||||
const line = (prefix) => (r, i) => (r.kind === 'path' || r.kind === 'gap'
|
||||
? h('div', { key: prefix + i, className: `ompe-full ompe-${r.kind}` }, r.text)
|
||||
: h('div', { key: prefix + i, className: `ompe-l ompe-${r.kind}` },
|
||||
numbered ? h('span', { className: 'ompe-ln' }, r.old) : null,
|
||||
numbered ? h('span', { className: 'ompe-ln' }, r.neu) : null,
|
||||
h('span', { className: 'ompe-code' }, r.text)));
|
||||
|
||||
const wrapLabel = tr(t, 'codeBlock.wrap', 'Wrap');
|
||||
const unwrapLabel = tr(t, 'codeBlock.unwrap', 'No wrap');
|
||||
const copyLabel = copied ? tr(t, 'copied', 'Copied') : tr(t, 'copy', 'Copy');
|
||||
return h('div', { className: 'ompe-card', 'data-diff': '', 'data-code-wrap': wrapped },
|
||||
h('div', { className: 'ompe-card-head', 'data-code-block-banner': true },
|
||||
h('div', { className: 'ompe-card-heading' },
|
||||
h('span', { className: 'ompe-card-lang' }, lang || tr(t, 'codeBlock.title', 'Code'))),
|
||||
h('div', { className: 'ompe-card-actions' },
|
||||
h(Tooltip, { label: wrapped ? unwrapLabel : wrapLabel, side: 'top', portal: true },
|
||||
h('button', { type: 'button', className: 'ompe-card-action', 'aria-label': wrapLabel, 'aria-pressed': wrapped, onClick: () => setWrapped((v) => !v) },
|
||||
h(wrapped ? IconNowrap : IconWrap, { size: 14 }))),
|
||||
h(Tooltip, { label: copyLabel, side: 'top', portal: true },
|
||||
h('button', { type: 'button', className: 'ompe-card-action', 'aria-label': copyLabel, onClick: onCopy },
|
||||
h(copied ? IconCheck : IconCopy, { size: 14 }))))),
|
||||
h('div', { className: 'ompe-body', 'data-numbered': numbered ? '' : undefined },
|
||||
head.map(line('h')),
|
||||
hidden > 0 ? h('button', {
|
||||
type: 'button',
|
||||
className: 'ompe-expand',
|
||||
'aria-expanded': expanded,
|
||||
'aria-label': expanded ? tr(t, 'diff.collapseAria', 'Collapse diff') : tr(t, 'diff.expandAria', `Show ${hidden} more lines`, { count: hidden }),
|
||||
onClick: () => setExpanded((v) => !v),
|
||||
}, expanded ? tr(t, 'collapse', 'Collapse') : tr(t, 'diff.expandRest', `⋯ ${hidden} more lines`, { count: hidden })) : null,
|
||||
tail.map(line('t'))));
|
||||
}
|
||||
|
||||
function useLocalDisclosure() {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const toggle = React.useCallback(() => setExpanded((v) => !v), []);
|
||||
return { expanded, setExpanded, toggle };
|
||||
}
|
||||
|
||||
const stopKeys = (e) => { if (e.key === 'Enter' || e.key === ' ') e.stopPropagation(); };
|
||||
|
||||
/** The shared row: DisclosureRow + summary/stat chrome exactly like the stock ToolRow. */
|
||||
function Row({ props, state, summary, filePath, stat, body }) {
|
||||
const { t, toolName, openFile, inspect } = props;
|
||||
const disclosure = (typeof props.useDisclosure === 'function' ? props.useDisclosure : useLocalDisclosure)();
|
||||
const expandable = state !== 'preparing' && body !== null;
|
||||
const open = disclosure.expanded && expandable;
|
||||
const settledWithCue = state === 'error' || state === 'stopped';
|
||||
const onOpen = React.useMemo(() => (filePath && typeof openFile === 'function' && !settledWithCue
|
||||
? (e) => { e.stopPropagation(); openFile(filePath); } : undefined), [filePath, openFile, settledWithCue]);
|
||||
const suffix = settledWithCue ? null : stat;
|
||||
const status = state === 'preparing' ? tr(t, 'row.preparing', '')
|
||||
: state === 'running' ? tr(t, 'row.running', '')
|
||||
: state === 'error' ? tr(t, 'row.failed', '')
|
||||
: state === 'stopped' ? tr(t, 'row.stopped', '') : '';
|
||||
|
||||
const collapsed = summary !== '' ? h(Fragment, null,
|
||||
h('span', { className: 'ompe-sep', 'data-shimmer-decoration': true, 'aria-hidden': true }),
|
||||
onOpen
|
||||
? h('button', { type: 'button', className: 'ompe-file', onClick: onOpen, onKeyDown: stopKeys }, h(Shimmer, null, summary))
|
||||
: h('span', { className: cls('ompe-summary', state === 'error' && 'ompe-error-summary', state === 'stopped' && 'ompe-stopped-summary') }, h(Shimmer, null, summary)),
|
||||
suffix ? h(Shimmer, { className: 'ompe-suffix ompe-stat' }, h(Fragment, null,
|
||||
h('span', { className: 'ompe-added' }, `+${suffix.added}`), ' ',
|
||||
h('span', { className: 'ompe-removed' }, `-${suffix.removed}`))) : null) : null;
|
||||
|
||||
const expanded = open ? h('div', { className: 'ompe-body-wrap' },
|
||||
body,
|
||||
typeof inspect === 'function' ? h('button', { type: 'button', className: 'ompe-inspect', onClick: inspect },
|
||||
IconInspect ? h(IconInspect, {}) : null, tr(t, 'row.inspect', 'Inspect')) : null) : undefined;
|
||||
|
||||
const title = tr(t, 'tool.title.edit', 'Edit');
|
||||
const D = P.DisclosureRow;
|
||||
return h('div', { className: 'ompe-root', 'data-variant': 'edit', 'data-tool': toolName, 'data-state': state },
|
||||
status ? h('span', { className: 'ompe-hidden' }, status) : null,
|
||||
D
|
||||
? h(D, {
|
||||
rowClassName: 'ompe-row',
|
||||
leadingClassName: 'ompe-leading',
|
||||
titleClassName: 'ompe-title',
|
||||
icon: EDIT_ICON,
|
||||
title,
|
||||
running: state === 'running' || state === 'preparing',
|
||||
open,
|
||||
expandable,
|
||||
expandOnRowClick: true,
|
||||
keepContentWhenOpen: true,
|
||||
onToggle: disclosure.toggle,
|
||||
collapsedContent: collapsed,
|
||||
children: expanded,
|
||||
})
|
||||
: h(Fragment, null,
|
||||
h('div', { className: 'ompe-row', role: 'button', tabIndex: 0, 'aria-expanded': open, onClick: disclosure.toggle, style: { display: 'flex', alignItems: 'center' } },
|
||||
h('span', { className: 'ompe-leading' }, EDIT_ICON), h('span', { className: 'ompe-title' }, title), collapsed),
|
||||
expanded));
|
||||
}
|
||||
|
||||
function PreparingEditRow(props) {
|
||||
const raw = typeof props.useToolCallArgumentsPartial === 'function' ? props.useToolCallArgumentsPartial() : '';
|
||||
const summary = raw ? tr(props.t, 'tool.preparing.content', '', { kilobytes: Math.ceil(raw.length / 1024) }) : '';
|
||||
return h(Row, { props, state: 'preparing', summary, filePath: undefined, stat: null, body: null });
|
||||
}
|
||||
|
||||
function StartedEditRow(props) {
|
||||
const { block, cwd, home, t } = props;
|
||||
const done = !!block && 'kind' in block;
|
||||
const state = !done ? 'running' : block.error?.code === 'interrupted' ? 'stopped' : block.isError ? 'error' : 'ok';
|
||||
const diffs = React.useMemo(() => (done && !block.isError ? readDiffs(block.meta) : []), [done, block]);
|
||||
const rows = React.useMemo(() => buildRows(diffs), [diffs]);
|
||||
const argsRaw = (done ? block.call?.argsRaw : block?.argsRaw) ?? '';
|
||||
const paths = diffs.length ? [...new Set(diffs.map((d) => d.path))] : argFiles(argsRaw);
|
||||
const output = done ? resultText(block) || null : null;
|
||||
const base = paths.map((p) => displayPath(p, cwd, home)).join(', ');
|
||||
const summary = state === 'error' && output !== null ? firstLine(output) : base;
|
||||
const stat = React.useMemo(() => (rows.length ? totals(rows) : null), [rows]);
|
||||
const body = rows.length
|
||||
? h(DiffCard, { rows, paths, t })
|
||||
: output !== null
|
||||
? h('div', { className: 'ompe-io-card' },
|
||||
h('div', { className: 'ompe-io-section' },
|
||||
h('span', { className: 'ompe-io-label' }, tr(t, 'row.output', 'Output')),
|
||||
h('span', { className: 'ompe-io-text', 'data-error': state === 'error' || undefined }, output)))
|
||||
: null;
|
||||
return h(Row, { props, state, summary, filePath: paths.length === 1 ? paths[0] : undefined, stat, body });
|
||||
}
|
||||
|
||||
/** Split by phase like the stock FileMutationRow so hook order never changes within one component. */
|
||||
function EditRow(props) {
|
||||
const { block, t, phase } = props;
|
||||
React.useEffect(installCss, []);
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const done = block && 'kind' in block;
|
||||
const diffs = done && !block.isError ? readDiffs(block.meta) : [];
|
||||
const rows = React.useMemo(() => buildRows(diffs), [block?.meta]);
|
||||
const argsRaw = block?.call?.argsRaw ?? block?.argsRaw ?? '';
|
||||
const files = diffs.length ? [...new Set(diffs.map((d) => d.path))].join(', ') : argsSummary(argsRaw);
|
||||
const { add, del } = totals(rows);
|
||||
const text = done && Array.isArray(block.content) ? block.content.filter((c) => c.type === 'text').map((c) => c.text).join('\n') : '';
|
||||
const running = !done || phase === 'preparing';
|
||||
const title = (typeof t === 'function' && t('tool.title.edit')) || 'Edit';
|
||||
return h('div', { className: 'ompe-row' },
|
||||
h('button', { type: 'button', className: 'ompe-head', onClick: () => setOpen((v) => !v), 'aria-expanded': open },
|
||||
h('span', { className: 'ompe-icon' }, h(Icon, { size: 14 })),
|
||||
h('span', { className: 'ompe-title' }, running ? `${title}…` : title),
|
||||
files ? h('span', { className: 'ompe-sum' }, files) : null,
|
||||
diffs.length ? h('span', { className: 'ompe-add' }, `+${add}`) : null,
|
||||
diffs.length ? h('span', { className: 'ompe-del' }, `-${del}`) : null,
|
||||
done && block.isError ? h('span', { className: 'ompe-err' }, '✕') : null),
|
||||
open && rows.length
|
||||
? h(DiffCard, { rows, files, t })
|
||||
: open && done && text
|
||||
? h('div', { className: 'ompe-card' }, h('pre', { className: `ompe-text${block.isError ? ' ompe-err' : ''}` }, text))
|
||||
: null);
|
||||
const preparing = props.phase === 'preparing' || (props.block && !('kind' in props.block) && props.block.phase === 'preparing');
|
||||
return preparing ? h(PreparingEditRow, props) : h(StartedEditRow, props);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user