trimprompt 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -41,6 +41,7 @@
41
41
  flex-direction: column;
42
42
  position: relative;
43
43
  overflow-x: hidden;
44
+ zoom: 80%;
44
45
  }
45
46
 
46
47
  /* Ambient radial glow background */
@@ -235,14 +236,16 @@
235
236
  font-family: 'Outfit', sans-serif;
236
237
  font-weight: 700;
237
238
  color: var(--text-muted);
238
- padding: 1rem;
239
+ padding: 1.1rem 1.2rem;
239
240
  border-bottom: 1px solid var(--border);
241
+ vertical-align: middle;
240
242
  }
241
243
 
242
244
  td {
243
- padding: 1rem;
245
+ padding: 1.1rem 1.2rem;
244
246
  border-bottom: 1px solid var(--border);
245
247
  color: var(--text);
248
+ vertical-align: middle;
246
249
  }
247
250
 
248
251
  tr:last-child td {
@@ -678,7 +681,13 @@
678
681
  </h2>
679
682
  </div>
680
683
  <div id="csa-body" style="display:none;">
681
- <div id="csa-list"></div>
684
+ <div id="csa-list">
685
+ <div style="padding:40px 20px;text-align:center;color:#818CF8;font-size:13px;display:flex;align-items:center;justify-content:center;gap:12px;">
686
+ <style>@keyframes csaSpin { to { transform: rotate(360deg); } }</style>
687
+ <span style="width:18px;height:18px;border:2px solid #818CF8;border-top-color:transparent;border-radius:50%;display:inline-block;animation:csaSpin 0.8s linear infinite;"></span>
688
+ <span style="font-weight:500;">Loading Live Session Context Controls...</span>
689
+ </div>
690
+ </div>
682
691
  <div class="csa-summary" id="csa-summary"></div>
683
692
  </div>
684
693
  </div>
@@ -740,6 +749,7 @@
740
749
  <script>
741
750
  let savingsChart = null;
742
751
  let currentFilter = 'ai';
752
+ const expandedGroupKeys = new Set();
743
753
 
744
754
  const FEATURE_META = {
745
755
  filter: { label: 'Filter', color: '#6366F1', bg: 'rgba(99,102,241,0.12)' },
@@ -771,17 +781,22 @@
771
781
  { key: 'lock', title: 'Lock file detection', desc: 'package-lock.json, yarn.lock, Cargo.lock, go.sum → summary line', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'on lock files', status: 'active' },
772
782
  { key: 'binary', title: 'Binary/minified detection', desc: '.png, .min.js, .wasm, .pdf, 40+ extensions → placeholder', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'on binaries', status: 'active' },
773
783
  { key: 'json_crusher', title: 'SmartCrusher (JSON arrays → tables)', desc: 'Arrays of objects compressed to tabular format. Removes resolved/integrity fields', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'on JSON arrays', status: 'active' },
774
- { key: 'skeleton', title: 'Code skeleton (imports + signatures only)', desc: 'Files over 100 lines → show structure only, collapse bodies', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'on large files', status: 'active' },
775
- { key: 'ccr', title: 'CCR — Reversible compression', desc: 'Store original in cache, send compressed. Agent can retrieve full via trim retrieve', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'deeper compression', status: 'off' },
784
+ { key: 'skeleton', title: 'Code skeleton (imports + signatures only)', desc: 'Files over 100 lines → show structure only, collapse bodies', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statSub: 'on large files', status: 'active' }
776
785
  ]},
777
786
  { category: 'S — Security', features: [
778
787
  { key: 'redact', title: 'Secret redaction (20 patterns)', desc: 'AWS keys, API tokens, JWTs, DB strings auto-blocked', badges: ['Claude','Cursor','Copilot','Gemini','Antigravity'], statKey: 'secrets', statSub: 'secrets blocked', status: 'active' },
779
788
  ]},
