lensmcp 1.10.0 → 1.12.0

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.
@@ -20541,11 +20541,7 @@ var PAGE = `<!doctype html>
20541
20541
  .sum { margin-left: auto; font-size: 11px; color: #6f84a3; white-space: nowrap; }
20542
20542
  .val { margin: 8px 0 0; max-height: 200px; overflow: auto; font-size: 12px; color: #9fb3cd; white-space: pre-wrap; word-break: break-word; }
20543
20543
  /* ---- flow trace view ---- */
20544
- .flow-chips { display: flex; gap: 8px; flex-wrap: wrap; }
20545
- .chip { background: #131a25; border: 1px solid #233045; color: #aebfd6; border-radius: 999px; padding: 4px 12px; cursor: pointer; font: inherit; }
20546
- .chip:hover { border-color: #3a5b85; }
20547
- .chip.active { background: #16263c; border-color: #3fb950; color: #d9e6f5; }
20548
- .chart-wrap { margin-top: 10px; background: #0d121b; border: 1px solid #1c2430; border-radius: 10px; overflow: hidden; display: flex; align-items: stretch; }
20544
+ .chart-wrap { background: #0d121b; border: 1px solid #1c2430; border-radius: 10px; overflow: hidden; display: flex; align-items: stretch; }
20549
20545
  .chart-left { flex: 1 1 auto; min-width: 0; }
20550
20546
  .chart-meta { display: flex; gap: 14px; padding: 8px 12px 0; color: #8aa0bd; font-size: 12px; flex-wrap: wrap; }
20551
20547
  .chart-scroll { overflow: auto; max-height: 76vh; }
@@ -20573,6 +20569,18 @@ var PAGE = `<!doctype html>
20573
20569
  .edge { fill: none; stroke: #3a4d6b; stroke-width: 1.4; }
20574
20570
  .edge.cross { stroke: #5b9dd9; stroke-dasharray: 5 4; }
20575
20571
  .detail { padding: 10px 12px; border-top: 1px solid #1c2430; color: #9fb3cd; font-size: 12px; white-space: pre-wrap; word-break: break-word; max-height: 180px; overflow: auto; display: none; }
20572
+ /* ---- flows view: scrollable list on the left, trace on the right ---- */
20573
+ .flows-layout { display: flex; gap: 12px; align-items: flex-start; }
20574
+ .flow-list-pane { width: 264px; flex: none; position: sticky; top: 64px; }
20575
+ .flow-list-pane .group-title { margin: 4px 0 8px; }
20576
+ .flow-list { max-height: calc(100vh - 150px); overflow-y: auto; border: 1px solid #1c2430; border-radius: 8px; background: #0a0f16; }
20577
+ .flow-item { display: block; width: 100%; text-align: left; background: transparent; border: 0; border-bottom: 1px solid #0f1620; border-left: 2px solid transparent; color: #aebfd6; padding: 7px 11px; cursor: pointer; font: inherit; }
20578
+ .flow-item:hover { background: #0e1622; }
20579
+ .flow-item.active { background: #16263c; border-left-color: #3fb950; }
20580
+ .flow-item .fi-title { color: #dbe6f3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
20581
+ .flow-item .fi-meta { color: #61708a; font-size: 11px; margin-top: 2px; }
20582
+ .flow-main { flex: 1; min-width: 0; }
20583
+ .flow-empty { padding: 28px 6px; }
20576
20584
  /* ---- navigation + cluster view ---- */
20577
20585
  .tabs { display: flex; gap: 2px; margin-left: 14px; }
20578
20586
  .tabs a { color: #8aa0bd; text-decoration: none; padding: 5px 12px; border-radius: 7px; border: 1px solid transparent; }
@@ -20596,14 +20604,21 @@ var PAGE = `<!doctype html>
20596
20604
  .logs-bar button { background: #131a25; border: 1px solid #233045; color: #aebfd6; border-radius: 7px; padding: 5px 12px; cursor: pointer; font: inherit; }
20597
20605
  .logs-bar button:hover { border-color: #3a5b85; }
20598
20606
  .logs-tail { color: #8aa0bd; font-size: 12px; display: flex; gap: 4px; align-items: center; }
20607
+ .logs-trace { display: inline-flex; align-items: center; gap: 7px; background: #16263c; border: 1px solid #3fb95066; color: #9fe0b5; border-radius: 999px; padding: 4px 11px; cursor: pointer; font: 11px ui-monospace, Menlo, monospace; white-space: nowrap; }
20608
+ .logs-trace:hover { border-color: #3fb950; }
20609
+ .logs-trace b { color: #d9e6f5; font-weight: 600; }
20599
20610
  .logs-list { height: calc(100vh - 150px); overflow-y: auto; border: 1px solid #1c2430; border-radius: 8px; background: #0a0f16; font: 11.5px/1.55 ui-monospace, Menlo, monospace; }
20600
20611
  .log-row { display: flex; gap: 10px; padding: 2px 10px; border-bottom: 1px solid #0f1620; white-space: nowrap; }
20601
20612
  .log-row:hover { background: #0e1622; }
20602
20613
  .log-row.jump { cursor: pointer; }
20603
20614
  .log-row .l-time { color: #5f7593; flex: 0 0 92px; }
20604
20615
  .log-row .l-src { color: #7fa3cf; flex: 0 0 104px; overflow: hidden; text-overflow: ellipsis; }
20616
+ .log-row .l-cat { color: #6f84a3; flex: 0 0 66px; overflow: hidden; text-overflow: ellipsis; font-size: 10.5px; align-self: center; }
20605
20617
  .log-row .l-sev { flex: 0 0 56px; text-transform: uppercase; font-size: 10px; align-self: center; }
20606
20618
  .log-row .l-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; color: #c6d4e6; }
20619
+ .log-row .l-count { flex: none; align-self: center; color: #d29922; background: rgba(210,153,34,.12); border: 1px solid #d2992233; border-radius: 999px; padding: 0 7px; font-size: 10px; }
20620
+ .log-row .l-flow { flex: none; align-self: center; color: #79b8ff; cursor: pointer; font-size: 10.5px; opacity: .75; }
20621
+ .log-row .l-flow:hover { opacity: 1; text-decoration: underline; }
20607
20622
  .log-row.sev-debug .l-sev { color: #5f7593; }
20608
20623
  .log-row.sev-info .l-sev { color: #4f9d6b; }
20609
20624
  .log-row.sev-warning { background: rgba(217,162,60,.07); } .log-row.sev-warning .l-sev { color: #d9a23c; }
@@ -20736,26 +20751,31 @@ var PAGE = `<!doctype html>
20736
20751
  </header>
20737
20752
  <main>
20738
20753
  <section class="view" id="view-flows">
20739
- <section class="group">
20740
- <h2 class="group-title">Flows \u2014 pick one to see its trace</h2>
20741
- <div class="flow-chips" id="flow-chips"><span class="muted">No flows yet \u2014 interact with the app under agent-dev.</span></div>
20742
- <div class="chart-wrap" id="chart-wrap" style="display:none">
20743
- <div class="chart-left">
20744
- <div class="chart-meta" id="chart-meta"></div>
20745
- <div id="flow-root" style="height:76vh;display:none"></div>
20746
- <div class="chart-scroll" id="chart-scroll"></div>
20747
- <div class="detail" id="detail"></div>
20748
- </div>
20749
- <aside class="step-panel" id="step-panel">
20750
- <div class="step-nav">
20751
- <button id="step-prev" title="previous step (\u2190)">\u25C0</button>
20752
- <button id="step-next" title="next step (\u2192)">\u25B6</button>
20753
- <span class="step-count" id="step-count"></span>
20754
+ <div class="flows-layout">
20755
+ <aside class="flow-list-pane">
20756
+ <h2 class="group-title">Flows \xB7 latest first</h2>
20757
+ <div class="flow-list" id="flow-list"><span class="muted">No flows yet \u2014 interact with the app under agent-dev.</span></div>
20758
+ </aside>
20759
+ <div class="flow-main">
20760
+ <div class="flow-empty muted" id="flow-empty">Pick a flow on the left to see its trace.</div>
20761
+ <div class="chart-wrap" id="chart-wrap" style="display:none">
20762
+ <div class="chart-left">
20763
+ <div class="chart-meta" id="chart-meta"></div>
20764
+ <div id="flow-root" style="height:76vh;display:none"></div>
20765
+ <div class="chart-scroll" id="chart-scroll"></div>
20766
+ <div class="detail" id="detail"></div>
20754
20767
  </div>
20755
- <div id="step-body"></div>
20756
- </aside>
20768
+ <aside class="step-panel" id="step-panel">
20769
+ <div class="step-nav">
20770
+ <button id="step-prev" title="previous step (\u2190)">\u25C0</button>
20771
+ <button id="step-next" title="next step (\u2192)">\u25B6</button>
20772
+ <span class="step-count" id="step-count"></span>
20773
+ </div>
20774
+ <div id="step-body"></div>
20775
+ </aside>
20776
+ </div>
20757
20777
  </div>
20758
- </section>
20778
+ </div>
20759
20779
  </section>
20760
20780
  <section class="view" id="view-cluster" style="display:none">
20761
20781
  <div id="cluster-root" class="cluster-root"></div>
@@ -20763,7 +20783,9 @@ var PAGE = `<!doctype html>
20763
20783
  <section class="view" id="view-logs" style="display:none">
20764
20784
  <div class="logs-bar">
20765
20785
  <select id="logs-source"><option value="">all services</option></select>
20766
- <input id="logs-filter" type="text" placeholder="filter \u2014 text, or /regex/i \u2026" autocomplete="off" spellcheck="false" />
20786
+ <input id="logs-filter" type="text" placeholder="filter \u2014 text, trace id, or /regex/i \u2026" autocomplete="off" spellcheck="false" />
20787
+ <span id="logs-trace" class="logs-trace" title="" style="display:none"></span>
20788
+ <label class="logs-tail" title="hide repeated visual-frame capture ticks"><input type="checkbox" id="logs-noise-cb" checked /> hide frames</label>
20767
20789
  <label class="logs-tail"><input type="checkbox" id="logs-tail-cb" checked /> tail</label>
20768
20790
  <button id="logs-clear" type="button">clear</button>
20769
20791
  <span id="logs-count" class="muted"></span>
@@ -20821,6 +20843,8 @@ var PAGE = `<!doctype html>
20821
20843
  var scroll = document.getElementById('chart-scroll');
20822
20844
  var meta = document.getElementById('chart-meta');
20823
20845
  var detailBox = document.getElementById('detail');
20846
+ var emptyBox = document.getElementById('flow-empty');
20847
+ if (emptyBox) emptyBox.style.display = 'none'; // a flow is selected \u2014 drop the placeholder
20824
20848
  wrap.style.display = 'flex'; // the wrap is a flex row: chart + step inspector panel
20825
20849
  var evs = detail.events || [];
20826
20850
  // identical flow \u2192 keep the DOM (and any open detail box) untouched
@@ -21079,24 +21103,36 @@ var PAGE = `<!doctype html>
21079
21103
  .catch(function () { /* next poll retries */ });
21080
21104
  }
21081
21105
 
21106
+ function relTime(ms) {
21107
+ var s = Math.max(0, Math.round((Date.now() - ms) / 1000));
21108
+ if (s < 60) return s + 's ago';
21109
+ var m = Math.round(s / 60); if (m < 60) return m + 'm ago';
21110
+ var h = Math.round(m / 60); if (h < 24) return h + 'h ago';
21111
+ return Math.round(h / 24) + 'd ago';
21112
+ }
21082
21113
  var lastChipsSig = '';
21083
- function renderFlowChips(snap) {
21084
- var holder = document.getElementById('flow-chips');
21114
+ // Vertical, scrollable, newest-first list on the left of the Flows view.
21115
+ // flow://recent is already most-recent-first (index 0 = newest), so index order = "latest up".
21116
+ function renderFlowList(snap) {
21117
+ var holder = document.getElementById('flow-list');
21085
21118
  var flowsRes = snap.resources['flow://recent'];
21086
21119
  var flows = (flowsRes && flowsRes.flows) || [];
21087
21120
  if (flows.length === 0) return;
21088
21121
  // Stable DOM: only rebuild when the flow list (or selection) actually changed,
21089
- // so chips keep identity across polls (clickable mid-poll, stable for a11y).
21122
+ // so rows keep identity across polls (clickable mid-poll, stable for a11y).
21090
21123
  var sig = flows.map(function (f) { return f.flowId + ':' + f.eventCount; }).join('|') + '|' + selectedFlow;
21091
21124
  if (sig === lastChipsSig) return;
21092
21125
  lastChipsSig = sig;
21093
21126
  var frag = document.createDocumentFragment();
21094
- flows.slice(0, 12).forEach(function (f) {
21095
- var label = (f.originNodeId ? f.originNodeId.split('/').pop() : (f.originType || f.flowId)) + ' (' + f.eventCount + ')';
21096
- var chip = el('button', 'chip' + (selectedFlow === f.flowId ? ' active' : ''), label);
21097
- chip.title = f.flowId;
21098
- chip.addEventListener('click', function () { selectFlow(f.flowId); });
21099
- frag.appendChild(chip);
21127
+ flows.forEach(function (f) {
21128
+ var name = f.originNodeId ? f.originNodeId.split('/').pop() : (f.headline || f.originType || f.flowId);
21129
+ var item = el('button', 'flow-item' + (selectedFlow === f.flowId ? ' active' : ''));
21130
+ item.title = f.flowId;
21131
+ item.appendChild(el('div', 'fi-title', name));
21132
+ item.appendChild(el('div', 'fi-meta', f.eventCount + ' event' + (f.eventCount === 1 ? '' : 's') +
21133
+ (f.lastSeenAt ? ' \xB7 ' + relTime(f.lastSeenAt) : '')));
21134
+ item.addEventListener('click', function () { selectFlow(f.flowId); });
21135
+ frag.appendChild(item);
21100
21136
  });
21101
21137
  holder.replaceChildren(frag);
21102
21138
  }
@@ -21143,23 +21179,57 @@ var PAGE = `<!doctype html>
21143
21179
  var logSeqSeen = 0;
21144
21180
  var logSrcSeen = {};
21145
21181
  var logRe = null, logText = '';
21146
- function logHay(r) { return r.source + ' ' + r.severity + ' ' + r.title + ' ' + (r.message || ''); }
21182
+ var traceFilter = ''; // when set, show only rows on this flow/trace id
21183
+ // flowId (the trace) is part of the haystack so text/regex search can target a trace.
21184
+ function logHay(r) { return r.source + ' ' + r.category + ' ' + r.severity + ' ' + r.title + ' ' + (r.message || '') + ' ' + (r.flowId || ''); }
21147
21185
  function logMatches(r) {
21186
+ if (traceFilter && r.flowId !== traceFilter) return false;
21187
+ // "hide frames": mute the high-frequency visual-frame capture ticks (the noise).
21188
+ // The events still flow to the visual lens \u2014 we only drop them from THIS view.
21189
+ var noiseCb = document.getElementById('logs-noise-cb');
21190
+ if (noiseCb && noiseCb.checked && r.title === 'visual-frame') return false;
21148
21191
  var srcSel = document.getElementById('logs-source').value;
21149
21192
  if (srcSel && r.source !== srcSel) return false;
21150
21193
  if (logRe) return logRe.test(logHay(r));
21151
21194
  if (logText) return logHay(r).toLowerCase().indexOf(logText) >= 0;
21152
21195
  return true;
21153
21196
  }
21197
+ function setTraceFilter(id) {
21198
+ traceFilter = id;
21199
+ var p = document.getElementById('logs-trace');
21200
+ p.style.display = 'inline-flex';
21201
+ p.replaceChildren(document.createTextNode('trace '), el('b', null, id.slice(-8)), document.createTextNode(' \u2715'));
21202
+ p.title = 'showing only trace ' + id + ' \u2014 click to clear';
21203
+ renderLogs();
21204
+ }
21205
+ function clearTraceFilter() {
21206
+ traceFilter = '';
21207
+ document.getElementById('logs-trace').style.display = 'none';
21208
+ renderLogs();
21209
+ }
21154
21210
  function p2(n, w) { return String(n).padStart(w || 2, '0'); }
21155
21211
  function fmtLogTime(ms) { var d = new Date(ms); return p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds()) + '.' + p2(d.getMilliseconds(), 3); }
21156
- function logRowEl(r) {
21212
+ // Identity for collapsing a run of repeated lines (service \xB7 category \xB7 severity \xB7 title).
21213
+ // The message/frame-id is intentionally excluded so a burst of visual-frame ticks (or any
21214
+ // repeated log) folds into ONE row with a \xD7N repeat badge.
21215
+ function logKey(r) { return r.source + '|' + r.category + '|' + r.severity + '|' + r.title; }
21216
+ function logRowEl(r, count) {
21157
21217
  var row = el('div', 'log-row sev-' + r.severity);
21158
- if (r.flowId) { row.className += ' jump'; row.title = 'open flow ' + r.flowId; row.addEventListener('click', function () { selectedFlow = r.flowId; navTo('flows'); }); }
21159
21218
  row.appendChild(el('span', 'l-time', fmtLogTime(r.at)));
21160
- row.appendChild(el('span', 'l-src', r.source));
21219
+ row.appendChild(el('span', 'l-src', r.source)); // WHICH service emitted it (raw.project, else source)
21220
+ row.appendChild(el('span', 'l-cat', r.category)); // what KIND of log (visual / network / cluster / \u2026)
21161
21221
  row.appendChild(el('span', 'l-sev', r.severity));
21162
21222
  row.appendChild(el('span', 'l-msg', r.title + (r.message ? ' \u2014 ' + r.message : '')));
21223
+ if (count && count > 1) { var c = el('span', 'l-count', '\xD7' + count); c.title = count + ' repeated lines'; row.appendChild(c); }
21224
+ if (r.flowId) {
21225
+ var tr = el('span', 'l-flow', '\u26D3 ' + r.flowId.slice(-6));
21226
+ tr.title = 'filter logs to trace ' + r.flowId;
21227
+ tr.addEventListener('click', function (e) { e.stopPropagation(); setTraceFilter(r.flowId); });
21228
+ row.appendChild(tr);
21229
+ row.className += ' jump';
21230
+ row.title = 'open flow ' + r.flowId;
21231
+ row.addEventListener('click', function () { selectedFlow = r.flowId; navTo('flows'); });
21232
+ }
21163
21233
  return row;
21164
21234
  }
21165
21235
  function renderLogs() {
@@ -21168,11 +21238,20 @@ var PAGE = `<!doctype html>
21168
21238
  var stick = document.getElementById('logs-tail-cb').checked && (list.scrollHeight - list.scrollTop - list.clientHeight < 60);
21169
21239
  var matched = [];
21170
21240
  for (var i = 0; i < logRows.length; i++) if (logMatches(logRows[i])) matched.push(logRows[i]);
21171
- var shown = matched.slice(-1200); // cap the DOM; newest are kept
21241
+ // collapse consecutive identical lines into one group, carrying the latest row + a repeat count
21242
+ var groups = [];
21243
+ for (var k = 0; k < matched.length; k++) {
21244
+ var r = matched[k];
21245
+ var last = groups.length ? groups[groups.length - 1] : null;
21246
+ if (last && logKey(last.row) === logKey(r)) { last.count++; last.row = r; }
21247
+ else groups.push({ row: r, count: 1 });
21248
+ }
21249
+ var shown = groups.slice(-1200); // cap the DOM; newest are kept
21172
21250
  var frag = document.createDocumentFragment();
21173
- for (var j = 0; j < shown.length; j++) frag.appendChild(logRowEl(shown[j]));
21251
+ for (var j = 0; j < shown.length; j++) frag.appendChild(logRowEl(shown[j].row, shown[j].count));
21174
21252
  list.replaceChildren(frag);
21175
- document.getElementById('logs-count').textContent = matched.length + ' shown / ' + logRows.length + ' lines';
21253
+ document.getElementById('logs-count').textContent =
21254
+ groups.length + ' row' + (groups.length === 1 ? '' : 's') + ' \xB7 ' + matched.length + ' lines / ' + logRows.length + ' total';
21176
21255
  if (stick) list.scrollTop = list.scrollHeight;
21177
21256
  }
21178
21257
  function refreshLogSources(sources) {
@@ -21209,6 +21288,8 @@ var PAGE = `<!doctype html>
21209
21288
  renderLogs();
21210
21289
  });
21211
21290
  document.getElementById('logs-source').addEventListener('change', renderLogs);
21291
+ document.getElementById('logs-noise-cb').addEventListener('change', renderLogs);
21292
+ document.getElementById('logs-trace').addEventListener('click', clearTraceFilter);
21212
21293
  document.getElementById('logs-clear').addEventListener('click', function () { logRows = []; renderLogs(); });
21213
21294
  })();
21214
21295
 
@@ -21255,7 +21336,7 @@ var PAGE = `<!doctype html>
21255
21336
  }
21256
21337
  fetch(U('/api/snapshot')).then(function (r) { return r.json(); })
21257
21338
  .then(function (snap) {
21258
- renderFlowChips(snap);
21339
+ renderFlowList(snap);
21259
21340
  if (v === 'resources') renderPanels(snap);
21260
21341
  setStatus(true, snap);
21261
21342
  if (v === 'flows' && selectedFlow) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -2,7 +2,7 @@
2
2
  "name": "lensmcp",
3
3
  "displayName": "LensMCP",
4
4
  "description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
5
- "version": "1.10.0",
5
+ "version": "1.12.0",
6
6
  "author": {
7
7
  "name": "David Antoon",
8
8
  "email": "davidmantoon@gmail.com"