function api() {
return window.LctxApi && window.LctxApi.apiFetch ? window.LctxApi.apiFetch : null;
}
function fmtLib() { return window.LctxFmt || {}; }
function tip(k) { return window.LctxShared && window.LctxShared.tip ? window.LctxShared.tip(k) : ''; }
function sparklineSvg(values, w, h) {
return window.LctxShared && window.LctxShared.sparklineSvg
? window.LctxShared.sparklineSvg(values, w, h) : '';
}
const BAND_CONFIG = {
green: { label: 'Optimal', icon: '\u2713', cls: 'band-green', desc: 'No action needed' },
yellow: { label: 'Moderate', icon: '\u25b2', cls: 'band-yellow', desc: 'Consider compressed reads for new files' },
orange: { label: 'High', icon: '\u26a0', cls: 'band-orange', desc: 'Review top eviction candidates' },
red: { label: 'Critical', icon: '\u2718', cls: 'band-red', desc: 'Compact or create handoff pack' },
};
const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
function fmtTok(n) {
if (n == null) return '0';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return String(Math.round(n));
}
function shortenPath(p) {
if (!p) return '';
const parts = p.replace(/\\/g, '/').split('/');
if (parts.length <= 3) return parts.join('/');
return '\u2026/' + parts.slice(-3).join('/');
}
function timeAgo(ts) {
if (!ts) return '\u2014';
const secs = Math.floor(Date.now() / 1000) - ts;
if (secs < 60) return 'just now';
if (secs < 3600) return Math.floor(secs / 60) + 'm ago';
if (secs < 86400) return Math.floor(secs / 3600) + 'h ago';
return Math.floor(secs / 86400) + 'd ago';
}
function toast(msg, kind) {
if (typeof window.showToast === 'function') window.showToast(msg, kind);
}
class CockpitCommander extends HTMLElement {
constructor() {
super();
this._data = null;
this._risk = null;
this._loading = true;
this._error = null;
this._powerMode = false;
this._sortKey = 'eviction_score';
this._sortDir = 'desc';
this._modeFilter = 'all';
this._expandedTrails = new Set();
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this._onRefresh = this._onRefresh || (() => {
const v = document.getElementById('view-commander');
if (v && v.classList.contains('active')) this.loadData();
});
document.addEventListener('lctx:refresh', this._onRefresh);
}
disconnectedCallback() {
if (this._onRefresh) document.removeEventListener('lctx:refresh', this._onRefresh);
}
async loadData() {
const fetchJson = api();
if (!fetchJson) { this._error = 'API not loaded'; this._loading = false; this.render(); return; }
this._loading = true;
this._error = null;
this.render();
const [triage, risk, signals] = await Promise.all([
fetchJson('/api/context-triage', { timeoutMs: 12000 }).catch(e => ({ __error: String(e?.error || e) })),
fetchJson('/api/context-risk', { timeoutMs: 12000 }).catch(e => ({ __error: String(e?.error || e) })),
fetchJson('/api/signals', { timeoutMs: 12000 }).catch(e => ({ __error: String(e?.error || e) })),
]);
if (triage?.__error) this._error = triage.__error;
this._data = triage?.__error ? null : triage;
this._risk = risk?.__error ? null : risk;
this._signals = signals?.__error ? null : signals;
this._loading = false;
this.render();
}
render() {
if (this._loading) {
this.innerHTML = '<div class="loading-pulse" style="padding:40px;text-align:center">Loading triage data\u2026</div>';
return;
}
if (this._error || !this._data) {
this.innerHTML = '<div class="card" style="padding:20px;color:var(--red)">\u26a0 ' + esc(this._error || 'No data') + '</div>';
return;
}
let h = '';
h += this._renderBudgetHero();
h += this._renderLiveSignals();
h += this._renderActionCards();
h += this._renderRiskAlerts();
h += this._renderPressureTable();
h += '<p class="hs" style="margin-top:4px;color:var(--muted)">Want the full inventory? ' +
'<a href="#context/contents" style="color:var(--accent)">Context Contents \u2192</a></p>';
this.innerHTML = h;
this._bind();
}
_renderBudgetHero() {
const b = this._data.budget;
const s = this._data.summary;
const band = BAND_CONFIG[b.band] || BAND_CONFIG.green;
const pct = Math.round((b.utilization || 0) * 100);
let h = '<div class="cmdr-budget-hero ' + band.cls + '">';
h += '<div class="cmdr-gauge-wrap">';
h += '<div class="cmdr-gauge-ring">';
h += '<svg viewBox="0 0 36 36" width="120" height="120">';
h += '<circle class="cmdr-gauge-bg" cx="18" cy="18" r="15.91549430918954" />';
const dashLen = Math.min(100, pct);
const gap = 100 - dashLen;
h += '<circle class="cmdr-gauge-fg" cx="18" cy="18" r="15.91549430918954" stroke-dasharray="' + dashLen + ' ' + gap + '" stroke-dashoffset="' + gap + '" />';
h += '</svg>';
h += '<div class="cmdr-gauge-label">' + pct + '%</div>';
h += '</div>';
h += '<div class="cmdr-gauge-info">';
h += '<div class="cmdr-band-badge" title="Health bands: \u2713 Optimal (<50% used) \u00b7 \u25b2 Moderate (50\u201375%) \u00b7 \u26a0 High (75\u201390%) \u00b7 \u2718 Critical (>90%)">' +
band.icon + ' ' + esc(band.label) + '</div>';
h += '<div class="cmdr-band-desc">' + esc(b.recommendation) + '</div>';
h += '<div class="cmdr-band-desc" style="opacity:.7;font-size:11px">Live value \u2014 changes as your agents read and write context.</div>';
h += '</div>';
h += '</div>';
h += '<div class="cmdr-stats-row">';
h += this._statCell('Files', s.total_files, '', '');
h += this._statCell('Tokens Used', fmtTok(s.total_tokens_sent), fmtTok(b.window_size) + ' window', '');
h += this._statCell('Tokens Saved', fmtTok(s.total_tokens_saved), '', 'var(--green)');
h += this._statCell('Remaining', fmtTok(b.remaining_tokens), '', b.band === 'red' ? 'var(--red)' : '');
h += this._statCell('Pinned', String(s.pinned_count), '', '');
h += this._statCell('At Risk', String(s.risk_count), '', s.risk_count > 0 ? 'var(--yellow)' : '');
h += '</div>';
h += '</div>';
return h;
}
_statCell(label, value, sub, color) {
let c = '<div class="cmdr-stat-cell">';
c += '<div class="cmdr-stat-label">' + label + '</div>';
c += '<div class="cmdr-stat-value"' + (color ? ' style="color:' + color + '"' : '') + '>' + esc(value) + '</div>';
if (sub) c += '<div class="cmdr-stat-sub">' + esc(sub) + '</div>';
return c + '</div>';
}
_renderLiveSignals() {
const sig = this._signals;
if (!sig) return '';
const tile = (icon, label, value, detail, tone) => {
const color = tone === 'on' ? 'var(--accent)' : tone === 'warn' ? 'var(--red)' : 'var(--muted)';
return '<div class="cmdr-stat-cell" style="min-width:130px"' + (detail ? ' title="' + esc(detail) + '"' : '') + '>' +
'<div class="cmdr-stat-label">' + icon + ' ' + esc(label) + '</div>' +
'<div class="cmdr-stat-value" style="font-size:14px;color:' + color + '">' + value + '</div>' +
'</div>';
};
let tiles = '';
const ed = sig.editor || {};
if (ed.active_file && ed.fresh) {
const base = String(ed.active_file).split('/').pop();
tiles += tile('\ud83d\udc41', 'Editor focus', esc(base),
ed.active_file + ' \u2014 ranked up while you look at it', 'on');
} else if (ed.active_file) {
tiles += tile('\ud83d\udc41', 'Editor focus', 'stale',
'Last focus signal is older than 2 min \u2014 not steering ranking. Needs the VS Code extension.', 'off');
} else {
tiles += tile('\ud83d\udc41', 'Editor focus', 'no signal',
'Sent by the lean-ctx VS Code extension on tab change (path only, never content).', 'off');
}
const dg = sig.diagnostics || {};
if (dg.errors > 0) {
tiles += tile('\u2716', 'Build errors', dg.errors + ' active',
'Files with active compiler/linter errors: ' + (dg.files || []).join(', ') + ' \u2014 forced to full reads + ranked up.', 'warn');
} else {
tiles += tile('\u2713', 'Build errors', 'none',
'Failing cargo/tsc/eslint runs mark their files as context priority; passing runs clear them.', 'off');
}
const git = sig.git || {};
tiles += tile('\u25cf', 'Git working set',
git.uncommitted > 0 ? git.uncommitted + ' uncommitted' : 'clean',
'Uncommitted files are the active task \u2014 they rank up and resist eviction.',
git.uncommitted > 0 ? 'on' : 'off');
const bm = sig.bounce_memory || {};
tiles += tile('\u21ba', 'Bounce memory',
bm.tracked_paths > 0
? bm.tracked_paths + ' tracked' + (bm.forced_full_paths > 0 ? ' \u00b7 ' + bm.forced_full_paths + ' forced full' : '')
: 'empty',
'Files where compressed reads kept getting re-read in full. After 2+ bounces auto-mode stops compressing them.',
bm.forced_full_paths > 0 ? 'on' : 'off');
const srcs = sig.auto_mode_sources || [];
const learnedSrcs = srcs.filter((s) =>
s[0] === 'path_bounce_memory' || s[0] === 'heatmap_conservative' || s[0] === 'active_diagnostic');
if (learnedSrcs.length > 0) {
const top = learnedSrcs.map((s) => s[0].replace(/_/g, ' ') + ' \u00d7' + s[1]).join(' \u00b7 ');
tiles += tile('\u2699', 'Learned decisions', esc(top),
'How often the learning loops (not static heuristics) decided the read mode, cumulative.', 'on');
} else {
tiles += tile('\u2699', 'Learned decisions', 'none yet',
'Counts how often bounce memory, heatmap or diagnostics override the default mode choice.', 'off');
}
let trend = '';
const bt = sig.bounce_trend || [];
if (bt.length >= 2) {
const rates = bt.map((d) => {
const reads = Math.max(1, d[2]);
return Math.min(1, d[1] / reads);
});
const spark = sparklineSvg(rates, 120, 26);
const last = bt[bt.length - 1];
const lastPct = Math.round(Math.min(1, last[1] / Math.max(1, last[2])) * 100);
trend = '<div style="display:flex;align-items:center;gap:10px;margin-left:auto" ' +
'title="Bounce rate per day = bounces / compressed reads. Falling = the mode policy is learning which files not to compress.">' +
'<div style="text-align:right"><div class="cmdr-stat-label">Bounce-rate trend (' + bt.length + 'd)</div>' +
'<div class="cmdr-stat-sub">today ' + lastPct + '%</div></div>' + spark + '</div>';
} else {
trend = '<div class="cmdr-stat-sub" style="margin-left:auto;align-self:center">' +
'Learning trend: collecting data \u2014 needs 2+ days of usage</div>';
}
return '<div class="cmdr-section"><div class="cmdr-section-header"><h3>Live Signals</h3>' +
'<span class="badge" title="What lean-ctx currently knows about your working context, and the loops it learned from. Updates with every agent action.">closed loop</span></div>' +
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:stretch">' + tiles + trend + '</div></div>';
}
_renderActionCards() {
const actions = this._data.actions || [];
if (actions.length === 0) return '';
let h = '<div class="cmdr-section">';
h += '<div class="cmdr-section-header"><h3>Recommended Actions</h3>';
h += '<span class="badge">' + actions.length + '</span></div>';
h += '<div class="cmdr-actions-grid">';
for (const a of actions) {
const icon = a.type === 'evict' ? '\u232b' : a.type === 'compress' ? '\u21e9' : '\u21bb';
const typeLabel = a.type === 'evict' ? 'Evict' : a.type === 'compress' ? 'Compress' : 'Full Read';
const savingsText = a.estimated_savings > 0 ? 'Save ' + fmtTok(a.estimated_savings) + ' tokens' : '';
h += '<div class="cmdr-action-card" data-action-type="' + esc(a.type) + '" data-action-path="' + esc(a.path) + '"';
if (a.to_mode) h += ' data-action-mode="' + esc(a.to_mode) + '"';
h += '>';
h += '<div class="cmdr-action-icon">' + icon + '</div>';
h += '<div class="cmdr-action-body">';
h += '<div class="cmdr-action-type">' + typeLabel + '</div>';
h += '<div class="cmdr-action-path" title="' + esc(a.path) + '">' + esc(shortenPath(a.path)) + '</div>';
h += '<div class="cmdr-action-reason">' + esc(a.reason) + '</div>';
if (savingsText) h += '<div class="cmdr-action-savings">' + savingsText + '</div>';
h += '</div>';
h += '<button type="button" class="cmdr-action-btn" title="Execute">Apply</button>';
h += '</div>';
}
h += '</div></div>';
return h;
}
_renderRiskAlerts() {
const warnings = this._risk?.warnings || [];
if (warnings.length === 0) return '';
let h = '<div class="cmdr-section">';
for (const w of warnings) {
const sev = w.severity === 'high' ? 'cmdr-risk-high' : 'cmdr-risk-medium';
h += '<div class="cmdr-risk-banner ' + sev + '">';
h += '<div class="cmdr-risk-icon">' + (w.severity === 'high' ? '\u26a0' : '\u24d8') + '</div>';
h += '<div class="cmdr-risk-body">';
h += '<div class="cmdr-risk-path">' + esc(shortenPath(w.path)) + ' <span class="tag tg">' + esc(w.mode) + '</span></div>';
h += '<div class="cmdr-risk-msg">' + esc(w.message) + '</div>';
h += '<div class="cmdr-risk-suggest">' + esc(w.suggestion) + '</div>';
h += '</div>';
h += '<button type="button" class="cmdr-action-btn" data-action-type="full_read" data-action-path="' + esc(w.path) + '">Read Full</button>';
h += '</div>';
}
h += '</div>';
return h;
}
_renderPressureTable() {
const items = this._data.items || [];
if (items.length === 0) return '<div class="card" style="padding:20px;text-align:center;color:var(--muted)">No files in context yet.</div>';
const modes = ['all', ...new Set(items.map(i => i.mode).filter(Boolean))];
let filtered = this._modeFilter !== 'all' ? items.filter(i => i.mode === this._modeFilter) : items;
const sk = this._sortKey;
const dir = this._sortDir === 'desc' ? -1 : 1;
filtered = [...filtered].sort((a, b) => {
let av = a[sk], bv = b[sk];
if (typeof av === 'string') av = av.toLowerCase();
if (typeof bv === 'string') bv = bv.toLowerCase();
if (av == null) av = 0;
if (bv == null) bv = 0;
return av < bv ? -1 * dir : av > bv ? dir : 0;
});
const th = (key, label, cls) => {
const active = sk === key;
const ind = active ? (this._sortDir === 'asc' ? ' \u25b2' : ' \u25bc') : ' \u25c7';
return '<th class="' + (cls || '') + (active ? ' th-sort-active' : '') + '" data-sort="' + key + '" style="cursor:pointer;user-select:none">' + label + '<span class="sort-ind">' + ind + '</span></th>';
};
const modeOpts = modes.map(m =>
'<option value="' + esc(m) + '"' + (m === this._modeFilter ? ' selected' : '') + '>' + (m === 'all' ? 'All modes' : esc(m)) + '</option>'
).join('');
let h = '<div class="cmdr-section">';
h += '<div class="cmdr-section-header">';
h += '<h3>Context Pressure Table</h3>';
h += '<div style="display:flex;align-items:center;gap:8px">';
h += '<span class="badge">' + filtered.length + '/' + items.length + '</span>';
h += '<select id="cmdrModeFilter" class="btn" style="padding:4px 8px;font-size:11px">' + modeOpts + '</select>';
h += '<button type="button" class="btn cmdr-power-toggle" id="cmdrPowerToggle">' + (this._powerMode ? 'Simple' : 'Detailed') + '</button>';
h += '</div></div>';
h += '<div class="table-scroll"><table>';
h += '<thead><tr>';
h += th('path', 'Path');
h += th('tokens_sent', 'Tokens', 'r');
h += th('mode', 'Mode');
if (this._powerMode) {
h += th('tokens_original', 'Original', 'r');
h += th('compression_pct', 'Saved %', 'r');
h += th('phi', '\u03a6', 'r');
h += th('last_accessed_ts', 'Last Access');
h += th('access_count', 'Reads', 'r');
h += th('eviction_score', 'Eviction', 'r');
}
h += '<th>Status</th>';
h += '<th>Actions</th>';
h += '</tr></thead><tbody>';
for (let idx = 0; idx < filtered.length; idx++) {
const r = filtered[idx];
const pd = encodeURIComponent(r.path);
const hasRisk = r.risk_flags && r.risk_flags.length > 0;
const rowCls = r.state === 'excluded' ? ' cmdr-row-excluded'
: hasRisk ? ' cmdr-row-risk' : r.pinned ? ' cmdr-row-pinned' : '';
h += '<tr class="cmdr-table-row' + rowCls + '">';
h += '<td class="ctx-path-cell" title="' + esc(r.path) + '">' + esc(shortenPath(r.path)) + '</td>';
h += '<td class="r">' + fmtTok(r.tokens_sent) + '</td>';
h += '<td><span class="tag tg">' + esc(r.mode) + '</span></td>';
if (this._powerMode) {
h += '<td class="r">' + fmtTok(r.tokens_original) + '</td>';
h += '<td class="r">' + (r.compression_pct || 0) + '%</td>';
h += '<td class="r">' + (r.phi != null ? Number(r.phi).toFixed(3) : '\u2014') + '</td>';
h += '<td>' + timeAgo(r.last_accessed_ts) + '</td>';
h += '<td class="r">' + (r.access_count || 0) + '</td>';
const evScore = r.eviction_score || 0;
const evColor = evScore > 0.7 ? 'var(--red)' : evScore > 0.4 ? 'var(--yellow)' : 'var(--green)';
h += '<td class="r" style="color:' + evColor + '">' + evScore.toFixed(2) + '</td>';
}
h += '<td>';
if (r.pinned) h += '<span class="cmdr-pin-badge">\ud83d\udccc</span> ';
if (hasRisk) h += '<span class="cmdr-risk-dot" title="Risk detected">\u26a0</span>';
if (r.diagnostics && r.diagnostics.length > 0) {
var errCount = r.diagnostics.filter(function (d) { return d.severity === 'error'; }).length;
if (errCount > 0) {
var firstErr = r.diagnostics.find(function (d) { return d.severity === 'error'; });
var diagTip = 'Active build error' + (errCount > 1 ? 's (' + errCount + ')' : '') +
(firstErr && firstErr.message ? ': ' + firstErr.message : '');
h += ' <span style="color:var(--red);font-weight:600" title="' + esc(diagTip) + '">\u2716 ' +
errCount + ' error' + (errCount > 1 ? 's' : '') + '</span>';
}
}
if (r.git_recency >= 1) {
h += ' <span style="color:var(--accent)" title="Uncommitted changes \u2014 active working set">\u25cf</span>';
}
if (r.editor_active) {
h += ' <span title="Currently open in the editor">\ud83d\udc41</span>';
}
if (r.state === 'excluded') {
h += ' <span class="tag td">Excluded</span>';
}
h += '</td>';
h += '<td style="white-space:nowrap">';
if (!r.pinned) {
h += '<button type="button" class="action-btn" data-act="pin" data-path="' + pd + '">Pin</button> ';
} else {
h += '<button type="button" class="action-btn" data-act="unpin" data-path="' + pd + '">Unpin</button> ';
}
if (r.state === 'excluded') {
h += '<span class="tag td" style="font-size:10px">Excluded</span>';
} else {
h += '<button type="button" class="action-btn danger" data-act="evict" data-path="' + pd + '">Evict</button>';
}
h += '</td></tr>';
if (this._powerMode && r.source_trail && r.source_trail.length > 0) {
const expanded = this._expandedTrails.has(idx);
h += '<tr class="cmdr-trail-toggle" data-trail-idx="' + idx + '">';
h += '<td colspan="' + (this._powerMode ? 12 : 6) + '" style="padding:2px 12px;font-size:10px;cursor:pointer;color:var(--muted)">';
h += (expanded ? '\u25bc' : '\u25b6') + ' Why in context? (' + r.source_trail.length + ')';
h += '</td></tr>';
if (expanded) {
h += '<tr class="cmdr-trail-content"><td colspan="' + (this._powerMode ? 12 : 6) + '">';
h += '<div class="cmdr-trail-items">';
for (const t of r.source_trail) {
h += '<div class="cmdr-trail-item">';
h += '<span class="cmdr-trail-type">' + esc(t.type) + '</span>';
if (t.tool) h += ' <span class="tag tg">' + esc(t.tool) + '</span>';
if (t.detail) h += ' ' + esc(t.detail);
if (t.ts) h += ' <span class="cmdr-trail-time">' + timeAgo(t.ts) + '</span>';
h += '</div>';
}
h += '</div></td></tr>';
}
}
}
h += '</tbody></table></div></div>';
return h;
}
_bind() {
this.querySelectorAll('th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const k = th.getAttribute('data-sort');
if (this._sortKey === k) {
this._sortDir = this._sortDir === 'asc' ? 'desc' : 'asc';
} else {
this._sortKey = k;
this._sortDir = 'desc';
}
this.render();
});
});
const modeFilter = this.querySelector('#cmdrModeFilter');
if (modeFilter) {
modeFilter.addEventListener('change', () => {
this._modeFilter = modeFilter.value;
this.render();
});
}
const toggle = this.querySelector('#cmdrPowerToggle');
if (toggle) {
toggle.addEventListener('click', () => {
this._powerMode = !this._powerMode;
this.render();
});
}
this.querySelectorAll('.cmdr-trail-toggle').forEach(row => {
row.addEventListener('click', () => {
const idx = parseInt(row.getAttribute('data-trail-idx'), 10);
if (this._expandedTrails.has(idx)) {
this._expandedTrails.delete(idx);
} else {
this._expandedTrails.add(idx);
}
this.render();
});
});
this.querySelectorAll('.cmdr-action-card .cmdr-action-btn').forEach(btn => {
btn.addEventListener('click', () => {
const card = btn.closest('.cmdr-action-card');
const type = card.getAttribute('data-action-type');
const path = card.getAttribute('data-action-path');
const mode = card.getAttribute('data-action-mode');
this._executeAction(type, path, mode);
});
});
this.querySelectorAll('.cmdr-risk-banner .cmdr-action-btn').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-action-type');
const path = btn.getAttribute('data-action-path');
this._executeAction(type, path);
});
});
this.querySelectorAll('.action-btn[data-act]').forEach(btn => {
btn.addEventListener('click', () => {
const act = btn.getAttribute('data-act');
const path = decodeURIComponent(btn.getAttribute('data-path'));
this._executeOverlay(act, path);
});
});
}
async _executeAction(type, path, mode) {
const fetchJson = api();
if (!fetchJson) return;
if (type === 'evict') {
await this._executeOverlay('evict', path);
} else if (type === 'compress' && mode) {
await this._executeOverlay('set_view', path, mode);
} else if (type === 'full_read') {
await this._executeOverlay('set_view', path, 'full');
}
}
async _executeOverlay(action, path, value) {
const fetchJson = api();
if (!fetchJson) return;
const body = { action, path };
if (value !== undefined) body.value = value;
try {
await fetchJson('/api/context-overlay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
timeoutMs: 15000,
});
const msg = action === 'set_view'
? 'View → ' + (value || 'default') + ': ' + shortenPath(path) + ' (next read)'
: action === 'evict'
? 'Evicted: ' + shortenPath(path)
: action + ': ' + shortenPath(path);
toast(msg, 'success');
await this.loadData();
} catch (e) {
toast('Failed: ' + (e?.error || String(e)), 'error');
}
}
}
(function register() {
var R = window.LctxRouter;
if (R && R.registerLoader) {
R.registerLoader('commander', function () {
var el = document.querySelector('cockpit-commander');
if (el && typeof el.loadData === 'function') return el.loadData();
});
}
})();
customElements.define('cockpit-commander', CockpitCommander);
export { CockpitCommander };