trimprompt 1.0.29 → 1.0.31

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)' },
@@ -776,11 +786,17 @@
776
786
  { category: 'S — Security', features: [
777
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' },
778
788
  ]},
779
- // --- PProxy section hidden for now (interceptor not functional yet). Re-enable by uncommenting. ---
780
- // { category: 'P Proxy (Universal AI API Compression)', features: [
781
- // { 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' },
782
- // { 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' },
783
- // ], 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
+ ]},
784
800
  ];
785
801
 
786
802
  let proxyRunning = false;
@@ -874,30 +890,52 @@
874
890
  try { await fetch('/api/config/features', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(csaToggles) }); } catch {}
875
891
  }
876
892
 
877
- function renderCsaSection(logs) {
878
- const pricePerM = PRICING_TABLE[document.getElementById('model-select').value] || 3.00;
879
- const totalRaw = logs.reduce((s, l) => s + l.estimated_raw_tokens, 0);
880
- const totalComp = logs.reduce((s, l) => s + l.estimated_compressed_tokens, 0);
881
- const totalSaved = logs.reduce((s, l) => s + l.tokens_saved, 0);
882
- const totalSecrets = logs.reduce((s, l) => s + (l.secrets_found || 0), 0);
883
- const savedUsd = (totalSaved * pricePerM) / 1000000;
884
- const reductionPct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(1) : '0.0';
885
-
886
- const featureCounts = {};
887
- const featureSavings = {};
888
- logs.forEach(l => {
889
- (l.features || []).forEach(f => {
890
- featureCounts[f] = (featureCounts[f] || 0) + 1;
891
- 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
+ }
892
919
  });
893
- });
894
920
 
895
- const list = document.getElementById('csa-list');
896
- 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){}
897
930
 
898
931
  CSA_FEATURES.forEach(cat => {
899
932
  html += `<div class="csa-category">${cat.category}</div>`;
900
- 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 => {
901
939
  const isSoon = f.status === 'soon';
902
940
  const isOff = f.status === 'off';
903
941
  const defaultOn = f.status === 'active';
@@ -905,6 +943,7 @@
905
943
  const count = featureCounts[f.key] || 0;
906
944
  const statusLabels = { active: 'Active', off: 'Off by default', soon: 'Coming soon' };
907
945
  let statVal, statColor;
946
+ let statSubText = f.statSub;
908
947
  if (f.statKey === 'secrets') {
909
948
  statVal = totalSecrets.toLocaleString();
910
949
  statColor = '#EF4444';
@@ -916,21 +955,69 @@
916
955
  const featureSaved = (featureSavedTokens * pricePerM) / 1000000;
917
956
  statVal = '$' + featureSaved.toFixed(2);
918
957
  statColor = '#10B981';
958
+ if (featureSavedTokens > 0) {
959
+ statSubText = `<span style="color:#10B981;font-weight:600;">⚡ ${featureSavedTokens.toLocaleString()} tokens saved</span>`;
960
+ }
919
961
  }
920
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
+
921
1001
  html += `<div class="${rowClass}">
922
1002
  <label class="csa-toggle">
923
1003
  <input type="checkbox" ${checked ? 'checked' : ''} ${isSoon ? 'disabled' : ''} onchange="saveCsaToggle('${f.key}', this.checked)">
924
1004
  <span class="slider"></span>
925
1005
  </label>
926
1006
  <div class="csa-info">
927
- <h4>${f.title}<span class="csa-status ${f.status}">${statusLabels[f.status]}</span></h4>
1007
+ <h4>${f.title} ${liveBadge}</h4>
928
1008
  <p>${f.desc}</p>
929
- <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>
930
1017
  </div>
931
1018
  <div class="csa-stat">
932
1019
  <div class="csa-val" style="color:${statColor};">${statVal}</div>
933
- <div class="csa-sub">${f.statSub}</div>
1020
+ <div class="csa-sub">${statSubText}</div>
934
1021
  </div>
935
1022
  </div>`;
936
1023
  });
@@ -962,14 +1049,87 @@
962
1049
  <div class="csa-sum-label">Secrets Blocked</div>
963
1050
  <div class="csa-sum-val" style="color:var(--red);">${totalSecrets.toLocaleString()}</div>
964
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
+ }
965
1126
  }
