feat: add card examples and speed up rtc connect
This commit is contained in:
parent
04afead5af
commit
23fd806e6d
41 changed files with 3327 additions and 3 deletions
15
examples/cards/templates/git-diff-live/manifest.json
Normal file
15
examples/cards/templates/git-diff-live/manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"key": "git-diff-live",
|
||||
"title": "Git Repo Diff",
|
||||
"notes": "Manual-refresh git working tree summary for a specific repo directory. Fill template_state with tool_name `exec`, tool_arguments.command running `$HOME/.nanobot/workspace/git_repo_diff_summary.py`, `tool_arguments.max_output_chars` sized for the repo diff payload, and optional subtitle. Refresh it from the card menu. The card shows branch, changed/untracked counts, insertions/deletions, and the top changed files.",
|
||||
"example_state": {
|
||||
"subtitle": "nanobot",
|
||||
"tool_name": "exec",
|
||||
"tool_arguments": {
|
||||
"command": "python3 $HOME/.nanobot/workspace/git_repo_diff_summary.py --repo /home/kacper/nanobot --max-files 8",
|
||||
"max_output_chars": 200000
|
||||
}
|
||||
},
|
||||
"created_at": "2026-03-12T14:00:00+00:00",
|
||||
"updated_at": "2026-03-14T17:00:00-04:00"
|
||||
}
|
||||
869
examples/cards/templates/git-diff-live/template.html
Normal file
869
examples/cards/templates/git-diff-live/template.html
Normal file
|
|
@ -0,0 +1,869 @@
|
|||
<style>
|
||||
[data-git-diff-card] {
|
||||
font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
|
||||
background: #f7ecdf;
|
||||
color: #65483a;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
[data-git-header] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
[data-git-header-left] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-git-subtitle] {
|
||||
font-size: 1.28rem;
|
||||
line-height: 1.06;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #c9694b;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-git-branch] {
|
||||
margin-top: 6px;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
color: #9a7b68;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-git-status] {
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
[data-git-metrics] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
margin: 12px 0 12px;
|
||||
}
|
||||
|
||||
[data-git-metric] {
|
||||
min-width: 0;
|
||||
background: rgba(255, 255, 255, 0.34);
|
||||
border: 1px solid rgba(186, 143, 113, 0.10);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px 9px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
|
||||
[data-git-metric-label] {
|
||||
font-size: 0.62rem;
|
||||
line-height: 1.2;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #a48a78;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[data-git-changed],
|
||||
[data-git-untracked] {
|
||||
margin-top: 5px;
|
||||
font-size: 1.8rem;
|
||||
line-height: 0.92;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.05em;
|
||||
color: #7d4f3f;
|
||||
}
|
||||
|
||||
[data-git-staging],
|
||||
[data-git-upstream],
|
||||
[data-git-updated] {
|
||||
margin-top: 5px;
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.35;
|
||||
color: #947662;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-git-diff-values] {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
[data-git-plus] {
|
||||
font-size: 1.12rem;
|
||||
line-height: 0.95;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #6f8f5f;
|
||||
}
|
||||
|
||||
[data-git-minus] {
|
||||
font-size: 1.12rem;
|
||||
line-height: 0.95;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #b46457;
|
||||
}
|
||||
|
||||
[data-git-files] {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(177, 140, 112, 0.16);
|
||||
}
|
||||
|
||||
[data-git-patch-row][data-selectable='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-git-patch-row][data-selected='true'] {
|
||||
box-shadow: inset 3px 0 0 rgba(101, 72, 58, 0.72);
|
||||
}
|
||||
</style>
|
||||
<div data-git-diff-card>
|
||||
<div data-git-header>
|
||||
<div data-git-header-left>
|
||||
<div data-git-subtitle>Loading…</div>
|
||||
<div data-git-branch>--</div>
|
||||
</div>
|
||||
<span data-git-status>Loading…</span>
|
||||
</div>
|
||||
|
||||
<div data-git-metrics>
|
||||
<section data-git-metric>
|
||||
<div data-git-metric-label>Changed</div>
|
||||
<div data-git-changed>--</div>
|
||||
<div data-git-staging>--</div>
|
||||
</section>
|
||||
|
||||
<section data-git-metric>
|
||||
<div data-git-metric-label>Untracked</div>
|
||||
<div data-git-untracked>--</div>
|
||||
<div data-git-upstream>--</div>
|
||||
</section>
|
||||
|
||||
<section data-git-metric>
|
||||
<div data-git-metric-label>Diff</div>
|
||||
<div data-git-diff-values>
|
||||
<span data-git-plus>+--</span>
|
||||
<span data-git-minus>- --</span>
|
||||
</div>
|
||||
<div data-git-updated>--</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div data-git-files></div>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
const script = document.currentScript;
|
||||
const root = script?.closest('[data-nanobot-card-root]');
|
||||
const state = window.__nanobotGetCardState?.(script) || {};
|
||||
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);
|
||||
}
|
||||
window.__nanobotSetCardSelection?.(script, 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;
|
||||
window.__nanobotSetCardSelection?.(script, 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;
|
||||
window.__nanobotSetCardSelection?.(
|
||||
script,
|
||||
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) => {
|
||||
window.__nanobotSetCardLiveContent?.(script, 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 (!window.__nanobotCallToolAsync) throw new Error('Async tool helper unavailable');
|
||||
const toolResult = await window.__nanobotCallToolAsync(
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
window.__nanobotSetCardRefresh?.(script, () => {
|
||||
void refresh();
|
||||
});
|
||||
void refresh();
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue