// omp-edit Web client: render hashline / patch edits as a DSH-native diff card with line numbers. // // 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 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). 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: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} `; 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); 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')); } /** Hunk → numbered rows. Uses host-provided rows/starts; falls back to a naive old/new split. */ function hunkRows(d) { const rows = Array.isArray(d.rows) ? d.rows : [ ...(d.oldText === null ? [] : d.oldText.split('\n').map((t) => ['-', t])), ...d.newText.split('\n').map((t) => ['+', t]), ]; const numbered = Number.isInteger(d.oldStart) && Number.isInteger(d.newStart); let o = d.oldStart; let n = d.newStart; return rows.map(([op, text]) => { const row = { kind: op === '+' ? 'add' : op === '-' ? 'del' : 'ctx', text, old: '', neu: '' }; if (numbered) { if (op !== '+') row.old = String(o++); if (op !== '-') row.neu = String(n++); } return row; }); } 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: '' }); 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 }); 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 ''; } 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'); } function DiffCard({ rows, files, t }) { const [wrap, setWrap] = 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))))); } 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); } 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)); }, }; }, });