lean-ctx 3.9.11

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
/**
 * ROI & Plan monitoring view.
 *
 * Renders the local, signed savings ROI (tokens / $ / energy saved + verification
 * provenance), the effective commercial plan with offline-grace status and its
 * entitlements, and the daily savings trend. Read-only; data comes from /api/roi.
 */

function croiApi() {
  return window.LctxApi && window.LctxApi.apiFetch ? window.LctxApi.apiFetch : null;
}

function croiFmt() {
  return window.LctxFmt || {};
}

function croiCharts() {
  return window.LctxCharts || {};
}

/** Humanise a unix-seconds verification time into "Nd ago". */
function ageDays(verifiedAt) {
  if (!verifiedAt) return null;
  var secs = Math.max(0, Math.floor(Date.now() / 1000) - Number(verifiedAt));
  return Math.floor(secs / 86400);
}

/** How often the view re-fetches /api/roi while it is the active view. */
var ROI_REFRESH_MS = 10000;

class CockpitRoi extends HTMLElement {
  constructor() {
    super();
    this._loading = true;
    this._error = null;
    this._data = null;
    this._fetching = false;
    this._updatedAt = null;
    this._timer = null;
    this._onRefresh = this._onRefresh.bind(this);
  }

  connectedCallback() {
    if (this._ready) return;
    this._ready = true;
    this.style.display = 'block';
    document.addEventListener('lctx:refresh', this._onRefresh);
    // Keep the view live: re-fetch on the cockpit cadence while active. The
    // _onRefresh guard means hidden views never fetch, and loadData() swaps
    // content in place (no "Loading…" flash) once data exists.
    this._timer = setInterval(this._onRefresh, ROI_REFRESH_MS);
    this.render();
    // Lazy-load (#452): the router loads this view's data on activation; the
    // interval above only refetches while the view is active (see _onRefresh).
  }

  disconnectedCallback() {
    document.removeEventListener('lctx:refresh', this._onRefresh);
    if (this._timer) {
      clearInterval(this._timer);
      this._timer = null;
    }
    this._destroyChart();
  }

  _onRefresh() {
    var v = document.getElementById('view-roi');
    if (v && v.classList.contains('active')) this.loadData();
  }

  _destroyChart() {
    var Ch = croiCharts();
    if (Ch.destroyIfNeeded) Ch.destroyIfNeeded('roi-trend');
  }

  async loadData() {
    var fetchJson = croiApi();
    if (!fetchJson) {
      this._error = 'API client not loaded';
      this._loading = false;
      this.render();
      return;
    }
    if (this._fetching) return;
    this._fetching = true;
    this._loading = true;
    this._error = null;
    // Flicker-free refresh: only show the loading placeholder before the first
    // payload. Background refreshes keep the current DOM and swap in place.
    if (!this._data) this.render();

    try {
      // Individual + local only. The team roll-up is a separate surface
      // (web /account/team, or `lean-ctx savings team`) — not this cockpit.
      // Stats feed the estimated cost-analysis card (moved here from Home,
      // GL #486) and are clearly labelled as estimates next to the ledger.
      var cached = window.LctxApi && window.LctxApi.cachedFetch
        ? window.LctxApi.cachedFetch : fetchJson;
      var results = await Promise.all([
        fetchJson('/api/roi', { timeoutMs: 12000 }),
        cached('/api/stats', { timeoutMs: 12000 }).catch(function () { return null; }),
        fetchJson('/api/spend', { timeoutMs: 8000 }).catch(function () { return null; }),
        // Org usage breakdown (enterprise#20): central admin API when
        // [gateway_server].admin_url is set, else this machine's snapshot.
        fetchJson('/api/usage-breakdown', { timeoutMs: 12000 }).catch(function () { return null; }),
      ]);
      this._data = results[0];
      this._stats = results[1];
      // Measured spend (real provider bill) + server-side pricing so the
      // estimated cost model de-hardcodes its blended rate (GL #486 follow-up).
      this._spend = results[2];
      this._usage = results[3];
      var Fp = croiFmt();
      if (this._spend && this._spend.pricing && Fp.applyServerPricing) {
        Fp.applyServerPricing(this._spend.pricing);
      }
      this._updatedAt = new Date();
      // Output-echo summary (#501) and edit-efficiency counters (#1008) ride
      // on /api/stats; non-fatal if missing.
      try {
        var stats = await fetchJson('/api/stats', { timeoutMs: 8000 });
        this._outputEcho = stats && stats.output_echo ? stats.output_echo : null;
        this._editEfficiency = stats && stats.edit_efficiency ? stats.edit_efficiency : null;
      } catch (e2) {
        this._outputEcho = null;
        this._editEfficiency = null;
      }
      // Echo learning trend (#507) from /api/signals; non-fatal if missing.
      try {
        var signals = await fetchJson('/api/signals', { timeoutMs: 8000 });
        this._echoTrend = signals && Array.isArray(signals.echo_trend) ? signals.echo_trend : null;
      } catch (e3) {
        this._echoTrend = null;
      }
    } catch (e) {
      this._error = e && e.error ? e.error : String(e || 'error');
      this._data = null;
    }
    this._loading = false;
    this._fetching = false;
    this.render();
    this._renderTrend();
  }

