feat: audit log, usage tracking UI, OpenAI orchestrator compaction, onboarding + docs
Tool audit log:
- Every orchestrator tool call logged to home/{user}/tool_audit/YYYY-MM-DD.jsonl
- Files panel sidebar: audit log group (collapsed), date-linked read-only table
- Admin endpoints: /api/audit/files, /api/audit/day, /api/audit/recent, /api/audit/stats
- Engine and model name recorded per entry
OpenAI orchestrator improvements:
- Context budget enforcement: 75% of model context_k (min 16k)
- Message compaction: truncates old tool results when approaching budget
- max_rounds respected per model config (intersected with server cap)
OpenRouter onboarding (setup.html, onboarding.py, app.js, settings.html):
- Step 3 of 3: /setup/model with curated model picker
- Chat banner for users on server-default model (informational, not alarmist)
- Settings quick-link card; /setup/model works standalone for existing users
Model registry + session store:
- set_role_config / get_role_config for per-role tool lists and system_append
- session_store: session rename, session name backfill endpoint
UI updates (app.js, index.html, style.css, local_llm.html):
- Role toggle in context panel
- Off-the-record mode
- Agent notes read-only viewer
- OPERATIONS.md loaded at T2+ in context
Documentation:
- HELP.md: full tool table, per-role tool sets, Agent Notes, usage tracking
- TOOLS.md: Agent Notes section, count corrected to 44
- ARCH__SYSTEM.md, ARCH__BACKENDS.md, MASTER.md updated to match reality
- CLAUDE.md: onboarding flow, documentation philosophy sections
- README.md: stack in practice, DeepSeek TUI mention, architecture diagram updated
- TODO__Agents.md: onboarding task completed with deviation notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,11 @@
|
||||
const settings_dd_el = document.getElementById('settings-dropdown');
|
||||
const sessionsBackdrop = document.getElementById('sessions-backdrop');
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Close all panels/dropdowns (mutual exclusion) ─────────────
|
||||
function closeAllPanels() {
|
||||
if (mode_dropdown_el) mode_dropdown_el.classList.remove('open');
|
||||
@@ -435,8 +440,32 @@
|
||||
availableRoles = d.available_roles || [];
|
||||
roleIdx = 0;
|
||||
setRoleToggleUI(availableRoles[0] || null);
|
||||
_maybeShowNoBanner(availableRoles);
|
||||
});
|
||||
|
||||
function _maybeShowNoBanner(roles) {
|
||||
const key = 'cx_no_model_banner_dismissed';
|
||||
if (roles.length > 0) { localStorage.removeItem(key); return; }
|
||||
if (localStorage.getItem(key)) return;
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'no-model-banner';
|
||||
banner.style.cssText = [
|
||||
'background:#1c1a0a','border-bottom:1px solid #78350f',
|
||||
'color:#fbbf24','font-size:0.82rem','padding:0.55rem 1rem',
|
||||
'display:flex','align-items:center','gap:0.75rem','flex-shrink:0',
|
||||
].join(';');
|
||||
banner.innerHTML = `
|
||||
<span style="flex:1">⚡ Using server default model — add your own for more choices and to track your usage.</span>
|
||||
<a href="/setup/model" style="color:#fbbf24;font-weight:600;white-space:nowrap;">Set up OpenRouter →</a>
|
||||
<button onclick="localStorage.setItem('${key}','1');document.getElementById('no-model-banner').remove();"
|
||||
style="background:none;border:none;color:#78350f;cursor:pointer;font-size:1rem;line-height:1;padding:0 0.2rem;"
|
||||
title="Dismiss">✕</button>
|
||||
`;
|
||||
// Insert at the top of #chat-col (or body if not found)
|
||||
const col = document.getElementById('chat-col') || document.body.firstElementChild;
|
||||
col.insertBefore(banner, col.firstChild);
|
||||
}
|
||||
|
||||
backendToggle.addEventListener('click', () => {
|
||||
if (availableRoles.length <= 1) return;
|
||||
roleIdx = (roleIdx + 1) % availableRoles.length;
|
||||
@@ -1067,6 +1096,19 @@
|
||||
sessionId = data.session_id;
|
||||
sessionEl.textContent = `session: ${sessionId}`;
|
||||
persist_session();
|
||||
|
||||
// Auto-name the session from the first user message
|
||||
if (wasNewSession) {
|
||||
const autoName = text.slice(0, 60).trimEnd() + (text.length > 60 ? '…' : '');
|
||||
fetch(`/sessions/${sessionId}?${_fileParams}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: autoName }),
|
||||
}).then(() => {
|
||||
sessionEl.textContent = `session: ${autoName}`;
|
||||
sessionNames.set(sessionId, autoName);
|
||||
}).catch(() => {});
|
||||
}
|
||||
thinkingDiv.className = 'message assistant';
|
||||
setMessageText(thinkingDiv, 'assistant', data.response);
|
||||
const assistHistIdx = currentHistory.length;
|
||||
@@ -1133,6 +1175,8 @@
|
||||
const text = inputEl.value.trim();
|
||||
if (!text || activeController) return;
|
||||
|
||||
const wasNewSession = !sessionId;
|
||||
|
||||
inputEl.value = '';
|
||||
syncHeight();
|
||||
sendBtn.style.display = 'none';
|
||||
@@ -1357,6 +1401,7 @@
|
||||
{ label: 'Memory', files: ['MEMORY_LONG.md', 'MEMORY_MID.md', 'MEMORY_SHORT.md'] },
|
||||
{ label: 'Profile', files: ['USER.md', 'HELP.md'] },
|
||||
{ label: 'Settings', files: ['email_allowlist.json'] },
|
||||
{ label: 'Agent Notes (read-only)', files: ['AGENT_NOTES.bak1.md', 'AGENT_NOTES.bak2.md', 'AGENT_NOTES.bak3.md'], collapsed: true },
|
||||
];
|
||||
|
||||
function fmtSize(bytes) {
|
||||
@@ -1394,7 +1439,7 @@
|
||||
fileSidebar.innerHTML = '';
|
||||
|
||||
for (const group of FILE_GROUPS) {
|
||||
const { groupEl, items } = _makeFileGroup(group.label);
|
||||
const { groupEl, items } = _makeFileGroup(group.label, group.collapsed || false);
|
||||
|
||||
for (const fname of group.files) {
|
||||
const f = byName[fname];
|
||||
@@ -1490,12 +1535,20 @@
|
||||
// Restore editor/preview buttons hidden by audit view
|
||||
fileRawBtn.style.display = '';
|
||||
filePreviewBtn.style.display = '';
|
||||
fileSaveBtn.style.display = '';
|
||||
const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`);
|
||||
if (!res.ok) { mdEditor.setValue(`Error loading ${name}`); return; }
|
||||
const data = await res.json();
|
||||
mdEditor.setValue(data.content);
|
||||
mdEditor.clearHistory();
|
||||
if (data.readonly) {
|
||||
mdEditor.setOption('readOnly', 'nocursor');
|
||||
fileSaveBtn.style.display = 'none';
|
||||
document.getElementById('file-modal-title').textContent = name + ' (read-only)';
|
||||
} else {
|
||||
mdEditor.setOption('readOnly', false);
|
||||
fileSaveBtn.style.display = '';
|
||||
document.getElementById('file-modal-title').textContent = name;
|
||||
}
|
||||
setFileMode(fileMode);
|
||||
}
|
||||
|
||||
@@ -1794,11 +1847,13 @@
|
||||
let memMid = localStorage.getItem('mem-mid') !== 'false';
|
||||
let memShort = localStorage.getItem('mem-short') !== 'false';
|
||||
|
||||
const TIER_LABELS = { 1: 'Min', 2: 'Std', 3: 'Ext', 4: 'Full' };
|
||||
|
||||
function updateTierUI() {
|
||||
document.querySelectorAll('.ctx-btn[data-tier]').forEach(btn => {
|
||||
btn.classList.toggle('active', parseInt(btn.dataset.tier) === currentTier);
|
||||
});
|
||||
ctxOpenBtn.querySelector('.tier-badge').textContent = currentTier;
|
||||
ctxOpenBtn.querySelector('.tier-badge').textContent = TIER_LABELS[currentTier] || currentTier;
|
||||
}
|
||||
|
||||
function updateMemUI() {
|
||||
@@ -1870,33 +1925,46 @@
|
||||
memShort = !memShort; localStorage.setItem('mem-short', memShort); updateMemUI();
|
||||
});
|
||||
|
||||
const _distillBtns = () => document.querySelectorAll(
|
||||
'#distill-short-btn, #distill-mid-btn, #distill-long-btn, #distill-all-btn, #distill-rebuild-btn'
|
||||
);
|
||||
|
||||
function showDistillStatus(msg, isErr) {
|
||||
distillStatus.textContent = msg;
|
||||
distillStatus.classList.toggle('err', !!isErr);
|
||||
distillStatus.classList.add('show');
|
||||
setTimeout(() => distillStatus.classList.remove('show'), 5000);
|
||||
setTimeout(() => distillStatus.classList.remove('show'), isErr ? 8000 : 5000);
|
||||
}
|
||||
|
||||
async function runDistill(endpoint) {
|
||||
showDistillStatus('distilling…', false);
|
||||
async function runDistill(endpoint, label) {
|
||||
_distillBtns().forEach(b => { b.disabled = true; });
|
||||
showDistillStatus(`${label || endpoint} running…`, false);
|
||||
try {
|
||||
const res = await fetch(`/distill/${endpoint}?${_fileParams}`, { method: 'POST' });
|
||||
const d = await res.json();
|
||||
if (!res.ok || d.ok === false) {
|
||||
const err = d.error || d.mid?.error || d.long?.error || `HTTP ${res.status}`;
|
||||
if (res.status === 409 || res.status === 429) {
|
||||
showDistillStatus(`⏳ ${d.detail}`, true);
|
||||
} else if (!res.ok || d.ok === false) {
|
||||
const err = d.detail || d.error || d.mid?.error || d.long?.error || `HTTP ${res.status}`;
|
||||
showDistillStatus(`✗ ${err}`, true);
|
||||
} else {
|
||||
showDistillStatus(`✓ ${endpoint} done`, false);
|
||||
showDistillStatus(`✓ ${label || endpoint} complete`, false);
|
||||
}
|
||||
} catch (err) {
|
||||
showDistillStatus(`✗ ${err.message}`, true);
|
||||
} finally {
|
||||
_distillBtns().forEach(b => { b.disabled = false; });
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('distill-short-btn').addEventListener('click', () => runDistill('short'));
|
||||
document.getElementById('distill-mid-btn').addEventListener('click', () => runDistill('mid'));
|
||||
document.getElementById('distill-long-btn').addEventListener('click', () => runDistill('long'));
|
||||
document.getElementById('distill-all-btn').addEventListener('click', () => runDistill('all'));
|
||||
document.getElementById('distill-short-btn').addEventListener('click', () => runDistill('short', 'Short distill'));
|
||||
document.getElementById('distill-mid-btn').addEventListener('click', () => runDistill('mid', 'Mid distill'));
|
||||
document.getElementById('distill-long-btn').addEventListener('click', () => runDistill('long', 'Long distill'));
|
||||
document.getElementById('distill-all-btn').addEventListener('click', () => runDistill('all', 'Full distill'));
|
||||
document.getElementById('distill-rebuild-btn').addEventListener('click', () => {
|
||||
if (!confirm('Rebuild memory from scratch?\n\nThis will wipe MEMORY_MID and MEMORY_LONG (backups kept) then regenerate them from session logs. Any hand-edited content will be replaced.\n\nContinue?')) return;
|
||||
runDistill('rebuild', 'Memory rebuild');
|
||||
});
|
||||
|
||||
updateTierUI();
|
||||
updateMemUI();
|
||||
|
||||
Reference in New Issue
Block a user