780
- // --- PProxy section hidden for now (interceptor not functional yet). Re-enable by uncommenting. ---
781
- // { category: 'P Proxy (Universal AI API Compression)', features: [
782
- // { key: 'conv_compress', title: 'Conversation history compression', desc: 'Score old messages by importance, drop low-value ones. Via trim proxy', badges: ['Claude Code CLI','Codex CLI','Aider','Copilot CLI','SDK / curl'], statSub: 'on conversation', status: 'active' },
783
- // { key: 'resp_optimize', title: 'Response length optimization', desc: 'Set optimal max_tokens per request based on task complexity. Via trim proxy', badges: ['Claude Code CLI','Codex CLI','Aider','Copilot CLI','SDK / curl'], statSub: 'on output', status: 'active' },
784
- // ], hasProxyToggle: true },
789
+ { category: 'VCONVERSATION COMPRESSION (LIVE CONTEXT)', features: [
790
+ { key: 'conv_claude', title: 'Claude Code Live Compression', desc: 'Compresses active sessions in ~/.claude/projects/ via side-cache', badges: ['Claude Code'], agentKey: 'claude', statSub: 'on conversation', status: 'active' },
791
+ { key: 'conv_gemini', title: 'Google Gemini Code Assistant', desc: 'Compresses Gemini CLI & extension session logs in ~/.config/gemini/', badges: ['Gemini'], agentKey: 'gemini', statSub: 'on gemini logs', status: 'active' },
792
+ { key: 'conv_codex', title: 'Codex Shared Memory Compression', desc: 'Compresses shared memory session logs with Claude', badges: ['Codex'], agentKey: 'codex', statSub: 'on shared memory', status: 'active' },
793
+ { key: 'conv_cursor', title: 'Cursor Workspace State Compression', desc: 'Compresses workspaceStorage JSON/vscdb files in real-time', badges: ['Cursor'], agentKey: 'cursor', statSub: 'on workspace state', status: 'active' },
794
+ { key: 'conv_aider', title: 'Aider Chat History Optimizer', desc: 'Compresses .aider.chat.history.md files automatically on launch', badges: ['Aider'], agentKey: 'aider', statSub: 'on markdown logs', status: 'active' },
795
+ { key: 'conv_copilot', title: 'Copilot CLI Session Trimmer', desc: 'Compresses github-copilot-cli session state logs', badges: ['Copilot CLI'], agentKey: 'copilot', statSub: 'on CLI sessions', status: 'active' },
796
+ { key: 'conv_openclaw', title: 'OpenClaw ContextEngine Plugin', desc: 'Runs as an internal ContextEngine plugin for OpenClaw pipeline', badges: ['OpenClaw'], agentKey: 'openclaw', statSub: 'on plugin context', status: 'active' },
797
+ { key: 'conv_opencode', title: 'OpenCode Config & Proxy Optimizer', desc: 'Injects proxy config and compresses .opencode/sessions payloads', badges: ['OpenCode'], agentKey: 'opencode', statSub: 'on open sessions', status: 'active' },
798
+ { key: 'conv_cortex', title: 'Cortex Code SDK Library Mode', desc: 'SDK wrapper library mode providing 60-65% token savings', badges: ['Cortex Code'], agentKey: 'cortex', statSub: 'on SDK memory', status: 'active' },
799
+ ]},
785
800
  ];
786
801
 
787
802
  let proxyRunning = false;
@@ -875,30 +890,52 @@
875
890
  try { await fetch('/api/config/features', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(csaToggles) }); } catch {}
876
891
  }
877
892
 
