869 lines
30 KiB
HTML
869 lines
30 KiB
HTML
<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>
|