v1.1.0: native diff card client, 200-line paging for read/grep/glob, guard rule against shell edits/reads/searches
This commit is contained in:
1 parent
71953ffb8f
commit
0dae0ee53d
5 files changed
+364
-22
No files matched your search
@@ -0,0 +1,115 @@
|
||||
// omp-edit Web client: render hashline / patch edits as DSH's native diff card.
|
||||
//
|
||||
// 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
|
||||
// hunks the host already persists in `meta.diffs` with the shared DiffBlock primitive.
|
||||
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 CSS = `
|
||||
.ompe-row{display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
.ompe-head{display:flex;align-items:center;gap:6px;min-width:0;height:24px;padding:0 4px;border:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-radius:var(--dsw-radius-sm);text-align:left}
|
||||
.ompe-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.ompe-title{color:var(--dsw-alias-label-primary);flex:none}
|
||||
.ompe-sum{color:var(--dsw-alias-label-caption);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.ompe-add{color:var(--dsw-alias-state-success,#3fb950);flex:none}
|
||||
.ompe-del{color:var(--dsw-alias-state-danger,#f85149);flex:none}
|
||||
.ompe-err{color:var(--dsw-alias-state-danger,#f85149)}
|
||||
.ompe-text{margin:0;padding:8px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-l2,rgba(127,127,127,.08));white-space:pre-wrap;font:12px/1.5 var(--dsw-font-mono,ui-monospace,monospace);color:var(--dsw-alias-label-secondary);max-height:320px;overflow:auto}
|
||||
`;
|
||||
let cssDone = false;
|
||||
const installCss = () => {
|
||||
if (cssDone || typeof document === 'undefined') return;
|
||||
const s = document.createElement('style');
|
||||
s.dataset.ompEdit = '';
|
||||
s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
cssDone = true;
|
||||
};
|
||||
|
||||
const Icon = P.IconEditOutlineRegular || (() => null);
|
||||
const Chevron = P.IconChevronRightOutlineRegular || (() => null);
|
||||
|
||||
function readDiffs(meta) {
|
||||
const d = meta && typeof meta === 'object' && Array.isArray(meta.diffs) ? meta.diffs : [];
|
||||
return d.filter((x) => x && typeof x.path === 'string' && typeof x.newText === 'string' && (x.oldText === null || typeof x.oldText === 'string'));
|
||||
}
|
||||
|
||||
function countLines(diffs) {
|
||||
// Approximate +/- counts from hunk texts (context lines are shared by both sides).
|
||||
let add = 0; let del = 0;
|
||||
for (const d of diffs) {
|
||||
const a = d.oldText === null ? [] : d.oldText.split('\n');
|
||||
const b = d.newText.split('\n');
|
||||
const common = new Set(a.filter((l) => b.includes(l)));
|
||||
add += b.filter((l) => !common.has(l)).length;
|
||||
del += a.filter((l) => !common.has(l)).length;
|
||||
}
|
||||
return { add, del };
|
||||
}
|
||||
|
||||
function argsSummary(argsRaw) {
|
||||
try {
|
||||
const a = JSON.parse(argsRaw || '{}');
|
||||
if (typeof a.file_path === 'string') return a.file_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(', ');
|
||||
}
|
||||
} catch { /* partial arguments */ }
|
||||
return '';
|
||||
}
|
||||
|
||||
const labels = (t) => ({
|
||||
codeLabel: t('code') || 'Code', wrapLabel: t('wrap') || 'Wrap', unwrapLabel: t('unwrap') || 'No wrap',
|
||||
copy: t('copy') || 'Copy', copied: t('copied') || 'Copied',
|
||||
collapseAria: 'Collapse', expandAria: (n) => `Show ${n} more lines`, collapse: t('collapse') || 'Collapse', expand: (n) => `Show ${n} more lines`,
|
||||
});
|
||||
|
||||
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 argsRaw = block?.call?.argsRaw ?? block?.argsRaw ?? '';
|
||||
const files = diffs.length ? [...new Set(diffs.map((d) => d.path))].join(', ') : argsSummary(argsRaw);
|
||||
const { add, del } = countLines(diffs);
|
||||
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', { style: { transform: open ? 'rotate(90deg)' : 'none', transition: 'transform .12s', display: 'inline-flex' } }, h(Chevron, { size: 12 })),
|
||||
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 && diffs.length && P.DiffBlock
|
||||
? h(P.DiffBlock, { diffs, labels: labels(typeof t === 'function' ? t : () => ''), maxLines: 24 })
|
||||
: open && done && text
|
||||
? h('pre', { className: `ompe-text${block.isError ? ' ompe-err' : ''}` }, text)
|
||||
: null);
|
||||
}
|
||||
|
||||
return {
|
||||
inject: ['slots'],
|
||||
apply(ctx) {
|
||||
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
||||
name: 'tool.call.toolview',
|
||||
key: 'edit',
|
||||
locale: 'conversation',
|
||||
priority: -1,
|
||||
}, EditRow));
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user