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) : ''; }
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;
document.addEventListener('lctx:refresh', () => this.loadData());
this.loadData();
}
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] = 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) })),
]);
if (triage?.__error) this._error = triage.__error;
this._data = triage?.__error ? null : triage;
this._risk = risk?.__error ? null : risk;
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._renderActionCards();
h += this._renderRiskAlerts();
h += this._renderPressureTable();
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">' + band.icon + ' ' + esc(band.label) + '</div>';
h += '<div class="cmdr-band-desc">' + esc(b.recommendation) + '</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>';
}
_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 = 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>';
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> ';
}
h += '<button type="button" class="action-btn danger" data-act="exclude" 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('exclude', 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 fetch('/api/context-overlay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (window.__LEAN_CTX_TOKEN__ || ''),
},
body: JSON.stringify(body),
});
toast(action + ': ' + shortenPath(path), 'success');
this.loadData();
} catch (e) {
toast('Failed: ' + 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 };