nanobot-voice-interface/examples/cards/templates/git-diff-live/card.js
kacper 4dfb7ca3cc
Some checks failed
CI / Backend Checks (push) Failing after 36s
CI / Frontend Checks (push) Failing after 40s
feat: unify card runtime and event-driven web ui
2026-04-06 15:42:53 -04:00

719 lines
27 KiB
JavaScript

export function mount({ root, state, host }) {
state = state || {};
const __cleanup = [];
const __setInterval = (...args) => {
const id = window.setInterval(...args);
__cleanup.push(() => window.clearInterval(id));
return id;
};
const __setTimeout = (...args) => {
const id = window.setTimeout(...args);
__cleanup.push(() => window.clearTimeout(id));
return id;
};
if (!(root instanceof HTMLElement)) return;
const subtitleEl = root.querySelector('[data-git-subtitle]');
const branchEl = root.querySelector('[data-git-branch]');
const statusEl = root.querySelector('[data-git-status]');
const changedEl = root.querySelector('[data-git-changed]');
const stagingEl = root.querySelector('[data-git-staging]');
const untrackedEl = root.querySelector('[data-git-untracked]');
const upstreamEl = root.querySelector('[data-git-upstream]');
const plusEl = root.querySelector('[data-git-plus]');
const minusEl = root.querySelector('[data-git-minus]');
const updatedEl = root.querySelector('[data-git-updated]');
const filesEl = root.querySelector('[data-git-files]');
if (!(subtitleEl instanceof HTMLElement) || !(branchEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(changedEl instanceof HTMLElement) || !(stagingEl instanceof HTMLElement) || !(untrackedEl instanceof HTMLElement) || !(upstreamEl instanceof HTMLElement) || !(plusEl instanceof HTMLElement) || !(minusEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement) || !(filesEl instanceof HTMLElement)) return;
const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : '';
const rawToolArguments = state && typeof state.tool_arguments === 'object' && state.tool_arguments && !Array.isArray(state.tool_arguments)
? state.tool_arguments
: {};
const subtitle = typeof state.subtitle === 'string' ? state.subtitle.trim() : '';
const numberFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 0 });
const setStatus = (label, fg, bg) => {
statusEl.textContent = label;
statusEl.style.color = fg;
statusEl.style.background = bg;
statusEl.style.padding = '3px 7px';
statusEl.style.borderRadius = '999px';
};
const statusTone = (value) => {
if (value === 'Clean') return { fg: '#6c8b63', bg: '#dfe9d8' };
if (value === 'Dirty') return { fg: '#9a6a2f', bg: '#f4e2b8' };
return { fg: '#a14d43', bg: '#f3d8d2' };
};
const formatBranch = (payload) => {
const parts = [];
const branch = typeof payload.branch === 'string' ? payload.branch : '';
if (branch) parts.push(branch);
if (typeof payload.upstream === 'string' && payload.upstream) {
parts.push(payload.upstream);
}
const ahead = Number(payload.ahead || 0);
const behind = Number(payload.behind || 0);
if (ahead || behind) {
parts.push(`+${ahead} / -${behind}`);
}
if (!parts.length && typeof payload.head === 'string' && payload.head) {
parts.push(payload.head);
}
return parts.join(' · ') || 'No branch information';
};
const formatUpdated = (raw) => {
if (typeof raw !== 'string' || !raw) return '--';
const parsed = new Date(raw);
if (Number.isNaN(parsed.getTime())) return raw;
return parsed.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
const chipStyle = (status) => {
if (status === '??') return { fg: '#6f7582', bg: '#e8edf2' };
if (status.includes('D')) return { fg: '#a45b51', bg: '#f3d7d2' };
if (status.includes('A')) return { fg: '#6d8a5d', bg: '#dce7d6' };
return { fg: '#9a6a2f', bg: '#f3e1ba' };
};
const asLineNumber = (value) => {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && /^\d+$/.test(value)) return Number(value);
return null;
};
const formatRangePart = (label, start, end) => {
if (start === null || end === null) return '';
return start === end ? `${label} ${start}` : `${label} ${start}-${end}`;
};
const buildSelectionPayload = (filePath, lines) => {
const oldNumbers = lines.map((line) => line.oldNumber).filter((value) => value !== null);
const newNumbers = lines.map((line) => line.newNumber).filter((value) => value !== null);
const oldStart = oldNumbers.length ? Math.min(...oldNumbers) : null;
const oldEnd = oldNumbers.length ? Math.max(...oldNumbers) : null;
const newStart = newNumbers.length ? Math.min(...newNumbers) : null;
const newEnd = newNumbers.length ? Math.max(...newNumbers) : null;
const rangeParts = [
formatRangePart('old', oldStart, oldEnd),
formatRangePart('new', newStart, newEnd),
].filter(Boolean);
const fileLabel = filePath || 'Selected diff';
const rangeLabel = rangeParts.join(' · ') || 'Selected diff lines';
return {
kind: 'git_diff_range',
file_path: filePath || fileLabel,
file_label: fileLabel,
range_label: rangeLabel,
label: `${fileLabel} · ${rangeLabel}`,
old_start: oldStart,
old_end: oldEnd,
new_start: newStart,
new_end: newEnd,
};
};
let activeSelectionController = null;
const clearActiveSelection = () => {
if (activeSelectionController) {
const controller = activeSelectionController;
activeSelectionController = null;
controller.clear(false);
}
host.setSelection(null);
};
const renderPatchBody = (target, item) => {
target.innerHTML = '';
target.dataset.noSwipe = '1';
target.style.marginTop = '10px';
target.style.marginLeft = '-12px';
target.style.marginRight = '-12px';
target.style.width = 'calc(100% + 24px)';
target.style.paddingTop = '10px';
target.style.borderTop = '1px solid rgba(177, 140, 112, 0.16)';
target.style.overflow = 'hidden';
target.style.minWidth = '0';
target.style.maxWidth = 'none';
const viewport = document.createElement('div');
viewport.dataset.noSwipe = '1';
viewport.style.width = '100%';
viewport.style.maxWidth = 'none';
viewport.style.minWidth = '0';
viewport.style.overflowX = 'auto';
viewport.style.overflowY = 'hidden';
viewport.style.touchAction = 'auto';
viewport.style.overscrollBehavior = 'contain';
viewport.style.webkitOverflowScrolling = 'touch';
viewport.style.scrollbarWidth = 'thin';
viewport.style.scrollbarColor = 'rgba(120, 94, 74, 0.28) transparent';
const diffText = typeof item?.diff === 'string' ? item.diff : '';
const diffLines = Array.isArray(item?.diff_lines) ? item.diff_lines : [];
if (!diffText && diffLines.length === 0) {
const message = document.createElement('div');
message.textContent = 'No line diff available for this path.';
message.style.fontSize = '0.76rem';
message.style.lineHeight = '1.4';
message.style.color = '#9a7b68';
message.style.fontWeight = '600';
target.appendChild(message);
return;
}
const block = document.createElement('div');
block.dataset.noSwipe = '1';
block.style.display = 'grid';
block.style.gap = '0';
block.style.padding = '0';
block.style.borderRadius = '0';
block.style.background = 'rgba(255,255,255,0.58)';
block.style.border = '1px solid rgba(153, 118, 92, 0.14)';
block.style.borderLeft = '0';
block.style.borderRight = '0';
block.style.fontFamily =
"var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace)";
block.style.fontSize = '0.64rem';
block.style.lineHeight = '1.45';
block.style.color = '#5f4a3f';
block.style.width = 'max-content';
block.style.minWidth = '100%';
const selectableLines = [];
let localSelection = null;
let localAnchorIndex = null;
const setSelectedState = (entry, selected) => {
if (selected) entry.lineEl.dataset.selected = 'true';
else delete entry.lineEl.dataset.selected;
};
const controller = {
clear(publish = true) {
localSelection = null;
localAnchorIndex = null;
for (const entry of selectableLines) setSelectedState(entry, false);
if (publish) {
if (activeSelectionController === controller) activeSelectionController = null;
host.setSelection(null);
}
},
};
const applySelection = (startIndex, endIndex) => {
const lower = Math.min(startIndex, endIndex);
const upper = Math.max(startIndex, endIndex);
localSelection = { startIndex: lower, endIndex: upper };
for (const [index, entry] of selectableLines.entries()) {
setSelectedState(entry, index >= lower && index <= upper);
}
if (activeSelectionController && activeSelectionController !== controller) {
activeSelectionController.clear(false);
}
activeSelectionController = controller;
host.setSelection(buildSelectionPayload(String(item?.path || ''), selectableLines.slice(lower, upper + 1)),
);
};
const handleSelectableLine = (index) => {
if (!localSelection) {
localAnchorIndex = index;
applySelection(index, index);
return;
}
const singleLine = localSelection.startIndex === localSelection.endIndex;
if (singleLine) {
const anchorIndex = localAnchorIndex ?? localSelection.startIndex;
if (index === anchorIndex) {
controller.clear(true);
return;
}
applySelection(anchorIndex, index);
localAnchorIndex = null;
return;
}
localAnchorIndex = index;
applySelection(index, index);
};
const registerSelectableLine = (lineEl, oldNumber, newNumber) => {
const entry = {
lineEl,
oldNumber: asLineNumber(oldNumber),
newNumber: asLineNumber(newNumber),
};
const index = selectableLines.push(entry) - 1;
lineEl.dataset.selectable = 'true';
lineEl.tabIndex = 0;
lineEl.setAttribute('role', 'button');
lineEl.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
handleSelectableLine(index);
});
lineEl.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
handleSelectableLine(index);
});
};
if (diffLines.length > 0) {
for (const row of diffLines) {
const lineEl = document.createElement('div');
lineEl.dataset.gitPatchRow = '1';
lineEl.style.display = 'grid';
lineEl.style.gridTemplateColumns = 'max-content max-content';
lineEl.style.columnGap = '8px';
lineEl.style.alignItems = 'start';
lineEl.style.justifyContent = 'start';
lineEl.style.padding = '0';
lineEl.style.borderRadius = '0';
lineEl.style.width = 'max-content';
lineEl.style.minWidth = '100%';
const numberEl = document.createElement('span');
const lineNumber =
typeof row?.line_number === 'number' || typeof row?.line_number === 'string'
? String(row.line_number)
: '';
numberEl.textContent = lineNumber;
numberEl.style.minWidth = '2.2em';
numberEl.style.textAlign = 'right';
numberEl.style.color = '#8c7464';
numberEl.style.opacity = '0.92';
const textEl = document.createElement('span');
textEl.dataset.gitPatchText = '1';
textEl.textContent = typeof row?.text === 'string' ? row.text : '';
textEl.style.whiteSpace = 'pre';
textEl.style.wordBreak = 'normal';
const kind = typeof row?.kind === 'string' ? row.kind : '';
if (kind === 'added') {
lineEl.style.color = '#0c3f12';
lineEl.style.background = 'rgba(158, 232, 147, 0.98)';
} else if (kind === 'removed') {
lineEl.style.color = '#6d0d08';
lineEl.style.background = 'rgba(249, 156, 145, 0.98)';
} else {
lineEl.style.color = '#5f4a3f';
}
lineEl.append(numberEl, textEl);
block.appendChild(lineEl);
}
} else {
const makePatchLine = (line, kind, oldNumber = '', newNumber = '') => {
const lineEl = document.createElement('div');
lineEl.dataset.gitPatchRow = '1';
lineEl.style.display = 'grid';
lineEl.style.gridTemplateColumns = 'max-content max-content max-content';
lineEl.style.columnGap = '8px';
lineEl.style.alignItems = 'start';
lineEl.style.justifyContent = 'start';
lineEl.style.padding = '0';
lineEl.style.borderRadius = '0';
lineEl.style.width = 'max-content';
lineEl.style.minWidth = '100%';
const oldEl = document.createElement('span');
oldEl.textContent = oldNumber ? String(oldNumber) : '';
oldEl.style.minWidth = '2.4em';
oldEl.style.textAlign = 'right';
oldEl.style.color = '#8c7464';
oldEl.style.opacity = '0.92';
const newEl = document.createElement('span');
newEl.textContent = newNumber ? String(newNumber) : '';
newEl.style.minWidth = '2.4em';
newEl.style.textAlign = 'right';
newEl.style.color = '#8c7464';
newEl.style.opacity = '0.92';
const textEl = document.createElement('span');
textEl.dataset.gitPatchText = '1';
textEl.textContent = line || ' ';
textEl.style.whiteSpace = 'pre';
textEl.style.wordBreak = 'normal';
if (kind === 'hunk') {
lineEl.style.color = '#6c523f';
lineEl.style.background = 'rgba(224, 204, 184, 0.94)';
lineEl.style.fontWeight = '800';
} else if (kind === 'added') {
lineEl.style.color = '#0f4515';
lineEl.style.background = 'rgba(170, 232, 160, 0.98)';
} else if (kind === 'removed') {
lineEl.style.color = '#74110a';
lineEl.style.background = 'rgba(247, 170, 160, 0.98)';
} else if (kind === 'context') {
lineEl.style.color = '#6f5b4d';
lineEl.style.background = 'rgba(247, 236, 223, 0.72)';
} else if (kind === 'meta') {
lineEl.style.color = '#725c4f';
lineEl.style.background = 'rgba(255, 255, 255, 0.42)';
} else if (kind === 'note') {
lineEl.style.color = '#8a6f5c';
lineEl.style.background = 'rgba(236, 226, 216, 0.72)';
lineEl.style.fontStyle = 'italic';
}
lineEl.append(oldEl, newEl, textEl);
if (kind === 'added' || kind === 'removed' || kind === 'context') {
registerSelectableLine(lineEl, oldNumber, newNumber);
}
return lineEl;
};
const parseHunkHeader = (line) => {
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
if (!match) return null;
return {
oldLine: Number(match[1] || '0'),
newLine: Number(match[3] || '0'),
};
};
const prelude = [];
const hunks = [];
let currentHunk = null;
for (const line of diffText.split('\n')) {
if (line.startsWith('@@')) {
if (currentHunk) hunks.push(currentHunk);
currentHunk = { header: line, lines: [] };
continue;
}
if (currentHunk) {
currentHunk.lines.push(line);
} else {
prelude.push(line);
}
}
if (currentHunk) hunks.push(currentHunk);
for (const line of prelude) {
let kind = 'meta';
if (line.startsWith('Binary files') || line.startsWith('\\')) kind = 'note';
else if (line.startsWith('+') && !line.startsWith('+++')) kind = 'added';
else if (line.startsWith('-') && !line.startsWith('---')) kind = 'removed';
else if (line.startsWith(' ')) kind = 'context';
block.appendChild(makePatchLine(line, kind));
}
for (const hunk of hunks) {
const section = document.createElement('section');
section.style.display = 'grid';
section.style.gap = '0';
section.style.marginTop = block.childNodes.length ? '10px' : '0';
section.style.borderTop = '1px solid rgba(177, 140, 112, 0.24)';
section.style.borderBottom = '1px solid rgba(177, 140, 112, 0.24)';
section.appendChild(makePatchLine(hunk.header, 'hunk'));
const parsed = parseHunkHeader(hunk.header);
let oldLine = parsed ? parsed.oldLine : 0;
let newLine = parsed ? parsed.newLine : 0;
for (const line of hunk.lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
section.appendChild(makePatchLine(line, 'added', '', newLine));
newLine += 1;
} else if (line.startsWith('-') && !line.startsWith('---')) {
section.appendChild(makePatchLine(line, 'removed', oldLine, ''));
oldLine += 1;
} else if (line.startsWith('\\')) {
section.appendChild(makePatchLine(line, 'note'));
} else if (line.startsWith('+++') || line.startsWith('---')) {
section.appendChild(makePatchLine(line, 'meta'));
} else {
const oldNumber = oldLine ? oldLine : '';
const newNumber = newLine ? newLine : '';
section.appendChild(makePatchLine(line, 'context', oldNumber, newNumber));
oldLine += 1;
newLine += 1;
}
}
block.appendChild(section);
}
}
viewport.appendChild(block);
target.appendChild(viewport);
if (item?.diff_truncated) {
const note = document.createElement('div');
note.textContent = 'Diff truncated for readability.';
note.style.marginTop = '8px';
note.style.fontSize = '0.72rem';
note.style.lineHeight = '1.35';
note.style.color = '#9a7b68';
note.style.fontWeight = '600';
target.appendChild(note);
}
};
const renderFiles = (items) => {
clearActiveSelection();
filesEl.innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
const empty = document.createElement('div');
empty.textContent = 'Working tree clean.';
empty.style.fontSize = '0.92rem';
empty.style.lineHeight = '1.4';
empty.style.color = '#7d8f73';
empty.style.fontWeight = '700';
empty.style.padding = '12px';
empty.style.borderRadius = '12px';
empty.style.background = 'rgba(223, 233, 216, 0.55)';
empty.style.border = '1px solid rgba(109, 138, 93, 0.12)';
filesEl.appendChild(empty);
return;
}
for (const item of items) {
const row = document.createElement('div');
row.style.display = 'block';
row.style.minWidth = '0';
row.style.maxWidth = '100%';
row.style.padding = '0';
row.style.borderRadius = '0';
row.style.background = 'transparent';
row.style.border = '0';
row.style.boxShadow = 'none';
const summaryButton = document.createElement('button');
summaryButton.type = 'button';
summaryButton.style.display = 'flex';
summaryButton.style.alignItems = 'flex-start';
summaryButton.style.justifyContent = 'space-between';
summaryButton.style.gap = '8px';
summaryButton.style.width = '100%';
summaryButton.style.minWidth = '0';
summaryButton.style.padding = '0';
summaryButton.style.margin = '0';
summaryButton.style.border = '0';
summaryButton.style.background = 'transparent';
summaryButton.style.textAlign = 'left';
summaryButton.style.cursor = 'pointer';
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'flex-start';
left.style.gap = '8px';
left.style.minWidth = '0';
left.style.flex = '1 1 auto';
const chip = document.createElement('span');
const chipTone = chipStyle(String(item?.status || 'M'));
chip.textContent = String(item?.status || 'M');
chip.style.fontSize = '0.72rem';
chip.style.lineHeight = '1.1';
chip.style.fontWeight = '800';
chip.style.color = chipTone.fg;
chip.style.background = chipTone.bg;
chip.style.padding = '4px 7px';
chip.style.borderRadius = '999px';
chip.style.flex = '0 0 auto';
const pathWrap = document.createElement('div');
pathWrap.style.minWidth = '0';
const pathEl = document.createElement('div');
pathEl.textContent = String(item?.path || '--');
pathEl.style.fontSize = '0.92rem';
pathEl.style.lineHeight = '1.3';
pathEl.style.fontWeight = '700';
pathEl.style.color = '#65483a';
pathEl.style.wordBreak = 'break-word';
const detailEl = document.createElement('div');
detailEl.style.marginTop = '3px';
detailEl.style.fontSize = '0.77rem';
detailEl.style.lineHeight = '1.35';
detailEl.style.color = '#9a7b68';
const insertions = Number(item?.insertions || 0);
const deletions = Number(item?.deletions || 0);
detailEl.textContent = insertions || deletions
? `+${numberFormatter.format(insertions)} / -${numberFormatter.format(deletions)}`
: 'No line diff';
pathWrap.append(pathEl, detailEl);
left.append(chip, pathWrap);
const toggle = document.createElement('span');
toggle.setAttribute('aria-hidden', 'true');
toggle.style.fontSize = '0.95rem';
toggle.style.lineHeight = '1';
toggle.style.fontWeight = '800';
toggle.style.color = '#9a7b68';
toggle.style.whiteSpace = 'nowrap';
toggle.style.flex = '0 0 auto';
toggle.style.paddingTop = '1px';
const body = document.createElement('div');
body.hidden = true;
body.style.width = '100%';
body.style.maxWidth = '100%';
body.style.minWidth = '0';
renderPatchBody(body, item);
const hasDiff = Boolean(item?.diff_available) || Boolean(item?.diff);
const setExpanded = (expanded) => {
body.hidden = !expanded;
summaryButton.setAttribute('aria-expanded', expanded ? 'true' : 'false');
toggle.textContent = expanded ? '▴' : '▾';
};
setExpanded(false);
summaryButton.addEventListener('click', () => {
setExpanded(body.hidden);
});
summaryButton.append(left, toggle);
row.append(summaryButton, body);
filesEl.appendChild(row);
}
};
const updateLiveContent = (snapshot) => {
host.setLiveContent(snapshot);
};
const render = (payload) => {
subtitleEl.textContent = subtitle || payload.repo_name || payload.repo_path || 'Git repo';
branchEl.textContent = formatBranch(payload);
changedEl.textContent = numberFormatter.format(Number(payload.changed_files || 0));
stagingEl.textContent = `${numberFormatter.format(Number(payload.staged_files || 0))} staged · ${numberFormatter.format(Number(payload.unstaged_files || 0))} unstaged`;
untrackedEl.textContent = numberFormatter.format(Number(payload.untracked_files || 0));
upstreamEl.textContent = typeof payload.repo_path === 'string' ? payload.repo_path : '--';
plusEl.textContent = `+${numberFormatter.format(Number(payload.insertions || 0))}`;
minusEl.textContent = `-${numberFormatter.format(Number(payload.deletions || 0))}`;
updatedEl.textContent = `Updated ${formatUpdated(payload.generated_at)}`;
const label = payload.dirty ? 'Dirty' : 'Clean';
const tone = statusTone(label);
setStatus(label, tone.fg, tone.bg);
renderFiles(payload.files);
updateLiveContent({
kind: 'git_repo_diff',
repo_name: payload.repo_name || null,
repo_path: payload.repo_path || null,
branch: payload.branch || null,
upstream: payload.upstream || null,
ahead: Number(payload.ahead || 0),
behind: Number(payload.behind || 0),
dirty: Boolean(payload.dirty),
changed_files: Number(payload.changed_files || 0),
staged_files: Number(payload.staged_files || 0),
unstaged_files: Number(payload.unstaged_files || 0),
untracked_files: Number(payload.untracked_files || 0),
insertions: Number(payload.insertions || 0),
deletions: Number(payload.deletions || 0),
files: Array.isArray(payload.files)
? payload.files.map((item) => ({
path: item?.path || null,
status: item?.status || null,
insertions: Number(item?.insertions || 0),
deletions: Number(item?.deletions || 0),
diff_available: Boolean(item?.diff_available),
diff_truncated: Boolean(item?.diff_truncated),
}))
: [],
generated_at: payload.generated_at || null,
});
};
const renderError = (message) => {
clearActiveSelection();
subtitleEl.textContent = subtitle || 'Git repo';
branchEl.textContent = 'Unable to load repo diff';
changedEl.textContent = '--';
stagingEl.textContent = '--';
untrackedEl.textContent = '--';
upstreamEl.textContent = '--';
plusEl.textContent = '+--';
minusEl.textContent = '- --';
updatedEl.textContent = message;
const tone = statusTone('Unavailable');
setStatus('Unavailable', tone.fg, tone.bg);
filesEl.innerHTML = '';
const error = document.createElement('div');
error.textContent = message;
error.style.fontSize = '0.88rem';
error.style.lineHeight = '1.4';
error.style.color = '#a45b51';
error.style.fontWeight = '700';
error.style.padding = '12px';
error.style.borderRadius = '12px';
error.style.background = 'rgba(243, 216, 210, 0.55)';
error.style.border = '1px solid rgba(164, 91, 81, 0.14)';
filesEl.appendChild(error);
updateLiveContent({
kind: 'git_repo_diff',
repo_name: null,
repo_path: null,
dirty: null,
error: message,
});
};
const loadPayload = async () => {
if (!configuredToolName) throw new Error('Missing template_state.tool_name');
if (!host.callToolAsync) throw new Error('Async tool helper unavailable');
const toolResult = await host.callToolAsync(
configuredToolName,
rawToolArguments,
{ timeoutMs: 180000 },
);
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object' && !Array.isArray(toolResult.parsed)) {
return toolResult.parsed;
}
const rawContent = typeof toolResult?.content === 'string' ? toolResult.content.trim() : '';
if (rawContent) {
if (rawContent.includes('(truncated,')) {
throw new Error('Tool output was truncated. Increase exec max_output_chars for this card.');
}
const normalizedContent = rawContent.replace(/\n+Exit code:\s*-?\d+\s*$/i, '').trim();
if (!normalizedContent.startsWith('{')) {
throw new Error(rawContent);
}
try {
const parsed = JSON.parse(normalizedContent);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Tool returned invalid JSON: ${detail}`);
}
}
throw new Error('Tool returned invalid JSON');
};
const refresh = async () => {
const loadingTone = { fg: '#9a7b68', bg: '#efe3d6' };
setStatus('Refreshing', loadingTone.fg, loadingTone.bg);
try {
const payload = await loadPayload();
render(payload);
} catch (error) {
renderError(String(error));
}
};
host.setRefreshHandler(() => {
void refresh();
});
void refresh();
return {
destroy() {
host.setRefreshHandler(null);
host.setLiveContent(null);
host.clearSelection();
for (const cleanup of __cleanup.splice(0)) cleanup();
},
};
}