Files
omp-edit/client.js
T

412 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 JSON. This keyed toolview (priority -1 shadows the stock one) renders the applied
// hunks the host persists in `meta.diffs`.
//
// 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-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)}
`;
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="@local/omp-edit"]')) {
const s = document.createElement('style');
s.dataset.plugin = '@local/omp-edit';
s.dataset.pluginCss = '@local/omp-edit';
s.textContent = CSS;
document.head.appendChild(s);
}
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 : [];
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;
});
}
/** 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) {
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) => ({ added: a.added + (r.kind === 'add'), removed: a.removed + (r.kind === 'del') }), { added: 0, removed: 0 });
/** 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.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])];
}
} catch { /* partial arguments */ }
return [];
}
/** 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');
}
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 [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 preparing = props.phase === 'preparing' || (props.block && !('kind' in props.block) && props.block.phase === 'preparing');
return preparing ? h(PreparingEditRow, props) : h(StartedEditRow, props);
}
/** `[first, last]` file lines a read call covers: from the result window once settled, else from the arguments. */
function readRange(block) {
if (!block) return null;
const done = 'kind' in block;
if (done && block.isError) return null;
const lines = done && block.meta && Array.isArray(block.meta.lines) ? block.meta.lines : null;
if (lines) {
if (!lines.length) return null;
const a = lines[0]?.number; const b = lines[lines.length - 1]?.number;
return Number.isInteger(a) && Number.isInteger(b) ? [a, b] : null;
}
let args = {};
try { args = JSON.parse((done ? block.call?.argsRaw : block.argsRaw) || '{}') || {}; } catch { return null; }
const pos = (v) => (Number.isInteger(v) && v >= 1 ? v : undefined);
const start = pos(args.offset) ?? 1;
const limit = Math.min(pos(args.limit) ?? PAGE_LINES, PAGE_LINES);
return [start, start + limit - 1];
}
const PAGE_LINES = 200;
/** The stock read row (priority 0 entry of the same slot); looked up at render time so load order doesn't matter. */
let stockRead;
function findStockRead(ctx, self) {
if (stockRead) return stockRead;
try {
const entries = typeof ctx.slots.entries === 'function' ? ctx.slots.entries('tool.call.toolview') : [];
const hit = entries.find((e) => e.options?.key === 'read' && e.component !== self);
if (hit) stockRead = hit.component;
} catch { /* slot service without entry access */ }
return stockRead;
}
/** Stock read row with the title changed to "Read lines A–B". */
function makeReadRow(ctx) {
function ReadRow(props) {
const stock = findStockRead(ctx, ReadRow);
if (!stock) return null;
const range = readRange(props.block);
if (!range) return h(stock, props);
const base = tr(props.t, 'tool.title.read', 'Read');
const title = `${base} lines ${range[0]}–${range[1]}`;
const t = (key, params) => (key === 'tool.title.read' ? title : props.t(key, params));
return h(stock, { ...props, t });
}
return ReadRow;
}
return {
inject: ['slots'],
apply(ctx) {
ctx.slots.inject('tool.call.toolview', function* () {
yield ctx.slots.register({
name: 'tool.call.toolview',
key: 'edit',
locale: 'conversation',
priority: -1,
}, EditRow);
yield ctx.slots.register({
name: 'tool.call.toolview',
key: 'read',
locale: 'conversation',
priority: -1,
}, makeReadRow(ctx));
});
},
};
},
});