966
1127
 
967
1128
  function isAiCommand(log) {
968
- // New logs have ai_detected field explicitly
969
- if (log.ai_detected === true) return true;
970
- if (log.ai_detected === false) return false;
971
- // Old logs (before detection feature): assume AI they went through TrimPrompt shims
972
- 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_'));
973
1133
  }
974
1134
 
975
1135
  function setFilter(mode) {
@@ -1122,39 +1282,21 @@
1122
1282
 
1123
1283
  async function loadStats() {
1124
1284
  const select = document.getElementById('model-select');
1125
- const model = select.value;
1285
+ const model = select ? select.value : 'auto';
1126
1286
 
1127
1287
  try {
1128
- // Fetch full history and compute AI-only stats client-side
1129
- const histResp = await fetch('/api/stats/history');
1130
- const allLogs = await histResp.json();
1131
- const aiLogs = allLogs.filter(l => isAiCommand(l));
1132
-
1133
1288
  const summaryResp = await fetch(`/api/stats/summary?model=${model}`);
1289
+ if (!summaryResp.ok) return;
1134
1290
  const data = await summaryResp.json();
1135
1291
 
1136
- // Summary counts ALL commands (all went through TrimPrompt and saved tokens)
1137
- const pricePerM = PRICING_TABLE[data.model_key] || PRICING_TABLE[model] || 3.00;
1138
- let totalRaw = 0, totalComp = 0, totalSaved = 0;
1139
- allLogs.forEach(l => {
1140
- totalRaw += l.estimated_raw_tokens;
1141
- totalComp += l.estimated_compressed_tokens;
1142
- totalSaved += l.tokens_saved;
1143
- });
1144
- const pct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(1) : '0.0';
1145
- const rawCostUsd = (totalRaw * pricePerM) / 1000000;
1146
- const compCostUsd = (totalComp * pricePerM) / 1000000;
1147
- const savedUsd = (totalSaved * pricePerM) / 1000000;
1148
-
1149
- document.getElementById('val-savings').innerText = `$${savedUsd.toFixed(4)}`;
1150
- document.getElementById('val-savings-desc').innerText = `based on ${data.model_name} pricing`;
1151
- document.getElementById('val-orig-cost').innerText = `$${rawCostUsd.toFixed(4)}`;
1152
- document.getElementById('val-comp-cost').innerText = `$${compCostUsd.toFixed(4)}`;
1153
- document.getElementById('val-reduction').innerText = `${pct}%`;
1154
- document.getElementById('val-tokens-desc').innerText = `raw: ${totalRaw.toLocaleString()} → compressed: ${totalComp.toLocaleString()}`;
1155
-
1156
- // Return for chart model resolution
1157
- 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
+ }
1158
1300
  return data;
1159
1301
  } catch (err) {
1160
1302
  console.error('Failed to fetch summary stats', err);
@@ -1163,27 +1305,25 @@
1163
1305
 
1164
1306
  async function loadHistory() {
1165
1307
  const select = document.getElementById('model-select');
1166
- const model = select.value;
1308
+ const model = select ? select.value : 'auto';
1309
+
1310
+ loadStats();
1167
1311
 
1168
1312
  try {
1169
1313
  const response = await fetch('/api/stats/history');
1314
+ if (!response.ok) return;
1170
1315
  const logs = await response.json();
1316
+ if (!Array.isArray(logs)) return;
1171
1317
 
1172
- let resolvedModel = model;
1173
- if (model === 'auto') {
1174
- const stats = await loadStats();
1175
- if (stats && stats.model_key) {
1176
- resolvedModel = stats.model_key;
1177
- }
1178
- }
1318
+ const resolvedModel = model === 'auto' ? 'claude-opus-4-8' : model;
1179
1319
 
1180
- // Filter logs: AI-only for chart and summary, all for table if toggled
1320
+ // Filter logs: AI-only for chart
1181
1321
  const aiLogs = logs.filter(l => isAiCommand(l));
1182
1322
 
1183
- // Chart + summary always use AI-only logs
1323
+ // Chart always uses AI-only logs
1184
1324
  renderChart(aiLogs, resolvedModel);
1185
1325
 
1186
- // CSA Feature Controls
1326
+ // CSA Feature Controls (Run asynchronously in background)
1187
1327
  renderCsaSection(logs);
1188
1328
 
1189
1329
  // Security banner — aggregate across all logs
@@ -1222,53 +1362,118 @@
1222
1362
  return;
1223
1363
  }
1224
1364
 
1225
- const INITIAL_LIMIT = 100;
1226
- 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
+
1227
1380
  tbody.innerHTML = '';
1228
1381
 
1229
- function renderRow(log) {
1230
- const tr = document.createElement('tr');
1231
- const time = new Date(log.timestamp).toLocaleTimeString();
1232
- const savingsPct = log.raw_chars > 0
1233
- ? ((log.estimated_raw_tokens - log.estimated_compressed_tokens) / log.estimated_raw_tokens * 100).toFixed(1)
1234
- : '0.0';
1235
- tr.innerHTML = `
1236
- <td>${time}</td>
1237
- <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>
1238
- <td>${featureTagHtml(log.features)}</td>
1239
- <td>${log.estimated_raw_tokens.toLocaleString()}</td>
1240
- <td>${log.estimated_compressed_tokens.toLocaleString()}</td>
1241
- <td style="color: var(--emerald); font-weight: 600;">${savingsPct}%</td>
1242
- <td>
1243
- <span class="status-badge ${log.exit_code === 0 ? 'success' : 'fail'}">
1244
- ${log.exit_code}
1245
- </span>
1246
- </td>
1247
- <td>
1248
- <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>
1249
- </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>
1250
1434
  `;
1251
- return tr;
1252
- }
1253
-
1254
- reversed.slice(0, INITIAL_LIMIT).forEach(log => tbody.appendChild(renderRow(log)));
1255
-
1256
- if (reversed.length > INITIAL_LIMIT) {
1257
- const moreTr = document.createElement('tr');
1258
- moreTr.id = 'show-more-row';
1259
- moreTr.innerHTML = `<td colspan="8" style="text-align:center;padding:12px;">
1260
- <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;">
1261
- Show ${reversed.length - INITIAL_LIMIT} more entries
1262
- </button>
1263
- </td>`;
1264
- tbody.appendChild(moreTr);
1265
- reversed.slice(INITIAL_LIMIT).forEach(log => {
1266
- const tr = renderRow(log);
1267
- tr.className = 'hidden-log-row';
1268
- 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
+ `;
1269
1474
  tbody.appendChild(tr);
1270
1475
  });
1271
- }
1476
+ });
1272
1477
  } catch (err) {
1273
1478
  console.error('Failed to fetch history logs', err);
1274
1479
  }
@@ -1278,6 +1483,15 @@
1278
1483
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1279
1484
  }
1280
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
+
1281
1495
  // Human-friendly labels for redactor rule types (see redactor.ts)
1282
1496
  const SECRET_TYPE_LABELS = {
1283
1497
  aws_key: 'AWS access key',
@@ -1354,7 +1568,8 @@
1354
1568
 
1355
1569
  async function inspectDiff(id, command, secretsFound, secretTypes) {
1356
1570
  try {
1357
- document.getElementById('modal-command-title').innerText = `Inspector: trim ${command}`;
1571
+ const cleanCmd = formatCommandName(command);
1572
+ document.getElementById('modal-command-title').innerText = `Inspector: trim ${cleanCmd}`;
1358
1573
  document.getElementById('diff-raw').innerText = 'Loading...';
1359
1574
  document.getElementById('diff-comp').innerText = 'Loading...';
1360
1575
  document.getElementById('diff-modal').style.display = 'flex';
@@ -1362,8 +1577,13 @@
1362
1577
  const response = await fetch(`/api/stats/inspect/${id}`);
1363
1578
  const data = await response.json();
1364
1579
 
1365
- document.getElementById('diff-raw').innerHTML = highlightRedacted(escapeHtml(data.raw), 'red');
1366
- 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
+ }
1367
1587
 
1368
1588
  const info = detectFilterType(data.raw, data.compressed, command);
1369
1589
  const bar = document.getElementById('inspect-info-bar');