878
- function renderCsaSection(logs) {
879
- const pricePerM = PRICING_TABLE[document.getElementById('model-select').value] || 3.00;
880
- const totalRaw = logs.reduce((s, l) => s + l.estimated_raw_tokens, 0);
881
- const totalComp = logs.reduce((s, l) => s + l.estimated_compressed_tokens, 0);
882
- const totalSaved = logs.reduce((s, l) => s + l.tokens_saved, 0);
883
- const totalSecrets = logs.reduce((s, l) => s + (l.secrets_found || 0), 0);
884
- const savedUsd = (totalSaved * pricePerM) / 1000000;
885
- const reductionPct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(1) : '0.0';
886
-
887
- const featureCounts = {};
888
- const featureSavings = {};
889
- logs.forEach(l => {
890
- (l.features || []).forEach(f => {
891
- featureCounts[f] = (featureCounts[f] || 0) + 1;
892
- featureSavings[f] = (featureSavings[f] || 0) + l.tokens_saved;
893
+ async function renderCsaSection(logs) {
894
+ try {
895
+ const modelSelect = document.getElementById('model-select');
896
+ const modelVal = modelSelect ? modelSelect.value : 'claude-opus-4-8';
897
+ const pricePerM = PRICING_TABLE[modelVal] || 3.00;
898
+ const totalRaw = logs.reduce((s, l) => s + (l.estimated_raw_tokens || 0), 0);
899
+ const totalComp = logs.reduce((s, l) => s + (l.estimated_compressed_tokens || 0), 0);
900
+ const totalSaved = logs.reduce((s, l) => s + (l.tokens_saved || 0), 0);
901
+ const totalSecrets = logs.reduce((s, l) => s + (l.secrets_found || 0), 0);
902
+ const savedUsd = (totalSaved * pricePerM) / 1000000;
903
+ const reductionPct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(1) : '0.0';
904
+
905
+ const featureCounts = {};
906
+ const featureSavings = {};
907
+ logs.forEach(l => {
908
+ (l.features || []).forEach(f => {
909
+ featureCounts[f] = (featureCounts[f] || 0) + 1;
910
+ featureSavings[f] = (featureSavings[f] || 0) + (l.tokens_saved || 0);
911
+ });
912
+ if (l.command) {
913
+ const matched = CSA_FEATURES.flatMap(c => c.features).find(feat => feat.agentKey && (l.command.includes(`conv:${feat.agentKey}`) || l.command.includes(feat.agentKey)));
914
+ if (matched) {
915
+ featureCounts[matched.key] = (featureCounts[matched.key] || 0) + 1;
916
+ featureSavings[matched.key] = (featureSavings[matched.key] || 0) + (l.tokens_saved || 0);
917
+ }
918
+ }
893
919
  });
894
- });
895
920
 
896
- const list = document.getElementById('csa-list');
897
- let html = '';
921
+ const list = document.getElementById('csa-list');
922
+ if (!list) return;
923
+ let html = '';
924
+
925
+ let globalConfigData = {};
926
+ try {
927
+ const cfgResp = await fetch('/api/config');
928
+ globalConfigData = await cfgResp.json();
929
+ } catch(e){}
898
930
 
899
931
  CSA_FEATURES.forEach(cat => {
900
932
  html += `<div class="csa-category">${cat.category}</div>`;
901
- cat.features.forEach(f => {
933
+ const sortedFeatures = [...cat.features].sort((a, b) => {
934
+ const savA = featureSavings[a.key] || 0;
935
+ const savB = featureSavings[b.key] || 0;
936
+ return savB - savA;
937
+ });
938
+ sortedFeatures.forEach(f => {
902
939
  const isSoon = f.status === 'soon';
903
940
  const isOff = f.status === 'off';
904
941
  const defaultOn = f.status === 'active';
@@ -906,6 +943,7 @@
906
943
  const count = featureCounts[f.key] || 0;
907
944
  const statusLabels = { active: 'Active', off: 'Off by default', soon: 'Coming soon' };
908
945
  let statVal, statColor;
946
+ let statSubText = f.statSub;
909
947
  if (f.statKey === 'secrets') {
910
948
  statVal = totalSecrets.toLocaleString();
911
949
  statColor = '#EF4444';
@@ -917,21 +955,69 @@
917
955
  const featureSaved = (featureSavedTokens * pricePerM) / 1000000;
918
956
  statVal = '$' + featureSaved.toFixed(2);
919
957
  statColor = '#10B981';
958
+ if (featureSavedTokens > 0) {
959
+ statSubText = `<span style="color:#10B981;font-weight:600;">⚡ ${featureSavedTokens.toLocaleString()} tokens saved</span>`;
960
+ }
920
961
  }
921
962
  const rowClass = isSoon ? 'csa-row is-soon' : isOff ? 'csa-row is-off' : 'csa-row';
963
+
964
+ let liveBadge = `<span class="csa-status ${f.status}">${statusLabels[f.status]}</span>`;
965
+ let latestFileHtml = '';
966
+ let claudeCompactHtml = '';
967
+ if (f.agentKey) {
968
+ const sess = globalConfigData.latestSessions && globalConfigData.latestSessions[f.agentKey];
969
+ let isToday = false;
970
+ if (sess && sess.lastModified) {
971
+ const fileDate = new Date(sess.lastModified).toDateString();
972
+ const todayDate = new Date().toDateString();
973
+ if (fileDate === todayDate) isToday = true;
974
+ }
975
+
976
+ if (isToday) {
977
+ liveBadge = `<span style="background:rgba(16,185,129,0.25);color:#10B981;border:1px solid #10B981;padding:2px 10px;border-radius:12px;font-size:10px;font-weight:700;display:inline-flex;align-items:center;gap:5px;margin-left:8px;box-shadow:0 0 10px rgba(16,185,129,0.2);"><span style="width:7px;height:7px;border-radius:50%;background:#10B981;box-shadow:0 0 6px #10B981;"></span>● LIVE SESSION TODAY</span>`;
978
+ } else {
979
+ liveBadge = `<span style="background:rgba(148,163,184,0.1);color:#94A3B8;border:1px solid rgba(148,163,184,0.2);padding:2px 8px;border-radius:12px;font-size:10px;font-weight:500;display:inline-flex;align-items:center;gap:4px;margin-left:8px;"><span style="width:6px;height:6px;border-radius:50%;background:#64748B;"></span>Idle</span>`;
980
+ }
981
+
982
+ if (f.agentKey) {
983
+ const agentConvLogs = logs.filter(l => l.command && l.command.includes(`conv:${f.agentKey}`));
984
+ const lastLog = agentConvLogs.length > 0 ? agentConvLogs[agentConvLogs.length - 1] : null;
985
+ const savedTokens = (sess && sess.activeSavedTokens > 0) ? sess.activeSavedTokens : (lastLog ? (lastLog.tokens_saved || lastLog.estimated_raw_tokens || 0) : 0);
986
+ if (savedTokens > 0 || (lastLog && lastLog.timestamp)) {
987
+ const timeStr = lastLog && lastLog.timestamp ? new Date(lastLog.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : (sess && sess.lastModified ? new Date(sess.lastModified).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'Today');
988
+ claudeCompactHtml = `<div style="font-size:11px;color:#F59E0B;margin-top:4px;font-family:sans-serif;display:inline-flex;align-items:center;gap:6px;font-weight:600;background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.3);padding:2px 8px;border-radius:6px;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#F59E0B;box-shadow:0 0 6px #F59E0B;"></span>🟡 Last Compacted: ${timeStr} (${savedTokens.toLocaleString()} tokens)</div>`;
989
+ } else {
990
+ claudeCompactHtml = `<div style="font-size:11px;color:#F59E0B;margin-top:4px;font-family:sans-serif;display:inline-flex;align-items:center;gap:6px;font-weight:600;background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.3);padding:2px 8px;border-radius:6px;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#F59E0B;box-shadow:0 0 6px #F59E0B;"></span>🟡 Last Compacted: Active Monitoring</div>`;
991
+ }
992
+ }
993
+
994
+ if (sess && sess.fileName) {
995
+ latestFileHtml = `<div style="font-size:11px;color:#10B981;margin-top:3px;font-family:monospace;display:flex;align-items:center;gap:4px;"><span>📄 Latest Session:</span> <strong style="color:#F1F5F9;">${sess.fileName}</strong></div>`;
996
+ } else {
997
+ latestFileHtml = `<div style="font-size:11px;color:#64748B;margin-top:3px;font-family:monospace;"><span>📄 Waiting for active session file...</span></div>`;
998
+ }
999
+ }
1000
+
922
1001
  html += `<div class="${rowClass}">
923
1002
  <label class="csa-toggle">
924
1003
  <input type="checkbox" ${checked ? 'checked' : ''} ${isSoon ? 'disabled' : ''} onchange="saveCsaToggle('${f.key}', this.checked)">
925
1004
  <span class="slider"></span>
926
1005
  </label>
927
1006
  <div class="csa-info">
928
- <h4>${f.title}<span class="csa-status ${f.status}">${statusLabels[f.status]}</span></h4>
1007
+ <h4>${f.title} ${liveBadge}</h4>
929
1008
  <p>${f.desc}</p>
930
- <div class="csa-badges">${f.badges.map(b => `<span class="csa-badge">${b}</span>`).join('')}</div>
1009
+ ${latestFileHtml}
1010
+ ${claudeCompactHtml}
1011
+ <div class="csa-badges" style="margin-top:6px;">
1012
+ ${f.badges.map(b => `<span class="csa-badge">${b}</span>`).join('')}
1013
+ ${f.agentKey ? `<button type="button" style="background:rgba(245,158,11,0.15);border:1px solid rgba(245,158,11,0.4);color:#F59E0B;font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-left:6px;font-weight:600;" onclick="event.preventDefault(); runManualCompact('${f.agentKey}', this)">⚡ Compact Now</button>` : ''}
1014
+ ${f.agentKey ? `<button style="background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.3);color:#10B981;font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-left:4px;font-weight:600;" onclick="openAgentFolder('${f.agentKey}')">📁 Open Directory</button>` : ''}
1015
+ ${f.agentKey ? `<button style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);color:#94A3B8;font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-left:4px;" onclick="openLocateModal('${f.agentKey}', '${f.title}')">⚙️ Change Path</button>` : ''}
1016
+ </div>
931
1017
  </div>
932
1018
  <div class="csa-stat">
933
1019
  <div class="csa-val" style="color:${statColor};">${statVal}</div>
934
- <div class="csa-sub">${f.statSub}</div>
1020
+ <div class="csa-sub">${statSubText}</div>
935
1021
  </div>
936
1022
  </div>`;
937
1023
  });
@@ -963,14 +1049,87 @@
963
1049
  <div class="csa-sum-label">Secrets Blocked</div>
964
1050
  <div class="csa-sum-val" style="color:var(--red);">${totalSecrets.toLocaleString()}</div>
965
1051
  </div>`;
1052
+ } catch(err) {
1053
+ console.error('Error rendering CSA section:', err);
1054
+ }
1055
+ }
1056
+
1057
+ async function openLocateModal(agentKey, title) {
1058
+ let currentPath = 'Default system path';
1059
+ try {
1060
+ const resp = await fetch('/api/config');
1061
+ const cfg = await resp.json();
1062
+ if (cfg.resolvedPaths && cfg.resolvedPaths[agentKey]) {
1063
+ currentPath = cfg.resolvedPaths[agentKey].join(' ; ');
1064
+ }
1065
+ if (cfg.custom_storage_paths && cfg.custom_storage_paths[agentKey]) {
1066
+ currentPath = cfg.custom_storage_paths[agentKey];
1067
+ }
1068
+ } catch(e){}
1069
+
1070
+ const customPath = prompt(`⚙️ Configure Session Storage Directory for ${title}:\n\nCurrently Monitored Path:\n${currentPath}\n\nEnter custom directory path (or leave empty to reset to default):`, currentPath.includes(' ; ') ? '' : currentPath);
1071
+ if (customPath !== null) {
1072
+ try {
1073
+ await fetch('/api/config/custom-path', {
1074
+ method: 'POST',
1075
+ headers: { 'Content-Type': 'application/json' },
1076
+ body: JSON.stringify({ agentKey, path: customPath.trim() || null })
1077
+ });
1078
+ alert(`Storage path updated for ${title}!\nMonitored directory is now active and saved permanently.`);
1079
+ renderCsa();
1080
+ } catch (e) {
1081
+ alert('Failed to save path.');
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ async function openAgentFolder(agentKey) {
1087
+ try {
1088
+ const resp = await fetch('/api/config/open-dir', {
1089
+ method: 'POST',
1090
+ headers: { 'Content-Type': 'application/json' },
1091
+ body: JSON.stringify({ agentKey })
1092
+ });
1093
+ const res = await resp.json();
1094
+ if (!res.success) {
1095
+ alert('Could not open directory.');
1096
+ }
1097
+ } catch (e) {
1098
+ alert('Error opening directory.');
1099
+ }
1100
+ }
1101
+
1102
+ async function runManualCompact(agentKey, btn) {
1103
+ const origText = btn.innerText;
1104
+ btn.innerText = '⏳ Compacting...';
1105
+ btn.disabled = true;
1106
+ try {
1107
+ const resp = await fetch('/api/agent/compact', {
1108
+ method: 'POST',
1109
+ headers: { 'Content-Type': 'application/json' },
1110
+ body: JSON.stringify({ agentKey })
1111
+ });
1112
+ const res = await resp.json();
1113
+ if (res.ok) {
1114
+ btn.innerText = '✅ Compacted!';
1115
+ setTimeout(() => { btn.innerText = origText; btn.disabled = false; }, 2000);
1116
+ loadStats();
1117
+ } else {
1118
+ alert(res.message || 'Compaction finished');
1119
+ btn.innerText = origText;
1120
+ btn.disabled = false;
1121
+ }
1122
+ } catch(e) {
1123
+ btn.innerText = origText;
1124
+ btn.disabled = false;
1125
+ }
966
1126
  }
967
1127
 
968
1128
  function isAiCommand(log) {
969
- // New logs have ai_detected field explicitly
970
- if (log.ai_detected === true) return true;
971
- if (log.ai_detected === false) return false;
972
- // Old logs (before detection feature): assume AI they went through TrimPrompt shims
973
- return true;
1129
+ if (!log) return false;
1130
+ const cmd = log.command || '';
1131
+ const feats = log.features || [];
1132
+ return cmd.startsWith('conv:') || feats.includes('live-conv') || feats.some(f => typeof f === 'string' && f.startsWith('conv_'));
974
1133
  }
975
1134
 
976
1135
  function setFilter(mode) {
@@ -1123,39 +1282,21 @@
1123
1282
 
1124
1283
  async function loadStats() {
1125
1284
  const select = document.getElementById('model-select');
1126
- const model = select.value;
1285
+ const model = select ? select.value : 'auto';
1127
1286
 
1128
1287
  try {
1129
- // Fetch full history and compute AI-only stats client-side
1130
- const histResp = await fetch('/api/stats/history');
1131
- const allLogs = await histResp.json();
1132
- const aiLogs = allLogs.filter(l => isAiCommand(l));
1133
-
1134
1288
  const summaryResp = await fetch(`/api/stats/summary?model=${model}`);
1289
+ if (!summaryResp.ok) return;
1135
1290
  const data = await summaryResp.json();
1136
1291
 
1137
- // Summary counts ALL commands (all went through TrimPrompt and saved tokens)
1138
- const pricePerM = PRICING_TABLE[data.model_key] || PRICING_TABLE[model] || 3.00;
1139
- let totalRaw = 0, totalComp = 0, totalSaved = 0;
1140
- allLogs.forEach(l => {
1141
- totalRaw += l.estimated_raw_tokens;
1142
- totalComp += l.estimated_compressed_tokens;
1143
- totalSaved += l.tokens_saved;
1144
- });
1145
- const pct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(1) : '0.0';
1146
- const rawCostUsd = (totalRaw * pricePerM) / 1000000;
1147
- const compCostUsd = (totalComp * pricePerM) / 1000000;
1148
- const savedUsd = (totalSaved * pricePerM) / 1000000;
1149
-
1150
- document.getElementById('val-savings').innerText = `$${savedUsd.toFixed(4)}`;
1151
- document.getElementById('val-savings-desc').innerText = `based on ${data.model_name} pricing`;
1152
- document.getElementById('val-orig-cost').innerText = `$${rawCostUsd.toFixed(4)}`;
1153
- document.getElementById('val-comp-cost').innerText = `$${compCostUsd.toFixed(4)}`;
1154
- document.getElementById('val-reduction').innerText = `${pct}%`;
1155
- document.getElementById('val-tokens-desc').innerText = `raw: ${totalRaw.toLocaleString()} → compressed: ${totalComp.toLocaleString()}`;
1156
-
1157
- // Return for chart model resolution
1158
- data.model_key = data.model_key || model;
1292
+ if (data && data.raw_cost_usd !== undefined) {
1293
+ document.getElementById('val-savings').innerText = `$${data.money_saved_usd.toFixed(4)}`;
1294
+ document.getElementById('val-savings-desc').innerText = `based on ${data.model_name} pricing`;
1295
+ document.getElementById('val-orig-cost').innerText = `$${data.raw_cost_usd.toFixed(4)}`;
1296
+ document.getElementById('val-comp-cost').innerText = `$${data.compressed_cost_usd.toFixed(4)}`;
1297
+ document.getElementById('val-reduction').innerText = `${data.savings_percentage}%`;
1298
+ document.getElementById('val-tokens-desc').innerText = `raw: ${data.total_raw_tokens.toLocaleString()} → compressed: ${data.total_compressed_tokens.toLocaleString()}`;
1299
+ }
1159
1300
  return data;
1160
1301
  } catch (err) {
1161
1302
  console.error('Failed to fetch summary stats', err);
@@ -1164,27 +1305,25 @@
1164
1305
 
1165
1306
  async function loadHistory() {
1166
1307
  const select = document.getElementById('model-select');
1167
- const model = select.value;
1308
+ const model = select ? select.value : 'auto';
1309
+
1310
+ loadStats();
1168
1311
 
1169
1312
  try {
1170
1313
  const response = await fetch('/api/stats/history');
1314
+ if (!response.ok) return;
1171
1315
  const logs = await response.json();
1316
+ if (!Array.isArray(logs)) return;
1172
1317
 
1173
- let resolvedModel = model;
1174
- if (model === 'auto') {
1175
- const stats = await loadStats();
1176
- if (stats && stats.model_key) {
1177
- resolvedModel = stats.model_key;
1178
- }
1179
- }
1318
+ const resolvedModel = model === 'auto' ? 'claude-opus-4-8' : model;
1180
1319
 
1181
- // Filter logs: AI-only for chart and summary, all for table if toggled
1320
+ // Filter logs: AI-only for chart
1182
1321
  const aiLogs = logs.filter(l => isAiCommand(l));
1183
1322
 
1184
- // Chart + summary always use AI-only logs
1323
+ // Chart always uses AI-only logs
1185
1324
  renderChart(aiLogs, resolvedModel);
1186
1325
 
1187
- // CSA Feature Controls
1326
+ // CSA Feature Controls (Run asynchronously in background)
1188
1327
  renderCsaSection(logs);
1189
1328
 
1190
1329
  // Security banner — aggregate across all logs
@@ -1223,53 +1362,118 @@
1223
1362
  return;
1224
1363
  }
1225
1364
 
1226
- const INITIAL_LIMIT = 100;
1227
- const reversed = displayLogs.reverse();
1365
+ // Group logs by command name
1366
+ const groups = {};
1367
+ displayLogs.forEach(l => {
1368
+ const key = l.command || 'unknown';
1369
+ if (!groups[key]) groups[key] = [];
1370
+ groups[key].push(l);
1371
+ });
1372
+
1373
+ // Sort groups by most recent log timestamp descending
1374
+ const sortedGroupKeys = Object.keys(groups).sort((a, b) => {
1375
+ const lastA = new Date(groups[a][groups[a].length - 1].timestamp).getTime();
1376
+ const lastB = new Date(groups[b][groups[b].length - 1].timestamp).getTime();
1377
+ return lastB - lastA;
1378
+ });
1379
+
1228
1380
  tbody.innerHTML = '';
1229
1381
 
1230
- function renderRow(log) {
1231
- const tr = document.createElement('tr');
1232
- const time = new Date(log.timestamp).toLocaleTimeString();
1233
- const savingsPct = log.raw_chars > 0
1234
- ? ((log.estimated_raw_tokens - log.estimated_compressed_tokens) / log.estimated_raw_tokens * 100).toFixed(1)
1235
- : '0.0';
1236
- tr.innerHTML = `
1237
- <td>${time}</td>
1238
- <td style="font-weight: 500;">${log.command}${log.secrets_found ? ' <span style="color:#F09595;font-size:11px;" title="' + log.secrets_found + ' secrets redacted">&#x1f6e1;' + log.secrets_found + '</span>' : ''}</td>
1239
- <td>${featureTagHtml(log.features)}</td>
1240
- <td>${log.estimated_raw_tokens.toLocaleString()}</td>
1241
- <td>${log.estimated_compressed_tokens.toLocaleString()}</td>
1242
- <td style="color: var(--emerald); font-weight: 600;">${savingsPct}%</td>
1243
- <td>
1244
- <span class="status-badge ${log.exit_code === 0 ? 'success' : 'fail'}">
1245
- ${log.exit_code}
1246
- </span>
1247
- </td>
1248
- <td>
1249
- <button class="btn-inspect" data-id="${log.id}" data-cmd="${log.command}" data-sec="${log.secrets_found || 0}" data-types="${(log.secret_types || []).join(',')}" onclick="inspectFromBtn(this)">Inspect</button>
1250
- </td>
1382
+ sortedGroupKeys.forEach((cmdKey, gIdx) => {
1383
+ const groupLogs = groups[cmdKey].reverse(); // Most recent first
1384
+ const latestLog = groupLogs[0];
1385
+ const latestTime = new Date(latestLog.timestamp).toLocaleTimeString();
1386
+
1387
+ let grpRaw = 0, grpComp = 0, grpSaved = 0, grpSecrets = 0;
1388
+ const isLiveConvGroup = cmdKey.startsWith('conv:') || (latestLog.features && latestLog.features.includes('live-conv'));
1389
+ if (isLiveConvGroup) {
1390
+ grpRaw = latestLog.estimated_raw_tokens || 0;
1391
+ grpComp = latestLog.estimated_compressed_tokens || 0;
1392
+ grpSaved = grpRaw - grpComp;
1393
+ groupLogs.forEach(l => { grpSecrets += (l.secrets_found || 0); });
1394
+ } else {
1395
+ groupLogs.forEach(l => {
1396
+ grpRaw += (l.estimated_raw_tokens || 0);
1397
+ grpComp += (l.estimated_compressed_tokens || 0);
1398
+ grpSaved += (l.tokens_saved || 0);
1399
+ grpSecrets += (l.secrets_found || 0);
1400
+ });
1401
+ }
1402
+ const grpSavingsPct = grpRaw > 0 ? ((grpSaved / grpRaw) * 100).toFixed(1) : '0.0';
1403
+ const groupId = `grp-${gIdx}`;
1404
+ const isGroupExpanded = expandedGroupKeys.has(cmdKey);
1405
+
1406
+ const trHead = document.createElement('tr');
1407
+ trHead.style.background = 'rgba(255,255,255,0.03)';
1408
+ trHead.style.cursor = 'pointer';
1409
+ trHead.style.borderBottom = '1px solid rgba(255,255,255,0.06)';
1410
+ trHead.onclick = () => {
1411
+ const el = document.querySelectorAll(`.${groupId}`);
1412
+ const chev = document.getElementById(`chev-${groupId}`);
1413
+ const isHidden = el[0] && el[0].style.display === 'none';
1414
+ if (isHidden) {
1415
+ expandedGroupKeys.add(cmdKey);
1416
+ } else {
1417
+ expandedGroupKeys.delete(cmdKey);
1418
+ }
1419
+ el.forEach(row => row.style.display = isHidden ? '' : 'none');
1420
+ if (chev) chev.style.transform = isHidden ? 'rotate(90deg)' : 'rotate(0deg)';
1421
+ };
1422
+
1423
+ const displayCmd = formatCommandName(latestLog.command);
1424
+
1425
+ trHead.innerHTML = `
1426
+ <td style="font-size:12px;color:#94A3B8;white-space:nowrap;"><span id="chev-${groupId}" style="display:inline-block;transition:transform 0.2s;margin-right:6px;font-size:10px;transform:${isGroupExpanded ? 'rotate(90deg)' : 'rotate(0deg)'}">▶</span>${latestTime}</td>
1427
+ <td style="font-weight: 600;color:#F1F5F9;text-transform:capitalize;">${displayCmd} <span style="font-size:11px;color:#818CF8;font-weight:400;margin-left:6px;">(${groupLogs.length} runs)</span>${grpSecrets ? ' <span style="color:#F09595;font-size:11px;" title="' + grpSecrets + ' secrets redacted">&#x1f6e1;' + grpSecrets + '</span>' : ''}</td>
1428
+ <td>${featureTagHtml(latestLog.features)}</td>
1429
+ <td style="font-weight:500;">${grpRaw.toLocaleString()}</td>
1430
+ <td style="font-weight:500;">${grpComp.toLocaleString()}</td>
1431
+ <td style="color: var(--emerald); font-weight: 700;">${grpSavingsPct}%</td>
1432
+ <td><span class="status-badge success">Active</span></td>
1433
+ <td><button class="btn-inspect" style="background:rgba(129,140,248,0.15);color:#818CF8;border:1px solid rgba(129,140,248,0.3);" onclick="event.stopPropagation(); this.closest('tr').click();">Details (${groupLogs.length})</button></td>
1251
1434
  `;
1252
- return tr;
1253
- }
1254
-
1255
- reversed.slice(0, INITIAL_LIMIT).forEach(log => tbody.appendChild(renderRow(log)));
1256
-
1257
- if (reversed.length > INITIAL_LIMIT) {
1258
- const moreTr = document.createElement('tr');
1259
- moreTr.id = 'show-more-row';
1260
- moreTr.innerHTML = `<td colspan="8" style="text-align:center;padding:12px;">
1261
- <button onclick="document.getElementById('show-more-row').remove();document.querySelectorAll('.hidden-log-row').forEach(r=>r.style.display='')" style="background:rgba(127,119,221,0.15);color:#7F77DD;border:none;padding:8px 24px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;">
1262
- Show ${reversed.length - INITIAL_LIMIT} more entries
1263
- </button>
1264
- </td>`;
1265
- tbody.appendChild(moreTr);
1266
- reversed.slice(INITIAL_LIMIT).forEach(log => {
1267
- const tr = renderRow(log);
1268
- tr.className = 'hidden-log-row';
1269
- tr.style.display = 'none';
1435
+ tbody.appendChild(trHead);
1436
+
1437
+ // Append child rows
1438
+ groupLogs.forEach(log => {
1439
+ const tr = document.createElement('tr');
1440
+ tr.className = groupId;
1441
+ tr.style.display = isGroupExpanded ? '' : 'none';
1442
+ tr.style.background = 'rgba(0,0,0,0.2)';
1443
+ const time = new Date(log.timestamp).toLocaleTimeString();
1444
+ const savingsPct = log.raw_chars > 0
1445
+ ? ((log.estimated_raw_tokens - log.estimated_compressed_tokens) / log.estimated_raw_tokens * 100).toFixed(1)
1446
+ : '0.0';
1447
+ const isConvLog = log.command && log.command.includes('conv:');
1448
+ let convBadgeHtml = '';
1449
+ if (log.features && log.features.includes('trim_offload')) {
1450
+ convBadgeHtml = `<div style="font-size:10px;color:#10B981;margin-top:4px;padding:2px 8px;border-radius:4px;background:rgba(16,185,129,0.1);display:inline-block;font-weight:600;">⚡ TrimPrompt Side-Cache Offloaded ${log.estimated_raw_tokens.toLocaleString()} Tokens</div>`;
1451
+ } else if (log.features && log.features.includes('official_compact')) {
1452
+ convBadgeHtml = `<div style="font-size:10px;color:#818CF8;margin-top:4px;padding:2px 8px;border-radius:4px;background:rgba(129,140,248,0.1);display:inline-block;font-weight:600;">📦 Official Agent Compact Saved ${log.estimated_raw_tokens.toLocaleString()} Tokens</div>`;
1453
+ } else if (isConvLog) {
1454
+ convBadgeHtml = `<div style="font-size:10px;color:#F59E0B;margin-top:4px;padding:2px 8px;border-radius:4px;background:rgba(245,158,11,0.1);display:inline-block;font-weight:500;">🔄 Active Session Context Monitored</div>`;
1455
+ }
1456
+ const childCmd = formatCommandName(log.command);
1457
+
1458
+ tr.innerHTML = `
1459
+ <td style="padding-left:24px;font-size:11px;color:#64748B;white-space:nowrap;">↳ ${time}</td>
1460
+ <td style="font-size:12px;color:#CBD5E1;text-transform:capitalize;">${childCmd}${convBadgeHtml}</td>
1461
+ <td>${featureTagHtml(log.features)}</td>
1462
+ <td style="font-size:12px;">${log.estimated_raw_tokens.toLocaleString()}</td>
1463
+ <td style="font-size:12px;">${log.estimated_compressed_tokens.toLocaleString()}</td>
1464
+ <td style="color: var(--emerald); font-size:12px;">${savingsPct}%</td>
1465
+ <td>
1466
+ <span class="status-badge ${log.exit_code === 0 ? 'success' : 'fail'}">
1467
+ ${log.exit_code}
1468
+ </span>
1469
+ </td>
1470
+ <td>
1471
+ <button class="btn-inspect" data-id="${log.id}" data-cmd="${log.command}" data-sec="${log.secrets_found || 0}" data-types="${(log.secret_types || []).join(',')}" onclick="inspectFromBtn(this)">Inspect</button>
1472
+ </td>
1473
+ `;
1270
1474
  tbody.appendChild(tr);
1271
1475
  });
1272
- }
1476
+ });
1273
1477
  } catch (err) {
1274
1478
  console.error('Failed to fetch history logs', err);
1275
1479
  }
@@ -1279,6 +1483,15 @@
1279
1483
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1280
1484
  }
1281
1485
 
1486
+ function formatCommandName(cmd) {
1487
+ if (!cmd) return 'unknown';
1488
+ if (cmd.includes('conv:')) {
1489
+ const parts = cmd.split('conv:');
1490
+ return parts[1] || cmd;
1491
+ }
1492
+ return cmd;
1493
+ }
1494
+
1282
1495
  // Human-friendly labels for redactor rule types (see redactor.ts)
1283
1496
  const SECRET_TYPE_LABELS = {
1284
1497
  aws_key: 'AWS access key',
@@ -1355,7 +1568,8 @@
1355
1568
 
1356
1569
  async function inspectDiff(id, command, secretsFound, secretTypes) {
1357
1570
  try {
1358
- document.getElementById('modal-command-title').innerText = `Inspector: trim ${command}`;
1571
+ const cleanCmd = formatCommandName(command);
1572
+ document.getElementById('modal-command-title').innerText = `Inspector: trim ${cleanCmd}`;
1359
1573
  document.getElementById('diff-raw').innerText = 'Loading...';
1360
1574
  document.getElementById('diff-comp').innerText = 'Loading...';
1361
1575
  document.getElementById('diff-modal').style.display = 'flex';
@@ -1363,8 +1577,13 @@
1363
1577
  const response = await fetch(`/api/stats/inspect/${id}`);
1364
1578
  const data = await response.json();
1365
1579
 
1366
- document.getElementById('diff-raw').innerHTML = highlightRedacted(escapeHtml(data.raw), 'red');
1367
- document.getElementById('diff-comp').innerHTML = highlightRedacted(escapeHtml(data.compressed), 'green');
1580
+ if (command && command.includes('conv:')) {
1581
+ document.getElementById('diff-raw').innerHTML = `<div style="color:#F59E0B;font-size:13px;padding:24px;font-weight:600;line-height:1.6;">⚡ Live Conversation Context Monitor Active<br><span style="font-size:12px;color:#94A3B8;font-weight:400;">Savings achieved via conversation compaction (Native Hook & Side-Cache active)</span></div>`;
1582
+ document.getElementById('diff-comp').innerHTML = `<div style="color:#10B981;font-size:13px;padding:24px;font-weight:600;line-height:1.6;">✅ Tokens Trimmed & Offloaded Live<br><span style="font-size:12px;color:#94A3B8;font-weight:400;">Active memory trimming and real-time token savings within conversation</span></div>`;
1583
+ } else {
1584
+ document.getElementById('diff-raw').innerHTML = highlightRedacted(escapeHtml(data.raw), 'red');
1585
+ document.getElementById('diff-comp').innerHTML = highlightRedacted(escapeHtml(data.compressed), 'green');
1586
+ }
1368
1587
 
1369
1588
  const info = detectFilterType(data.raw, data.compressed, command);
1370
1589
  const bar = document.getElementById('inspect-info-bar');