  /* ---- render ---- */

  render() {
    var F = croiFmt();
    var esc = F.esc || function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; }); };

    if (this._loading) {
      this.innerHTML = '<div class="card"><div class="loading-state">Loading ROI\u2026</div></div>';
      return;
    }
    if (this._error || !this._data) {
      this.innerHTML =
        '<div class="card"><h3>Error</h3>' +
        '<p class="hs" style="color:var(--red)">' + esc(String(this._error || 'no data')) + '</p></div>';
      return;
    }

    var roi = this._data.roi || {};
    if (!roi.total_events) {
      this.innerHTML =
        '<div class="card"><div class="empty-state">' +
        '<h2>No verified savings yet</h2>' +
        '<p>Use lean-ctx (ctx_read / ctx_search / \u2026) for a while. Your signed savings ' +
        'ledger fills up automatically, then this view shows your ROI.</p></div></div>';
      // Still render org usage + plan so gateway admins see the org picture
      // (and users their plan) even before this machine has local events.
      this.innerHTML += this._renderOrgUsage(esc);
      this.innerHTML += this._renderPlan(esc);
      return;
    }

    var body = this._renderHero(esc);
    body += this._renderLiveStamp(esc);
    body += this._renderOutputEfficiency(esc);
    body += this._renderEditEfficiency(esc);
    body += this._renderOutputSavings(esc);
    body += this._renderVerification(esc);
    body += this._renderMethodology();
    body += this._renderMeasuredSpend(esc);
    body += this._renderOrgUsage(esc);
    body += this._renderCostAnalysis(esc);
    body += this._renderPlan(esc);
    body += this._renderTrendCard(esc);
    body += this._renderBreakdown(esc);
    body += this._renderShare(esc);
    this.innerHTML = body;
  }

  /**
   * Measured spend — the real provider bill. Shown only when the proxy has
   * recorded usage (proxy-routed clients). This is the *measured* counterpart to
   * the estimated cost-analysis card below: real model + billed tokens read from
   * upstream responses, priced with the shared table.
   */
  _renderMeasuredSpend(esc) {
    var spend = this._spend;
    if (!spend || !spend.available) return '';
    var rows = Array.isArray(spend.per_model) ? spend.per_model : [];
    if (!rows.length) return '';
    var F = croiFmt();
    var ff = F.ff || function (n) { return String(n); };
    var fu = F.fu || function (a) { return '$' + Number(a).toFixed(2); };

    var bodyRows = rows.slice(0, 10).map(function (m) {
      var estTag = m.pricing_estimated
        ? ' <span class="tag ty" title="Pricing matched heuristically; no exact or live entry in the price table">est. price</span>'
        : '';
      if (m.measured_requests > 0) {
        var allMeasured = m.measured_requests >= m.requests;
        estTag = ' <span class="tag tg" title="' +
          (allMeasured
            ? 'Cost reported by the provider itself (usage accounting) \u2014 this is the bill, not an estimate'
            : 'Partially provider-reported: ' + esc(ff(m.measured_requests)) + ' of ' + esc(ff(m.requests)) + ' requests carry the provider\u2019s own charge') +
          '">' + (allMeasured ? 'provider-billed' : 'partly billed') + '</span>' +
          (m.pricing_estimated ? estTag : '');
      }
      return '<tr><td>' + esc(String(m.model)) + estTag + '</td>' +
        '<td class="r">' + esc(ff(m.requests)) + '</td>' +
        '<td class="r">' + esc(ff(m.input_tokens)) + '</td>' +
        '<td class="r">' + esc(ff(m.output_tokens)) + '</td>' +
        '<td class="r">' + esc(ff(m.cache_read_tokens)) + '</td>' +
        '<td class="r" style="color:var(--green)">' + esc(fu(m.cost_usd)) + '</td></tr>';
    }).join('');

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Measured spend</h3>' +
      '<span class="tag tg">measured</span></div>' +
      '<p class="hs" style="margin:-4px 0 10px;color:var(--muted)">' +
      'Your real provider bill \u2014 actual model &amp; billed tokens (incl. cache reads/writes ' +
      '&amp; reasoning) read from upstream responses for proxy-routed clients ' +
      '(Claude Code, Codex, Pi, Gemini CLI, OpenCode). MCP-only IDEs (Cursor, Copilot, \u2026) ' +
      'bypass the proxy and show under <b>estimated</b> below.</p>' +
      '<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:10px">' +
      '<div class="hv" style="color:var(--green)">' + esc(fu(spend.total_usd)) + '</div>' +
      '<span class="hs">total measured spend</span></div>' +
      '<div class="table-scroll"><table><thead><tr><th>Model</th>' +
      '<th class="r">Reqs</th><th class="r">Input</th><th class="r">Output</th>' +
      '<th class="r">Cache rd</th><th class="r">Cost</th></tr></thead>' +
      '<tbody>' + bodyRows + '</tbody></table></div></div>'
    );
  }

  /**
   * Org usage breakdown (enterprise#20) — the "monitoring \u00d7 saving" table.
   * Central source: gateway admin API (person \u00d7 project \u00d7 model \u00d7 provider
   * from Postgres usage_events, incl. counterfactual savings + seat projection).
   * Local source: model-level snapshot of this machine, same shape, no identity
   * dimension. Hidden entirely when there is no usage at all.
   */
  _renderOrgUsage(esc) {
    var u = this._usage;
    if (!u || !Array.isArray(u.rows)) return '';
    var totals = u.totals || {};
    var central = u.source === 'central';
    if (!u.rows.length && !(totals.requests > 0)) return '';
    var F = croiFmt();
    var ff = F.ff || function (n) { return String(n); };
    var fu = F.fu || function (a) { return '$' + Number(a).toFixed(2); };

    var idCols = central ? '<th>Person</th><th>Project</th>' : '';
    var bodyRows = u.rows.slice(0, 15).map(function (r) {
      var id = central
        ? '<td>' + esc(String(r.person || '\u2014')) + '</td><td>' + esc(String(r.project || '\u2014')) + '</td>'
        : '';
      var provTag = r.provider
        ? ' <span class="tag ty">' + esc(String(r.provider)) + '</span>'
        : '';
      return '<tr>' + id +
        '<td>' + esc(String(r.model)) + provTag + '</td>' +
        '<td class="r">' + esc(ff(r.requests || 0)) + '</td>' +
        '<td class="r">' + esc(ff((r.input_tokens || 0) + (r.output_tokens || 0))) + '</td>' +
        '<td class="r">' + esc(fu(r.cost_usd || 0)) + '</td>' +
        '<td class="r" style="color:var(--green)">' + esc(fu(r.saved_usd || 0)) + '</td></tr>';
    }).join('');

    var sourceTag = central
      ? '<span class="tag tg">central</span>'
      : '<span class="tag ty" title="No [gateway_server].admin_url configured \u2014 showing this machine only">local snapshot</span>';
    var orgLabel = u.org_label ? esc(String(u.org_label)) + ' \u00b7 ' : '';

    var heroBits =
      '<div style="display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;margin-bottom:10px">' +
      '<div><div class="hv">' + esc(ff(totals.requests || 0)) + '</div><span class="hs">requests (30d)</span></div>' +
      '<div><div class="hv">' + esc(fu(totals.cost_usd || 0)) + '</div><span class="hs">measured cost</span></div>' +
      '<div><div class="hv" style="color:var(--green)">' + esc(fu(totals.saved_usd || 0)) + '</div><span class="hs">saved vs. baseline</span></div>' +
      (central && totals.reference_cost_usd > 0
        ? '<div><div class="hv">' + esc(fu(totals.reference_cost_usd)) + '</div><span class="hs">reference cost (counterfactual)</span></div>'
        : '') +
      '</div>';

    var projection = '';
    if (totals.projection_usd_per_month > 0 && totals.projection_seats > 0) {
      projection =
        '<div class="view-hint" style="margin-top:10px">' +
        '<span class="tag tg">projection</span>' +
        '<span>At <b>' + esc(ff(totals.projection_seats)) + ' seats</b> this saving rate is ' +
        '<b style="color:var(--green)">' + esc(fu(totals.projection_usd_per_month)) + ' / month</b>' +
        (totals.active_persons > 0
          ? ' (extrapolated from ' + esc(ff(totals.active_persons)) + ' active ' +
            (totals.active_persons === 1 ? 'person' : 'people') + ', 30-day window).'
          : '.') +
        '</span></div>';
    }

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Usage breakdown</h3>' + sourceTag + '</div>' +
      '<p class="hs" style="margin:-4px 0 10px;color:var(--muted)">' + orgLabel +
      (central
        ? 'Org-wide usage from the gateway\u2019s usage_events store \u2014 who uses which model on which project, what it costs, and what lean-ctx saved (routing, compression, caching vs. the configured baseline).'
        : 'This machine\u2019s measured usage. Point <code>[gateway_server].admin_url</code> at your org gateway to see the org-wide person \u00d7 project breakdown here.') +
      '</p>' +
      heroBits +
      '<div class="table-scroll"><table><thead><tr>' + idCols +
      '<th>Model</th><th class="r">Reqs</th><th class="r">Tokens</th>' +
      '<th class="r">Cost</th><th class="r">Saved</th></tr></thead>' +
      '<tbody>' + bodyRows + '</tbody></table></div>' +
      projection + '</div>'
    );
  }

  /**
   * Estimated cost comparison (moved from Home, GL #486). Sits right after
   * the methodology card on purpose: these are the modelled all-time numbers
   * the methodology explains, not the verified ledger above.
   */
  _renderCostAnalysis(esc) {
    var stats = this._stats;
    if (!stats) return '';
    var F = croiFmt();
    var fu = F.fu || function (a) { return '$' + Number(a).toFixed(2); };
    var gc = F.gc;
    if (!gc) return '';

    var totalIn = stats.total_input_tokens || 0;
    var totalOut = stats.total_output_tokens || 0;
    var calls = stats.total_commands || 0;
    if (totalIn <= 0) return '';
    var c = gc(totalIn, totalOut, calls);

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Cost analysis (estimated, all-time)</h3></div>' +
      '<div class="cost-row">' +
      '<div class="cost-box bad">' +
      '<div class="amt" style="color:var(--red)">' +
      esc(fu(c.tW)) + '</div>' +
      '<div class="lb">Without lean-ctx</div></div>' +
      '<div class="cost-arrow">\u2192</div>' +
      '<div class="cost-box good">' +
      '<div class="amt" style="color:var(--green)">' +
      esc(fu(c.tC)) + '</div>' +
      '<div class="lb">With lean-ctx</div></div>' +
      '</div>' +
      '<div class="cost-detail">' +
      '<div class="cd-item"><div class="v" style="color:var(--green)">' +
      esc(fu(c.sv)) + '</div><div class="l">Total saved</div></div>' +
      '<div class="cd-item"><div class="v">' +
      esc(fu(c.iW - c.iC)) + '</div><div class="l">Input saved</div></div>' +
      '<div class="cd-item"><div class="v">' +
      esc(fu(c.oW - c.oC)) + '</div><div class="l">Output saved</div></div>' +
      '<div class="cd-item"><div class="v">' +
      esc(fu(c.tC)) + '</div><div class="l">Actual cost</div></div>' +
      '</div>' +
      '</div>'
    );
  }

  /**
   * "Why is this number smaller than Home?" — the two surfaces count
   * differently on purpose (GL #479). Static copy, no user data involved.
   */
  _renderMethodology() {
    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Methodology: verified vs. estimated</h3></div>' +
      '<div class="sr"><span class="sl">This page (verified)</span><span class="sv">' +
      'Only <b>measured</b> compression: raw bytes that actually entered a tool vs. what was sent. ' +
      'No counterfactual multipliers. Append-only, hash-chained, signable.</span></div>' +
      '<div class="sr"><span class="sl">Home (estimated)</span><span class="sv">' +
      'All-time stats including modelled baselines: search assumes a native grep costs ' +
      '<b>2.5\u00d7</b> the raw matched output (refinement runs, wider context), and a cache hit ' +
      'counts the full original as saved (the re-read counterfactual).</span></div>' +
      '<div class="sr"><span class="sl">Why smaller here</span><span class="sv">' +
      'The ledger starts later than the all-time stats, skips zero-saving calls and never ' +
      'applies estimate factors \u2014 so the verified total is the conservative floor, ' +
      'not a contradiction.</span></div>' +
      '</div>'
    );
  }

  /** Muted liveness line so the view is visibly auto-updating, not frozen. */
  _renderLiveStamp(esc) {
    if (!this._updatedAt) return '';
    var t = this._updatedAt.toLocaleTimeString();
    return (
      '<p class="hs" style="margin:-8px 0 16px;color:var(--muted);text-align:right">' +
      'Updated ' + esc(t) + ' \u00b7 auto-refreshes every ' +
      esc(String(Math.round(ROI_REFRESH_MS / 1000))) + '\u202fs</p>'
    );
  }

  _renderHero(esc) {
    var F = croiFmt();
    var ff = F.ff || function (n) { return String(n); };
    var fu = F.fu || function (n) { return '$' + n; };
    var roi = this._data.roi;
    var energyWh = F.ewh ? F.ewh(roi.net_saved_tokens) : 0;
    var energy = F.fe ? F.fe(energyWh) : '\u2014';

    // The signed ledger starts later than the all-time stats on Home, so the
    // totals legitimately differ. Say so prominently, or the numbers look
    // contradictory next to Home's estimated all-time figures.
    var trend = this._data.trend || [];
    var since = trend.length && trend[0] && trend[0][0] ? String(trend[0][0]) : null;

    var scopeBanner =
      '<div class="view-hint" style="margin-bottom:14px">' +
      '<span class="tag tg">verified</span>' +
      '<span>These numbers come from the <b>signed ledger</b>' +
      (since ? ' (recording since <b>' + esc(since) + '</b>)' : '') +
      ' \u2014 it only counts <b>measured</b> compression: actual tokens observed ' +
      'before vs. after, per event, hash-chained. The totals on ' +
      '<a href="#overview" style="color:var(--accent)">Home</a> are an <b>estimate</b> ' +
      'of what agents would have loaded without lean-ctx \u2014 they include the full ' +
      'history and a modeled baseline for search results. Estimated is the bigger ' +
      'picture; this page is the auditable floor.</span>' +
      '</div>';

    return (
      scopeBanner +
      '<div class="hero r4 stagger" style="margin-bottom:4px">' +
      '<div class="hc"><span class="hl">Net tokens saved ' +
      '<span class="tag tg">verified</span></span>' +
      '<div class="hv">' + esc(ff(roi.net_saved_tokens)) + '</div></div>' +
      '<div class="hc"><span class="hl">$ saved ' +
      '<span class="tag tg">verified</span></span>' +
      '<div class="hv" style="color:var(--green)">' + esc(fu(roi.saved_usd)) + '</div></div>' +
      '<div class="hc"><span class="hl">Energy saved</span>' +
      '<div class="hv">' + esc(energy) + '</div></div>' +
      '<div class="hc"><span class="hl">Verified events</span>' +
      '<div class="hv">' + esc(ff(roi.total_events)) + '</div></div>' +
      '</div>'
    );
  }

  /**
   * Output efficiency (#501): share of recent agent replies that re-quoted
   * content lean-ctx had already delivered. Output tokens cost 4-5x input
   * tokens, so echo directly burns the input-side savings.
   */
  _renderOutputEfficiency(esc) {
    var echo = this._outputEcho;
    if (!echo || !echo.window) return '';
    var pct = Math.round((echo.avg_ratio || 0) * 100);
    var good = pct <= 15;
    var mid = pct > 15 && pct <= 30;
    var color = good ? 'var(--green)' : mid ? 'var(--yellow)' : 'var(--red)';
    var verdict = good
      ? 'Healthy — replies mostly reference instead of re-quoting.'
      : mid
        ? 'Moderate — some replies re-quote delivered file content.'
        : 'High — a large share of reply code was already in context; agents should reference lines (F1:42-58) instead.';
    // Learning trend (#507): daily avg echo ratio. Falling = agents quote
    // less of what was already delivered.
    var trendHtml = '';
    var trend = this._echoTrend || [];
    if (trend.length >= 2) {
      var S = window.LctxShared || {};
      var spark = S.sparklineSvg ? S.sparklineSvg(trend.map(function (d) { return d[1]; }), 140, 28) : '';
      if (spark) {
        trendHtml =
          '<div style="display:flex;align-items:center;gap:10px;margin-top:8px" ' +
          'title="Daily average echo ratio over the last ' + esc(String(trend.length)) + ' days. Falling = learning.">' +
          spark +
          '<span class="hs" style="color:var(--muted)">' + esc(String(trend.length)) + '-day trend \u2014 lower is better</span>' +
          '</div>';
      }
    } else {
      trendHtml =
        '<p class="hs" style="margin-top:6px;color:var(--muted)">Trend: collecting data \u2014 ' +
        'shows after 2+ days of agent activity.</p>';
    }

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Output Efficiency</h3>' +
      '<span class="badge" title="Share of code lines in recent agent replies that were verbatim echoes of files lean-ctx already delivered into context. Measured locally from the last ' +
      esc(String(echo.window)) + ' replies.">' +
      esc(String(echo.window)) + ' replies analyzed</span></div>' +
      '<div style="display:flex;align-items:baseline;gap:12px">' +
      '<div class="hv" style="color:' + color + '">' + esc(String(pct)) + '%</div>' +
      '<span class="hs">of reply code lines echoed already-delivered content</span>' +
      '</div>' +
      '<p class="hs" style="margin-top:6px;color:var(--muted)">' + esc(verdict) + '</p>' +
      trendHtml +
      '</div>'
    );
  }

  /**
   * Edit Efficiency (#1008): anchored editing (ctx_patch) vs str_replace
   * (ctx_edit). "Avoided" is measured per applied op — tokens of the replaced
   * span the model referenced by (line, hash) anchor instead of re-emitting it
   * as old_string. A separate channel from read-gain; hidden until an edit
   * path has been used at all.
   */
  _renderEditEfficiency(esc) {
    var em = this._editEfficiency;
    if (!em) return '';
    var F = croiFmt();
    var ff = F.ff || function (n) { return String(n); };
    var n = function (x) { return Number(x || 0); };
    var anchored = n(em.anchored_calls);
    var sr = n(em.str_replace_calls);
    var srMisses = n(em.str_replace_misses);
    if (!anchored && !sr && !srMisses) return '';

    var avoided = n(em.anchored_avoided_output_tokens);
    var conflicts = n(em.anchored_conflicts);
    var conflictPct = anchored + conflicts > 0
      ? Math.round((conflicts / (anchored + conflicts)) * 100) : 0;

    var anchoredRows = anchored
      ? '<div class="sr"><span class="sl">Anchored (ctx_patch)</span><span class="sv">' +
        esc(ff(anchored)) + ' edits \u00b7 ' + esc(ff(n(em.anchored_ops))) + ' ops \u00b7 ' +
        '<b style="color:var(--green)">' + esc(ff(avoided)) + ' output tok avoided</b></span></div>' +
        '<div class="sr"><span class="sl">Anchor conflicts</span><span class="sv">' +
        esc(ff(conflicts)) + ' stale-anchor retries (' + esc(String(conflictPct)) + '% of attempts)</span></div>'
      : '';
    var srRows = (sr || srMisses)
      ? '<div class="sr"><span class="sl">str_replace (ctx_edit)</span><span class="sv">' +
        esc(ff(sr)) + ' edits \u00b7 ' + esc(ff(n(em.str_replace_old_string_tokens))) +
        ' output tok paid on old_string \u00b7 ' + esc(ff(srMisses)) + ' old_string misses</span></div>'
      : '';

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Edit Efficiency</h3>' +
      '<span class="tag tg" title="Measured per applied op: tokens of the replaced span minus the anchor arguments the model sent instead. Never estimated.">measured</span></div>' +
      '<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:6px">' +
      '<div class="hv" style="color:var(--green)">' + esc(ff(avoided)) + '</div>' +
      '<span class="hs">output tokens never re-emitted \u2014 anchored edits reference ' +
      '<code>line:hash</code> instead of quoting the replaced span</span></div>' +
      anchoredRows + srRows +
      '<p class="hs" style="margin-top:8px;color:var(--muted)">Output tokens cost ~5\u00d7 input; ' +
      'anchored editing (<code>ctx_read mode=anchored</code> \u2192 <code>ctx_patch</code>) saves them at the source.</p>' +
      '</div>'
    );
  }

  /**
   * Output Tokens Saved (#895). lean-ctx shapes output via cache-safe effort
   * control + verbosity steering; this card reports how much that saved. It is
   * honestly labelled: a real A/B **measured** reduction with a 95% CI when an
   * output_holdout is running, otherwise a model-based **estimate** band.
   */
  _renderOutputSavings(esc) {
    var o = this._data && this._data.output;
    if (!o || !o.status) return '';
    var n = function (x) { return Number(x || 0); };
    var fix1 = function (x) { return n(x).toFixed(1); };
    var round = function (x) { return Math.round(n(x)); };

    if (o.status === 'measured') {
      return (
        '<div class="card" style="margin-bottom:16px">' +
        '<div class="card-header"><h3>Output Tokens Saved</h3>' +
        '<span class="tag tg">measured</span></div>' +
        '<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:6px">' +
        '<div class="hv" style="color:var(--green)">' + esc(fix1(o.reduction_pct)) + '%</div>' +
        '<span class="hs">fewer output tokens \u00b7 95% CI ' +
        esc(fix1(o.ci95_low_pct)) + '\u2013' + esc(fix1(o.ci95_high_pct)) + '%</span></div>' +
        '<div class="sr"><span class="sl">Avg output / turn</span><span class="sv">' +
        esc(String(round(o.control_avg_output))) + ' \u2192 ' +
        esc(String(round(o.treatment_avg_output))) + ' tok ' +
        '(\u2212' + esc(String(round(o.tokens_saved_per_turn))) + ')</span></div>' +
        '<div class="sr"><span class="sl">Sample</span><span class="sv">' +
        esc(String(n(o.control_n))) + ' control \u00b7 ' +
        esc(String(n(o.treatment_n))) + ' shaped turns</span></div>' +
        '<p class="hs" style="margin-top:8px;color:var(--muted)">' +
        'Real A/B result from your <code>output_holdout</code> control arm.</p>' +
        '</div>'
      );
    }
    if (o.status === 'pending') {
      var need = n(o.needed_per_arm);
      return (
        '<div class="card" style="margin-bottom:16px">' +
        '<div class="card-header"><h3>Output Tokens Saved</h3>' +
        '<span class="tag tb">holdout running</span></div>' +
        '<p class="hs">Collecting paired turns: <b>' + esc(String(n(o.control_n))) + '/' + esc(String(need)) +
        '</b> control, <b>' + esc(String(n(o.treatment_n))) + '/' + esc(String(need)) + '</b> shaped. ' +
        'A measured reduction with a 95% CI appears once both arms reach ' + esc(String(need)) + ' turns.</p>' +
        '</div>'
      );
    }
    // estimated
    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Output Tokens Saved</h3>' +
      '<span class="tag ty">estimated</span></div>' +
      '<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:6px">' +
      '<div class="hv">~' + esc(String(round(o.point_pct))) + '%</div>' +
      '<span class="hs">model-based estimate \u00b7 band ' +
      esc(String(round(o.low_pct))) + '\u2013' + esc(String(round(o.high_pct))) + '%</span></div>' +
      '<p class="hs" style="margin-top:6px;color:var(--muted)">' +
      'This is an estimate, not a measurement. Enable a holdout control arm to ' +
      'measure your real output savings:</p>' +
      '<pre class="mono" style="background:var(--bg-elev,#0d1117);padding:10px;border-radius:8px;overflow:auto">' +
      'lean-ctx config set proxy.output_holdout 0.1</pre>' +
      '</div>'
    );
  }

  _renderVerification(esc) {
    var roi = this._data.roi;
    var usage = this._data.usage || {};
    var chainTag = roi.chain_valid
      ? '<span class="tag tg">chain valid</span>'
      : '<span class="tag td">chain BROKEN</span>';
    var signTag = roi.signed
      ? '<span class="tag tg">signed (Ed25519)</span>'
      : '<span class="tag ty">unsigned</span>';
    var billTag = usage.billable
      ? '<span class="tag tg">billable</span>'
      : '<span class="tag tb">not billable</span>';
    var signer = roi.signed && roi.signer_public_key
      ? '<div class="sr"><span class="sl">Signer</span><span class="sv mono">' +
        esc(String(roi.signer_public_key).slice(0, 24)) + '\u2026</span></div>'
      : '';
    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Verification</h3>' + chainTag + '</div>' +
      '<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">' +
      signTag + billTag + '</div>' +
      '<div class="sr"><span class="sl">Chain head</span><span class="sv mono">' +
      esc(String(roi.last_entry_hash || '\u2014').slice(0, 24)) + '\u2026</span></div>' +
      signer +
      '<p class="hs" style="margin-top:8px;color:var(--muted)">' +
      'Numbers derive from a local, hash-chained, Ed25519-signed savings ledger \u2014 ' +
      'tamper-evident and shareable.</p>' +
      '</div>'
    );
  }

  _renderPlan(esc) {
    var plan = (this._data && this._data.plan) || { plan: 'free', source: 'none', entitlements: {} };
    var e = plan.entitlements || {};
    var label = String(plan.plan || 'free');

    var sourceTag;
    if (plan.source === 'live') {
      sourceTag = '<span class="tag tg">live</span>';
    } else if (plan.source === 'cached') {
      var age = ageDays(plan.verified_at);
      var remaining = age == null ? null : Math.max(0, (plan.grace_days || 14) - age);
      sourceTag = '<span class="tag tb">cached' +
        (age == null ? '' : ' \u00b7 verified ' + age + 'd ago, valid ' + remaining + 'd more') + '</span>';
    } else if (plan.source === 'expired') {
      sourceTag = '<span class="tag ty">cached plan expired</span>';
    } else {
      sourceTag = '<span class="tag tb">no account</span>';
    }

    function ent(name, ok) {
      return '<div class="sr"><span class="sl">' + esc(name) + '</span>' +
        '<span class="sv">' + (ok ? '<span class="tag tg">yes</span>' : '<span class="tag tb">no</span>') +
        '</span></div>';
    }
    var seats = e.seats === 4294967295 ? 'unlimited' : (e.seats != null ? String(e.seats) : '\u2014');

    var cta;
    if (plan.source === 'expired') {
      cta = 'Reconnect to restore your plan: <code>lean-ctx login</code> then <code>lean-ctx sync</code>.';
    } else if (label === 'free') {
      cta = 'Upgrade for hosted sync &amp; team ROI roll-up: <code>lean-ctx cloud upgrade</code>.';
    } else if (label === 'pro') {
      cta = 'On a team? Aggregate everyone\u2019s ROI: <code>lean-ctx cloud upgrade --plan team</code>.';
    } else if (label === 'team') {
      cta = 'Need org SSO + 1-year audit? <code>lean-ctx cloud upgrade --plan business</code>.';
    } else {
      cta = 'Manage billing &amp; invoices from the customer portal.';
    }

    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Plan: ' + esc(label) + '</h3>' + sourceTag + '</div>' +
      ent('Personal Cloud sync', !!e.cloud_sync) +
      '<div class="sr"><span class="sl">Seats</span><span class="sv">' + esc(seats) + '</span></div>' +
      ent('Private registry', !!e.private_registry) +
      ent('Org SSO (OIDC)', !!e.sso_oidc) +
      ent('SAML SSO / SCIM', !!e.sso_scim) +
      ent('Supporter', !!e.supporter) +
      '<p class="hs" style="margin-top:8px;color:var(--muted)">' + cta + '</p>' +
      '<p class="hs" style="color:var(--muted)">The local engine is always free and never gated.</p>' +
      '</div>'
    );
  }

  _renderTrendCard(esc) {
    var trend = (this._data && this._data.trend) || [];
    if (!trend.length) return '';
    return (
      '<div class="card" style="margin-bottom:16px">' +
      '<div class="card-header"><h3>Daily savings</h3>' +
      '<span class="badge">' + esc(String(trend.length)) + ' days</span></div>' +
      '<canvas id="roi-trend" height="90"></canvas></div>'
    );
  }

  _renderTrend() {
    var trend = (this._data && this._data.trend) || [];
    if (!trend.length) return;
    var Ch = croiCharts();
    if (!Ch.lineChart || typeof Chart === 'undefined') return;
    if (!document.getElementById('roi-trend')) return;
    var labels = trend.map(function (r) { return String(r[0]).slice(5); });
    var values = trend.map(function (r) { return Number(r[1]) || 0; });
    try {
      Ch.lineChart('roi-trend', labels, values, '#34d399', 'rgba(52,211,153,.08)');
    } catch (_) {}
  }

  _renderBreakdown(esc) {
    var F = croiFmt();
    var ff = F.ff || function (n) { return String(n); };
    var fu = F.fu || function (n) { return '$' + n; };
    var roi = this._data.roi;
    var models = Array.isArray(roi.top_models) ? roi.top_models : [];
    var tools = Array.isArray(roi.top_tools) ? roi.top_tools : [];

    var modelRows = models.slice(0, 8).map(function (m) {
      var name = String(m[0]);
      var label = name === 'fallback-blended'
        ? '<span title="Events without model attribution, priced at a blended average input rate">model unknown <span class="hs">(blended rate)</span></span>'
        : esc(name);
      return '<tr><td>' + label + '</td>' +
        '<td class="r">' + esc(ff(m[1])) + '</td>' +
        '<td class="r">' + esc(fu(m[2])) + '</td></tr>';
    }).join('');
    var toolRows = tools.slice(0, 8).map(function (t) {
      return '<tr><td>' + esc(String(t[0])) + '</td>' +
        '<td class="r">' + esc(ff(t[1])) + '</td></tr>';
    }).join('');

    var modelsCard = models.length
      ? '<div class="card"><div class="card-header"><h3>Top models</h3></div>' +
        '<div class="table-scroll"><table><thead><tr><th>Model</th>' +
        '<th class="r">Tokens saved</th><th class="r">$ saved</th></tr></thead>' +
        '<tbody>' + modelRows + '</tbody></table></div></div>'
      : '';
    var toolsCard = tools.length
      ? '<div class="card"><div class="card-header"><h3>Top tools</h3></div>' +
        '<div class="table-scroll"><table><thead><tr><th>Tool</th>' +
        '<th class="r">Tokens saved</th></tr></thead>' +
        '<tbody>' + toolRows + '</tbody></table></div></div>'
      : '';
    if (!modelsCard && !toolsCard) return '';
    return '<div class="row r2" style="margin-bottom:16px">' + modelsCard + toolsCard + '</div>';
  }

  _renderShare(esc) {
    return (
      '<div class="card"><div class="card-header"><h3>Share your ROI</h3></div>' +
      '<p class="hs">Export a signed, shareable report for your manager, finance, or README:</p>' +
      '<pre class="mono" style="background:var(--bg-elev,#0d1117);padding:10px;border-radius:8px;overflow:auto">' +
      'lean-ctx roi --export roi.md</pre></div>'
    );
  }
}

customElements.define('cockpit-roi', CockpitRoi);

(function registerRoiLoader() {
  function doRegister() {
    var R = window.LctxRouter;
    if (!R || !R.registerLoader) return;
    R.registerLoader('roi', function () {
      var el = document.getElementById('roiView');
      if (el && typeof el.loadData === 'function') return el.loadData();
    });
  }
  if (window.LctxRouter && window.LctxRouter.registerLoader) doRegister();
  else document.addEventListener('DOMContentLoaded', doRegister);
})();

export { CockpitRoi };