lean-ctx-engine 3.12.3

LeanCTX | OSS library: the open-source runtime for the Context Gateway for AI agents behind the lean-ctx CLI, MCP server and proxy integrations.
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
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
// SPDX-License-Identifier: Apache-2.0
import { atom, read, update } from 'claude-code'
import type { Elements, EngineInterface, On, RenderChildren } from 'claude-code'

import type { Board, Depth, FeedItem, Frame, Overlay, Proof, SourceStat, TurnStat, ValueSnap } from '../types'

// The LeanCTX cockpit, in the product's own terms: a Context Gateway between
// Claude and the sources it reads. It shows what Claude saw and how deep, the
// two checks before the handoff (may it be read? may it be delivered?), the
// tokens kept out, and the receipts behind them. A handoff line runs above the
// prompt while Claude works; short cards mark turns and milestones. lean-ctx's
// status line below the prompt keeps the one-line totals.

const PANE = 'leanctx-cockpit'
const DOCK_COLUMNS = 62
// lean-ctx writes a session's snapshot at most once a second.
const REFRESH_AFTER_TOOL_MS = 1_300
const REFRESH_MS = 15_000
// `lean-ctx value` re-verifies both hash chains end to end (~8 s): rarely, in the background.
const PROVE_FIRST_MS = 4_000
const PROVE_MS = 5 * 60_000
const PROVE_TIMEOUT_MS = 120_000
const FRAME_MS = 70
const COUNT_EASE = 0.16
const TURN_OVERLAY_MS = 6_500
const MILESTONE_OVERLAY_MS = 8_000
// A call travels the handoff line: into the gate, then on to Claude.
const PACKET_IN_MS = 700
const PACKET_OUT_MS = 1_600
// Same freshness window as lean-ctx's own status line.
const ACTIVE_MS = 12 * 3_600_000
const TURNS_KEPT = 60
const HISTORY_KEPT = 480
// The saving curve: three rows of eighth blocks (24 levels) and an axis line.
const CURVE_ROWS = 3
const FEED_KEPT = 30
const FEED_SHOWN = 5
const SOURCES_KEPT = 200
const TOKEN_MILESTONES = [10e3, 25e3, 50e3, 100e3, 250e3, 500e3, 1e6, 2.5e6, 5e6, 10e6, 25e6]
const SOURCE_MILESTONES = [25, 100, 250, 1000]
const LEAN_CTX_TOOL = /^mcp__lean[-_]ctx__/

// Keys are versioned by name: $.state outlives reloads, older shapes stay behind.
const board = atom({ plugin: 'lean-ctx', key: 'gateway' } as const, {
  milestone: 0,
  turns: [],
  isWorking: false,
  leanRunning: 0,
  feed: [],
  sources: {},
  turnSeen: { sources: 0, governed: 0, structure: 0 },
  celebrated: [],
} as Board)
const frame = atom({ plugin: 'lean-ctx', key: 'clock' } as const, { tick: 0 } as Frame)

// ── LeanCTX brand: leanctx.com premium-tokens.css (dark theme) ─────────────
const CANVAS = 0x111626
const PANEL = 0x1b2034
const FOREGROUND = 0xf1f3ff
const SECONDARY = 0xafb8d0
const HAIRLINE = 0x353e5a
const SIGNATURE = 0x668cff
const BLUE_FIELD = 0x31416c
// Derived within the family for light and motion; status colours only where earned.
const ICE = 0xc9d6ff
const VIOLET = 0xa08cff
const AMBER = 0xf5b84b
const DANGER = 0xff6b81
const OK = 0x7ee0b5
const DEFAULT_BG = 0x01000000
const BRAND_SWEEP = [BLUE_FIELD, SIGNATURE, ICE, VIOLET, SIGNATURE, BLUE_FIELD]
const DATA = [BLUE_FIELD, SIGNATURE, ICE]
const hex = (c: number) => `#${c.toString(16).padStart(6, '0')}`

// Each read depth keeps its colour everywhere: mix bar, legend, receipts.
const DEPTHS: readonly Depth[] = ['structure', 'passage', 'full', 'search', 'command']
const DEPTH_COLOR: Record<Depth, number> = {
  structure: SIGNATURE,
  passage: ICE,
  full: VIOLET,
  search: 0x7fc8ff,
  command: 0x8fa9ff,
}

function mix(a: number, b: number, t: number): number {
  const k = Math.min(1, Math.max(0, t))
  const ch = (s: number) => {
    const x = (a >> s) & 255
    const y = (b >> s) & 255
    return Math.round(x + (y - x) * k) << s
  }
  return ch(16) | ch(8) | ch(0)
}

function ramp(stops: readonly number[], t: number): number {
  const u = Math.min(1, Math.max(0, t)) * (stops.length - 1)
  const i = Math.min(stops.length - 2, Math.floor(u))
  return mix(stops[i] ?? 0, stops[i + 1] ?? 0, u - i)
}
const cycle = (stops: readonly number[], t: number) => ramp(stops, ((t % 1) + 1) % 1)

// Budgets stay brand blue and warm only under real pressure.
const pressure = (frac: number) =>
  frac < 0.7 ? SIGNATURE : frac < 0.9 ? mix(SIGNATURE, AMBER, (frac - 0.7) / 0.2) : mix(AMBER, DANGER, (frac - 0.9) / 0.1)

// ── raster cells ───────────────────────────────────────────────────────────
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

function toBase64(bytes: Uint8Array): string {
  let out = ''
  for (let i = 0; i < bytes.length; i += 3) {
    const a = bytes[i] ?? 0
    const b = bytes[i + 1]
    const c = bytes[i + 2]
    const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0)
    out += B64.charAt((n >> 18) & 63) + B64.charAt((n >> 12) & 63)
    out += b === undefined ? '=' : B64.charAt((n >> 6) & 63)
    out += c === undefined ? '=' : B64.charAt(n & 63)
  }
  return out
}

class Grid {
  private readonly view: DataView
  private readonly bytes: Uint8Array
  constructor(
    readonly cols: number,
    readonly rows: number,
  ) {
    this.bytes = new Uint8Array(cols * rows * 12)
    this.view = new DataView(this.bytes.buffer)
    for (let i = 0; i < cols * rows; i++) this.put(i, 0x20, DEFAULT_BG, DEFAULT_BG)
  }
  private put(i: number, ch: number, fg: number, bg: number) {
    this.view.setUint32(i * 12, ch, true)
    this.view.setUint32(i * 12 + 4, fg, true)
    this.view.setUint32(i * 12 + 8, bg, true)
  }
  set(x: number, y: number, ch: string, fg: number, bg = DEFAULT_BG) {
    if (x < 0 || y < 0 || x >= this.cols || y >= this.rows) return
    this.put(y * this.cols + x, ch.charCodeAt(0), fg, bg)
  }
  encode(): string {
    return toBase64(this.bytes)
  }
}

// The handoff line: sources on the left, Claude on the right, the gateway ◆ in
// between. While Claude works the line breathes in brand colours; every call
// travels it as a light — governed calls pass through the gate, direct calls
// (Claude's own tools) run past it dimmed, failures turn red.
function handoffCells(cols: number, tick: number, feed: readonly FeedItem[], now: number, isWorking: boolean): string {
  const g = new Grid(cols, 1)
  const gate = Math.floor(cols * 0.5)
  const breath = (Math.sin(tick * 0.09) + 1) / 2
  for (let x = 0; x < cols; x++) {
    const base = isWorking
      ? mix(HAIRLINE, cycle(BRAND_SWEEP, x / cols / 1.6 - tick * 0.004), 0.3 + breath * 0.3)
      : HAIRLINE
    g.set(x, 0, '─', base)
  }
  const gatePulse = isWorking ? (Math.sin(tick * 0.25) + 1) / 2 : 0
  const draw = (x: number, color: number, isHead: boolean) => {
    const at = Math.round(x)
    g.set(at, 0, isHead ? '●' : '━', color)
    if (isHead) {
      g.set(at - 1, 0, '━', mix(color, HAIRLINE, 0.35))
      g.set(at - 2, 0, '─', mix(color, HAIRLINE, 0.65))
    }
  }
  for (const item of feed) {
    const color = item.isError ? DANGER : item.governed ? ICE : SECONDARY
    if (item.ms === undefined) {
      const age = now - item.startedAt
      // Direct calls never enter the gate: they cross the whole line.
      if (!item.governed) {
        if (age < PACKET_IN_MS + PACKET_OUT_MS) draw(((age / (PACKET_IN_MS + PACKET_OUT_MS)) % 1) * (cols - 1), color, true)
      } else if (age < PACKET_IN_MS) draw((age / PACKET_IN_MS) * gate, color, true)
      continue
    }
    const age = now - (item.startedAt + item.ms)
    if (age < 0 || age > PACKET_OUT_MS) continue
    const from = item.governed ? gate : (age / PACKET_OUT_MS) * gate
    draw(from + ((cols - 1 - from) * age) / PACKET_OUT_MS, color, true)
  }
  const waiting = feed.some(f => f.governed && f.ms === undefined && now - f.startedAt >= PACKET_IN_MS)
  g.set(gate, 0, '◆', waiting ? mix(SIGNATURE, FOREGROUND, gatePulse) : isWorking ? mix(SIGNATURE, ICE, gatePulse * 0.6) : BLUE_FIELD)
  return g.encode()
}

// A light that sweeps once across the band as a turn card opens.
function sweepCells(cols: number, progress: number, tick: number): string {
  const g = new Grid(cols, 1)
  const head = progress * (cols + 12) - 6
  for (let x = 0; x < cols; x++) {
    const near = Math.max(0, 1 - Math.abs(x - head) / 8)
    const base = cycle(BRAND_SWEEP, x / cols / 1.5 - tick * 0.01)
    g.set(x, 0, '━', mix(mix(HAIRLINE, base, 0.75), FOREGROUND, near))
  }
  return g.encode()
}

// Twinkling field for milestones: deterministic per cell, phased by the tick.
function sparkleCells(cols: number, tick: number, seed: number): string {
  const g = new Grid(cols, 1)
  const glyphs = ['·', '✧', '✦', '✧', '·']
  for (let x = 0; x < cols; x++) {
    const h = Math.sin((x + 1) * 12.9898 + seed * 78.233) * 43758.5453
    const r = h - Math.floor(h)
    if (r < 0.55) continue
    const phase = (Math.sin(tick * 0.35 + r * 40) + 1) / 2
    g.set(x, 0, glyphs[Math.floor(phase * (glyphs.length - 1) + 0.5)] ?? '·', mix(BLUE_FIELD, r > 0.8 ? ICE : SIGNATURE, phase))
  }
  return g.encode()
}

// 4×7 display figures, folded into half blocks: four terminal rows.
const FONT: Record<string, readonly string[]> = {
  '0': ['.##.', '#..#', '#..#', '#..#', '#..#', '#..#', '.##.'],
  '1': ['.#..', '##..', '.#..', '.#..', '.#..', '.#..', '###.'],
  '2': ['.##.', '#..#', '...#', '..#.', '.#..', '#...', '####'],
  '3': ['###.', '...#', '...#', '.##.', '...#', '...#', '###.'],
  '4': ['#..#', '#..#', '#..#', '####', '...#', '...#', '...#'],
  '5': ['####', '#...', '###.', '...#', '...#', '#..#', '.##.'],
  '6': ['.##.', '#...', '#...', '###.', '#..#', '#..#', '.##.'],
  '7': ['####', '...#', '..#.', '..#.', '.#..', '.#..', '.#..'],
  '8': ['.##.', '#..#', '#..#', '.##.', '#..#', '#..#', '.##.'],
  '9': ['.##.', '#..#', '#..#', '.###', '...#', '...#', '.##.'],
  '.': ['.', '.', '.', '.', '.', '.', '#'],
  K: ['#..#', '#.#.', '##..', '#...', '##..', '#.#.', '#..#'],
  M: ['#...#', '##.##', '#.#.#', '#...#', '#...#', '#...#', '#...#'],
  B: ['###.', '#..#', '#..#', '###.', '#..#', '#..#', '###.'],
}
const FIGURE_ROWS = 4
const figureWidth = (text: string) => [...text].reduce((w, ch) => w + (FONT[ch]?.[0]?.length ?? 0) + 1, -1)

// A display figure: a slow brand sweep runs across it; brighter while it counts.
function figureCells(text: string, cols: number, phase: number, lift: number): string {
  const g = new Grid(cols, FIGURE_ROWS)
  const width = Math.max(8, figureWidth(text))
  let x0 = 0
  for (const ch of text) {
    const glyph = FONT[ch]
    if (!glyph) continue
    const w = glyph[0]?.length ?? 0
    for (let x = 0; x < w; x++) {
      const color = mix(cycle(BRAND_SWEEP, (x0 + x) / width / 2.2 - phase), FOREGROUND, lift)
      for (let r = 0; r < FIGURE_ROWS; r++) {
        const top = glyph[r * 2]?.charAt(x) === '#'
        const bottom = glyph[r * 2 + 1]?.charAt(x) === '#'
        const cell = top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ''
        if (cell) g.set(x0 + x, r, cell, color)
      }
    }
    x0 += w + 1
  }
  return g.encode()
}

const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']

// Meter: fractional blocks on a dim track, a gradient along the fill.
function meterCells(cols: number, frac: number, color: (t: number) => number): string {
  const g = new Grid(cols, 1)
  const filled = Math.min(1, Math.max(0, frac)) * cols
  const full = Math.floor(filled)
  const part = Math.floor((filled - full) * 8)
  for (let x = 0; x < cols; x++) {
    const c = color(x / Math.max(1, cols - 1))
    if (x < full) g.set(x, 0, '█', c)
    else if (x === full && part > 0) g.set(x, 0, EIGHTHS[part] ?? '▏', c, PANEL)
    else g.set(x, 0, '█', PANEL)
  }
  return g.encode()
}

// The depth mix: one bar, a segment per read depth in its colour.
function mixCells(cols: number, counts: Record<Depth, number>): string {
  const g = new Grid(cols, 1)
  const total = DEPTHS.reduce((n, d) => n + counts[d], 0)
  if (total === 0) {
    for (let x = 0; x < cols; x++) g.set(x, 0, '█', PANEL)
    return g.encode()
  }
  let x = 0
  DEPTHS.forEach((d, i) => {
    const isLast = i === DEPTHS.length - 1
    const width = isLast ? cols - x : Math.round((counts[d] / total) * cols)
    for (let k = 0; k < width && x < cols; k++, x++) g.set(x, 0, '█', DEPTH_COLOR[d])
  })
  return g.encode()
}

const LEVELS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']

export type CurvePoint = { at: number; saved: number; raw?: number }

// The value a running total held at time `t`: the last sample at or before it.
function stepAt(points: readonly CurvePoint[], t: number, pick: (p: CurvePoint) => number | undefined): number {
  let value = 0
  for (const p of points) {
    if (p.at > t) break
    value = pick(p) ?? value
  }
  return value
}

// The saving curve, true to time: x runs from the session's first sample to
// now, y from zero to the largest total, so a column is a slice of wall time
// and a height is a token count. The raw total sits behind as a dim envelope;
// "kept out" fills in front of it, rising from blue field to ice, so the gap
// between them is what Claude still received. Columns in which lean-ctx kept
// tokens out are lit; the newest one breathes while Claude works.
export function curveCells(cols: number, rows: number, points: readonly CurvePoint[], now: number, pulse: number): string {
  const g = new Grid(cols, rows)
  const start = points[0]?.at ?? now
  const span = Math.max(60_000, now - start)
  const timeAt = (x: number) => start + ((x + 1) / cols) * span
  const saved = Array.from({ length: cols }, (_, x) => stepAt(points, timeAt(x), p => p.saved))
  const raw = Array.from({ length: cols }, (_, x) => Math.max(saved[x] ?? 0, stepAt(points, timeAt(x), p => p.raw)))
  const top = Math.max(1, ...raw, ...saved)
  const steps = rows * 8
  const eighths = (v: number) => (v <= 0 ? 0 : Math.max(1, Math.round((v / top) * steps)))
  const envelope = mix(PANEL, BLUE_FIELD, 0.7)
  for (let x = 0; x < cols; x++) {
    const keptH = eighths(saved[x] ?? 0)
    const rawH = eighths(raw[x] ?? 0)
    const gained = (saved[x] ?? 0) > (x > 0 ? (saved[x - 1] ?? 0) : 0)
    const isLast = x === cols - 1
    for (let r = 0; r < rows; r++) {
      const y = rows - 1 - r
      const kept = Math.min(8, Math.max(0, keptH - r * 8))
      const behind = Math.min(8, Math.max(0, rawH - r * 8))
      if (kept === 0 && behind === 0) {
        if (r === 0) g.set(x, y, '▁', HAIRLINE)
        continue
      }
      let color = ramp(DATA, (r * 8 + kept) / steps)
      if (gained && r * 8 + kept >= keptH) color = mix(color, ICE, 0.55)
      if (isLast) color = mix(color, FOREGROUND, 0.25 + pulse * 0.55)
      // A partial "kept" block sits on the raw envelope only where raw fills
      // the whole cell; a cell cannot show two partial heights, and drawing
      // the envelope there would overstate raw.
      const bg = behind >= 8 ? envelope : DEFAULT_BG
      if (kept === 0) g.set(x, y, LEVELS[behind - 1] ?? '█', envelope)
      else g.set(x, y, LEVELS[kept - 1] ?? '█', color, kept < 8 ? bg : DEFAULT_BG)
    }
  }
  return g.encode()
}

// What a stretch of the curve adds: the gain over the last `windowMs`.
export function recentGain(points: readonly CurvePoint[], now: number, windowMs: number): number {
  const latest = points.at(-1)?.saved ?? 0
  return Math.max(0, latest - stepAt(points, now - windowMs, p => p.saved))
}

// ── formatting (lean-ctx's own style: 49.1K, 1.2M) ─────────────────────────
function compact(n: number): string {
  const a = Math.abs(n)
  const f = (v: number, s: string) => `${v >= 100 ? v.toFixed(0) : v.toFixed(1)}${s}`
  if (a >= 1e9) return f(n / 1e9, 'B')
  if (a >= 1e6) return f(n / 1e6, 'M')
  if (a >= 1e3) return f(n / 1e3, 'K')
  return Math.round(n).toString()
}
const grouped = (n: number) => Math.round(n).toLocaleString('en-US')
const pct = (n: number | undefined) => (n === undefined ? '—' : `${Math.round(n)}%`)
const dur = (ms: number) => (ms < 1000 ? `${ms}ms` : ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms / 60_000)}m`)
// The engine's own turn-duration style: 3s, 1m 4s.
function clock(ms: number): string {
  const s = Math.max(0, Math.round(ms / 1000))
  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
}
function ago(ms: number): string {
  const s = Math.max(0, Math.round(ms / 1000))
  return s < 60 ? `${s}s ago` : s < 3600 ? `${Math.round(s / 60)}m ago` : `${Math.round(s / 3600)}h ago`
}
function span(ms: number): string {
  const m = Math.max(0, Math.round(ms / 60_000))
  return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`
}
const plural = (n: number, one: string, many: string) => `${compact(n)} ${n === 1 ? one : many}`

function shortTool(tool: string): string {
  return /^mcp__.+?__(.+)$/.exec(tool)?.[1] ?? tool
}

function tail(path: string): string {
  return path.split('/').filter(Boolean).at(-1) ?? path
}

type Rec = Record<string, unknown>
const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v)
const num = (r: Rec, k: string) => (typeof r[k] === 'number' ? (r[k] as number) : 0)
const str = (r: Rec, k: string) => (typeof r[k] === 'string' ? (r[k] as string) : undefined)

function describe(args: Rec): string {
  for (const key of ['path', 'file_path', 'command', 'pattern', 'query', 'task', 'symbol', 'url', 'description', 'prompt']) {
    const v = args[key]
    if (typeof v !== 'string' || v.length === 0) continue
    const line = v.split('\n')[0] ?? ''
    return key.endsWith('path') ? tail(line) : line.slice(0, 80)
  }
  const paths = args['paths']
  if (Array.isArray(paths) && typeof paths[0] === 'string') return `${tail(paths[0])} +${paths.length - 1}`
  return ''
}

// The read depth a call brought a source to Claude at, or none for calls that
// bring no source (edits, sessions, agents). `ctx_read` modes map onto the
// website's depths: structure (map, signatures), a passage (lines, reference,
// task, compressed modes), or the whole file.
function depthOf(tool: string, args: Rec): Depth | undefined {
  const t = shortTool(tool)
  if (/^(ctx_read|ctx_multi_read|Read)$/.test(t)) {
    const mode = str(args, 'mode') ?? ''
    if (/^(map|signatures)$/.test(mode)) return 'structure'
    if (/^(lines|reference|task|aggressive|entropy|diff)/.test(mode)) return 'passage'
    return 'full'
  }
  if (/search|grep|glob|tree|compose|callgraph|graph|semantic/i.test(t)) return 'search'
  if (/shell|bash|execute/i.test(t)) return 'command'
  return undefined
}

const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']

// ── lean-ctx value snapshots ───────────────────────────────────────────────
function parseSnap(text: string): ValueSnap | undefined {
  const r: unknown = JSON.parse(text)
  if (!isRec(r)) return undefined
  const sessionId = str(r, 'session_id')
  const projectRoot = str(r, 'project_root')
  if (!sessionId || !projectRoot) return undefined
  const sec = isRec(r['security']) ? r['security'] : {}
  return {
    sessionId,
    projectRoot,
    hostPid: typeof r['host_pid'] === 'number' ? (r['host_pid'] as number) : undefined,
    startedAt: str(r, 'started_at'),
    updatedAt: str(r, 'updated_at'),
    toolCalls: num(r, 'tool_calls'),
    tokensInput: num(r, 'tokens_input'),
    tokensSaved: num(r, 'tokens_saved'),
    cacheHits: num(r, 'cache_hits'),
    filesRead: num(r, 'files_read'),
    commandsRun: num(r, 'commands_run'),
    secretsRedacted: num(sec, 'secrets_redacted'),
    shellBlocked: num(sec, 'shell_blocked'),
    pathBlocked: num(sec, 'path_blocked'),
    injectionFlagged: num(sec, 'injection_flagged'),
  }
}

// One spelling per path: Windows hands out backslashes, either drive-letter
// case and sometimes the `\\?\` verbatim prefix (#2077).
const normPath = (p: string) => {
  const s = p.replace(/^\\\\\?\\/, '').replace(/\\/g, '/').replace(/\/+$/, '')
  return /^[a-z]:(\/|$)/i.test(s) ? s.toLowerCase() : s
}
const inProject = (cwd: string, root: string) => {
  const [c, r] = [normPath(cwd), normPath(root)]
  return c === r || c.startsWith(`${r}/`)
}
const newest = <S extends { updatedAt?: string }>(snaps: S[]) =>
  [...snaps].sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))[0]

/**
 * The session this Claude Code shows. Nearest ancestor first: the session whose
 * server recorded it as `host_pid`. `host_pid` alone decides the conversation,
 * as for the status line, so a Claude Code started outside the project (in
 * $HOME) still shows the session its agent bound to a project (#2181). Among
 * several sessions of one host, the one for the working directory's project
 * wins, then the newest.
 */
export function pickSession<S extends { hostPid?: number; projectRoot: string; updatedAt?: string }>(
  snaps: readonly S[],
  hosts: readonly number[],
  cwd: string,
): S | undefined {
  for (const host of hosts) {
    const mine = snaps.filter(s => s.hostPid === host)
    const snap = newest(mine.filter(s => inProject(cwd, s.projectRoot))) ?? newest(mine)
    if (snap) return snap
  }
  return undefined
}

const countersOf = (s: ValueSnap | undefined) => ({
  sessionId: s?.sessionId,
  saved: s?.tokensSaved ?? 0,
  calls: s?.toolCalls ?? 0,
  files: s?.filesRead ?? 0,
  commands: s?.commandsRun ?? 0,
})

// ── module state (a reload starts it over; what drawing reads lives in $.state) ──
let valueDir: string | undefined
let cwd: string | undefined
let refreshing = false
let refreshAgain = false
let pendingRefresh: { cancel: () => void } | undefined
let frames: { cancel: () => void } | undefined
let isWorking = false
let overlayUntil = 0
let packetsUntil = 0
let proving = false
// The token figure counts toward the session total.
let shown = -1
let target = 0

// $.state outlives reloads: fill in anything an older build left out.
async function readBoard($: EngineInterface): Promise<Board> {
  const b: Partial<Board> = (await read($, board)) ?? {}
  return {
    ...b,
    milestone: typeof b.milestone === 'number' ? b.milestone : 0,
    turns: Array.isArray(b.turns) ? b.turns : [],
    isWorking: b.isWorking === true,
    leanRunning: typeof b.leanRunning === 'number' ? Math.max(0, b.leanRunning) : 0,
    feed: Array.isArray(b.feed) ? b.feed : [],
    sources: isRec(b.sources) ? (b.sources as Record<string, SourceStat>) : {},
    turnSeen: isRec(b.turnSeen) ? b.turnSeen : { sources: 0, governed: 0, structure: 0 },
    celebrated: Array.isArray(b.celebrated) ? b.celebrated : [],
  }
}

// Our ancestors, nearest first, as lean-ctx sees them from the process that
// runs it: the agent is among them, whether hooks run in it or in a worker.
// lean-ctx walks the tree natively; `ps` and `sh` are missing on Windows (#2077).
let chain: number[] | undefined

// The data directory lean-ctx reports itself, so XDG and custom homes hold,
// and the ancestors whose sessions serve this conversation.
async function locate($: EngineInterface): Promise<{ dir: string; cwd: string; hosts: number[] }> {
  if (!valueDir || !chain) {
    const { exitCode, stdout } = await $.process.run(['lean-ctx', 'prompt-segment', '--json'], { timeoutMs: 15_000 })
    if (exitCode !== 0) throw new Error('lean-ctx prompt-segment failed')
    const parsed: unknown = JSON.parse(stdout)
    const watch = isRec(parsed) ? str(parsed, 'watch') : undefined
    if (!watch) throw new Error('lean-ctx reports no value directory')
    const hosts = isRec(parsed) && Array.isArray(parsed['hosts']) ? parsed['hosts'] : []
    valueDir = watch.replace(/[\\/]projects[\\/]?$/, '')
    chain = hosts.filter((h): h is number => typeof h === 'number')
  }
  cwd ??= await $.session.cwd()
  return { dir: valueDir, cwd, hosts: chain }
}

function scheduleRefresh($: EngineInterface) {
  if (pendingRefresh) return
  pendingRefresh = $.clock.after(REFRESH_AFTER_TOOL_MS, () => {
    pendingRefresh = undefined
    void refresh($).catch(() => {})
  })
}

// A gateway first worth a moment, celebrated once per conversation.
function gatewayFirst(prev: ValueSnap | undefined, snap: ValueSnap, sources: number, done: string[]): { key: string; title: string; detail: string } | undefined {
  const firsts = [
    {
      key: 'first-secret',
      hit: snap.secretsRedacted > 0 && (prev?.secretsRedacted ?? 0) === 0,
      title: 'FIRST SECRET KEPT OUT OF CLAUDE’S CONTEXT',
      detail: 'Redacted before the handoff · see lean-ctx value',
    },
    {
      key: 'first-block',
      hit: snap.shellBlocked + snap.pathBlocked > 0 && (prev ? prev.shellBlocked + prev.pathBlocked : 0) === 0,
      title: 'FIRST READ STOPPED AT THE GATE',
      detail: 'A command or path outside your rules was blocked',
    },
    ...SOURCE_MILESTONES.map(n => ({
      key: `sources-${n}`,
      hit: sources >= n,
      title: `${grouped(n)} SOURCES THROUGH THE GATEWAY`,
      detail: 'Each one selected, checked and recorded',
    })),
  ]
  return firsts.find(f => f.hit && !done.includes(f.key))
}

// Reads the snapshot of the lean-ctx session serving this conversation (the
// same one lean-ctx's status line shows), keeps its history, raises milestones.
async function refresh($: EngineInterface) {
  if (refreshing) {
    refreshAgain = true
    return
  }
  refreshing = true
  try {
    // Claude's side (window, cost) rides along: refresh only runs from timers.
    await refreshUsage($)
    const where = await locate($)
    const sessions = `${where.dir}/sessions`
    const now = Date.now()
    const entries = (await $.fs.list(sessions)).filter(
      e => e.kind === 'file' && e.name.endsWith('.json') && now - e.mtimeMs < ACTIVE_MS,
    )
    const snaps: ValueSnap[] = []
    for (const e of entries) {
      try {
        const s = parseSnap(await $.fs.read(`${sessions}/${e.name}`))
        if (s) snaps.push(s)
      } catch {
        // a snapshot mid-write or gone: skip it this round
      }
    }
    const snap = pickSession(snaps, where.hosts, where.cwd)
    if (!snap) {
      chain = undefined // a reconnect starts a new server: look again next time
      return
    }

    const b = await readBoard($)
    const at = Date.now()
    const governedSources = Object.values(b.sources).filter(s => s.governed).length
    // The first look only records where the session stands; later crossings celebrate.
    const isLive = b.milestone > 0 || b.turns.length > 0 || b.snap !== undefined
    const reached = TOKEN_MILESTONES.filter(m => m <= snap.tokensSaved).pop() ?? 0
    const first = isLive && b.snap?.sessionId === snap.sessionId ? gatewayFirst(b.snap, snap, governedSources, b.celebrated) : undefined
    let celebration: Overlay | undefined
    if (first) {
      celebration = { kind: 'milestone', title: first.title, detail: first.detail, at }
    } else if (reached > b.milestone && isLive) {
      celebration = {
        kind: 'milestone',
        title: `${compact(reached)} TOKENS KEPT OUT OF CONTEXT`,
        detail: 'Selected, not sent · numbers from lean-ctx value',
        at,
      }
    }
    await update($, board, x => {
      const same = x.history?.sessionId === snap.sessionId ? x.history.points : []
      const last = same.at(-1)
      // A point per change, and one a minute while flat, so time stays honest.
      const isNew =
        !last || last.saved !== snap.tokensSaved || last.raw !== snap.tokensInput || at - last.at >= 60_000
      const points = isNew ? [...same, { at, saved: snap.tokensSaved, raw: snap.tokensInput }].slice(-HISTORY_KEPT) : same
      return {
        ...x,
        snap,
        history: { sessionId: snap.sessionId, points },
        milestone: Math.max(x.milestone ?? 0, reached),
        celebrated: first ? [...(Array.isArray(x.celebrated) ? x.celebrated : []), first.key] : x.celebrated,
        overlay: celebration ?? x.overlay,
      }
    })
    if (shown < 0) shown = 0
    target = snap.tokensSaved
    if (celebration && cardsEnabled) {
      overlayUntil = at + MILESTONE_OVERLAY_MS
      const title = celebration.kind === 'milestone' ? celebration.title : ''
      $.ui.toast(`★ LeanCTX · ${title.toLowerCase()}`, { timeoutMs: 6000 })
    }
    if (celebration || shown !== target) startFrames($)
  } catch {
    // the cockpit is cosmetic; lean-ctx's status line still carries the numbers
  } finally {
    refreshing = false
    if (refreshAgain) {
      refreshAgain = false
      scheduleRefresh($)
    }
  }
}

// Prove: re-derive the evidence from both hash chains, the way `lean-ctx value` does.
async function prove($: EngineInterface) {
  if (proving) return
  proving = true
  const before = (await readBoard($)).proof
  await update($, board, x => ({ ...x, proof: { ...(x.proof ?? {}), isRunning: true } }))
  try {
    const b = await readBoard($)
    const argv = ['lean-ctx', 'value', '--json', ...(b.snap ? ['--session', b.snap.sessionId] : [])]
    const { stdout } = await $.process.run(argv, { timeoutMs: PROVE_TIMEOUT_MS })
    const r: unknown = JSON.parse(stdout)
    if (!isRec(r)) throw new Error('lean-ctx value returned no report')
    const ledger = isRec(r['ledger']) ? r['ledger'] : {}
    const audit = isRec(r['audit']) ? r['audit'] : {}
    const evidence = isRec(r['ledger_evidence']) ? r['ledger_evidence'] : {}
    const proof: Proof = {
      isRunning: false,
      checkedAt: Date.now(),
      ledgerEntries: num(ledger, 'entries'),
      ledgerIntact: ledger['intact'] === true,
      auditEntries: num(audit, 'entries'),
      auditIntact: audit['intact'] === true,
      sessionEntries: num(evidence, 'entries'),
    }
    await update($, board, x => ({ ...x, proof }))
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err)
    await update($, board, x => ({ ...x, proof: { ...(before ?? {}), isRunning: false, error: message } }))
  } finally {
    proving = false
  }
}

async function refreshUsage($: EngineInterface) {
  try {
    const u = await $.session.usage()
    await update($, board, x => ({
      ...x,
      contextPct: u.context.percent,
      contextTokens: u.context.tokens,
      window: u.context.window,
      costUsd: u.cost?.usd ?? x.costUsd,
    }))
  } catch {
    // usage is cosmetic; never disturb the session for it
  }
}

// The turn just ended: sum up what lean-ctx did and what Claude saw. Runs from a
// `$.clock.after` timer (scheduled by the `turn.complete` hook) once lean-ctx's
// last write is in: work that outlives a hook's dispatch must not hang off the
// hook itself. Without a snapshot the card still shows, with the turn alone.
async function finishTurn($: EngineInterface, durationMs: number) {
  await refresh($).catch(() => {})
  const b = await readBoard($)
  const now = countersOf(b.snap)
  // A turn that spans a session change (a reconnect) counts from the new
  // session's start, never as the difference of two sessions' counters.
  const start =
    b.turnStart && b.turnStart.sessionId === now.sessionId
      ? b.turnStart
      : { sessionId: now.sessionId, saved: 0, calls: 0, files: 0, commands: 0 }
  const turn: TurnStat = {
    n: b.turns.length + 1,
    durationMs,
    saved: Math.max(0, now.saved - start.saved),
    calls: Math.max(0, now.calls - start.calls),
    files: Math.max(0, now.files - start.files),
    commands: Math.max(0, now.commands - start.commands),
    sources: b.turnSeen.sources,
    governed: b.turnSeen.governed,
    structure: b.turnSeen.structure,
    at: Date.now(),
  }
  await update($, board, x => {
    // A milestone on show keeps the band; the turn card does not cut it short.
    const isMilestoneShowing = x.overlay?.kind === 'milestone' && Date.now() - x.overlay.at < MILESTONE_OVERLAY_MS
    const overlay: Overlay | undefined = isMilestoneShowing ? x.overlay : { kind: 'turn', turn, at: Date.now() }
    const turns = [...(Array.isArray(x.turns) ? x.turns : []), turn].slice(-TURNS_KEPT)
    return { ...x, turns, overlay }
  })
  if (cardsEnabled) overlayUntil = Math.max(overlayUntil, Date.now() + TURN_OVERLAY_MS)
  startFrames($)
}

// Tool calls as the conversation records them: an assistant row's `tool_use`
// blocks start calls (with the source and depth they bring), a `tool_result`
// ends one. Reading rows keeps the cockpit off the tool chain the rest of the
// mod shapes.
async function observeRow($: EngineInterface, row: Rec) {
  const message = isRec(row['message']) ? row['message'] : {}
  const blocks = (Array.isArray(message['content']) ? message['content'] : []).filter(isRec)
  const now = Date.now()
  const started: FeedItem[] = []
  const ended = new Map<string, boolean>()
  for (const block of blocks) {
    if (block['type'] === 'tool_use' && typeof block['id'] === 'string' && typeof block['name'] === 'string') {
      const input = isRec(block['input']) ? block['input'] : {}
      started.push({
        id: block['id'],
        tool: block['name'],
        label: describe(input),
        isSubagent: typeof row['agentId'] === 'string',
        startedAt: now,
        depth: depthOf(block['name'], input),
        governed: LEAN_CTX_TOOL.test(block['name']),
      })
    } else if (block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string') {
      ended.set(block['tool_use_id'], block['is_error'] === true)
    }
  }
  if (started.length === 0 && ended.size === 0) return
  await update($, board, x => {
    const feed = [...(Array.isArray(x.feed) ? x.feed : []), ...started].slice(-FEED_KEPT)
    let running = (x.leanRunning ?? 0) + started.filter(f => f.governed).length
    const done = feed.map(f => {
      const isError = ended.get(f.id)
      if (isError === undefined || f.ms !== undefined) return f
      if (f.governed) running -= 1
      return { ...f, ms: now - f.startedAt, isError }
    })
    // Sources Claude saw: one entry per path / pattern / command, deepest read kept.
    const sources = { ...(isRec(x.sources) ? (x.sources as Record<string, SourceStat>) : {}) }
    const seen = isRec(x.turnSeen) ? { ...x.turnSeen } : { sources: 0, governed: 0, structure: 0 }
    for (const f of started) {
      if (!f.depth || !f.label) continue
      const key = `${f.depth === 'command' ? 'cmd' : 'src'}:${f.label}`
      const prev = sources[key]
      sources[key] = { label: f.label, depth: f.depth, governed: f.governed || (prev?.governed ?? false), n: (prev?.n ?? 0) + 1, last: now }
      seen.sources += 1
      if (f.governed) seen.governed += 1
      if (f.depth === 'structure') seen.structure += 1
    }
    const kept = Object.entries(sources)
      .sort(([, a], [, b]) => b.last - a.last)
      .slice(0, SOURCES_KEPT)
    return { ...x, feed: done, leanRunning: Math.max(0, running), sources: Object.fromEntries(kept), turnSeen: seen }
  })
  // Keep the handoff line moving while calls travel it.
  packetsUntil = now + PACKET_IN_MS + PACKET_OUT_MS
  startFrames($)
  if (ended.size > 0) scheduleRefresh($)
}

// One frame clock for every effect; it runs only while something moves.
function startFrames($: EngineInterface) {
  if (frames) return
  frames = $.clock.every(FRAME_MS, () => {
    if (shown >= 0 && shown !== target) {
      const gap = target - shown
      const step = Math.sign(gap) * Math.max(1, Math.abs(gap) * COUNT_EASE)
      shown = Math.abs(step) >= Math.abs(gap) ? target : Math.round(shown + step)
    }
    const now = Date.now()
    if (!isWorking && !proving && shown === target && now > overlayUntil && now > packetsUntil) {
      frames?.cancel()
      frames = undefined
    }
    void update($, frame, f => ({ tick: (f?.tick ?? 0) + 1 })).catch(() => {})
  })
}

// Whether the sidebar opens with a session: the `cockpit_panel` option, then
// `/cockpit on|off` (#2057). The pulse line, spinner and turn rows stay either way.
let panelEnabled = true
// The band above the prompt while Claude works (`cockpit_pulse_line`) and the
// turn and milestone cards there (`cockpit_turn_cards`), each `/cockpit pulse|cards
// on|off`. The sidebar keeps the same turns and milestones; the spinner stays.
let pulseEnabled = true
let cardsEnabled = true
// The proof clock re-verifies receipts only the sidebar shows (~8 s of work).
let proofClock: { cancel: () => void } | undefined

function startProofClock($: EngineInterface) {
  if (proofClock) return
  $.clock.after(PROVE_FIRST_MS, () => void prove($).catch(() => {}))
  proofClock = $.clock.every(PROVE_MS, () => void prove($).catch(() => {}))
}

function stopProofClock() {
  proofClock?.cancel()
  proofClock = undefined
}

// Registers /cockpit, opens the sidebar unless it is turned off, and starts
// the refresh clock (and, with the sidebar, the proof clock).
async function cockpitStart($: EngineInterface): Promise<void> {
  await $.command.register({
    name: 'cockpit',
    description: 'Open the LeanCTX cockpit; on/off keeps the sidebar open or closed, pulse/cards on|off the lines above the prompt',
    argumentHint: '[on|off|pulse on|pulse off|cards on|cards off]',
    immediate: true,
  })
  $.ui.status(undefined)
  // Work that outlives this hook runs from timers, never off the hook itself.
  $.clock.after(300, () => void refresh($).catch(() => {}))
  $.clock.every(REFRESH_MS, () => void refresh($).catch(() => {}))
  if (!panelEnabled) return
  // Fire-and-forget, and never an unhandled rejection: a surface that draws
  // no panes (or none yet) simply leaves the sidebar closed. No `closeOnEscape`:
  // that also closes on Escape at an idle, empty prompt, and a pane the person
  // closed reopens unasked only from 144 columns, so the sidebar went missing.
  $.ui.open({ id: PANE, title: 'LeanCTX', columns: DOCK_COLUMNS }).catch(() => {})
  startProofClock($)
}

// Stores a choice in the mod's option `option` (`cockpit_panel`, …) so new
// sessions keep it; `false` when the setting could not be written.
async function persistChoice($: EngineInterface, option: string, on: boolean): Promise<boolean> {
  try {
    const row = (await $.config.list()).find(r => r.key.endsWith(`.${option}`))
    if (!row) return false
    const { deny } = await $.config.set({ key: row.key, value: on })
    return deny === undefined
  } catch {
    return false
  }
}

const COCKPIT_USAGE =
  'Usage: /cockpit opens the sidebar · /cockpit on|off keeps it open or closed in new sessions · ' +
  '/cockpit pulse on|off the line above the prompt while Claude works · /cockpit cards on|off the turn and milestone cards.'

// `/cockpit pulse|cards on|off`: shows or hides one of the lines above the
// prompt, now and in new sessions.
async function runSurfaceCommand($: EngineInterface, surface: 'pulse' | 'cards', on: boolean): Promise<string> {
  const [option, name] =
    surface === 'pulse' ? ['cockpit_pulse_line', 'pulse line'] : ['cockpit_turn_cards', 'turn and milestone cards']
  if (surface === 'pulse') pulseEnabled = on
  else cardsEnabled = on
  // Redraw now, so a card or band on show goes away (or a hidden one returns).
  startFrames($)
  const kept = await persistChoice($, option, on)
  const state = on ? 'on' : 'off'
  const back = on ? '' : ` /cockpit ${surface} on brings it back.`
  return kept
    ? `LeanCTX ${name} ${state}, now and in new sessions.${back}`
    : `LeanCTX ${name} ${state} for this session (the setting could not be saved; /config → lean-ctx).`
}

// `/cockpit [on|off]`: no argument opens the sidebar now; on/off also decides
// whether it opens in new sessions. `pulse|cards on|off` go to runSurfaceCommand.
async function runCockpitCommand($: EngineInterface, args: string): Promise<string> {
  const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean)
  const [head, value] = words
  if (head === 'pulse' || head === 'cards') {
    if (words.length !== 2 || (value !== 'on' && value !== 'off')) return COCKPIT_USAGE
    return runSurfaceCommand($, head, value === 'on')
  }
  const choice = words.join(' ')
  if (choice === 'off') {
    panelEnabled = false
    stopProofClock()
    await $.ui.close({ id: PANE }).catch(() => {})
    const kept = await persistChoice($, 'cockpit_panel', false)
    return kept
      ? 'LeanCTX cockpit sidebar off: closed now and in new sessions. /cockpit on brings it back.'
      : 'LeanCTX cockpit sidebar closed for this session (the setting could not be saved; /config → lean-ctx).'
  }
  if (choice !== '' && choice !== 'on') return COCKPIT_USAGE
  const kept = choice === 'on' ? await persistChoice($, 'cockpit_panel', true) : true
  if (choice === 'on') panelEnabled = true
  const { isPlaced } = await $.ui.open({ id: PANE, title: 'LeanCTX', columns: DOCK_COLUMNS })
  $.clock.after(0, () => void refresh($).catch(() => {}))
  $.clock.after(0, () => void prove($).catch(() => {}))
  if (panelEnabled) startProofClock($)
  const opened = isPlaced ? 'LeanCTX cockpit opened.' : 'LeanCTX cockpit is waiting for room (fullscreen, ~110+ columns).'
  if (choice !== 'on') return opened
  return kept ? `${opened} It opens in new sessions again.` : `${opened} (The setting could not be saved; /config → lean-ctx.)`
}

// ── drawing helpers ────────────────────────────────────────────────────────
type Ui = Elements['terminal']

// Text with a soft light travelling through it (Charm-style shimmer).
function shimmer(ui: Ui, text: string, tick: number, base: number, light: number, bold = false) {
  const { Text } = ui
  const head = (tick * 0.9) % (text.length + 14)
  return (
    <Text bold={bold}>
      {[...text].map((ch, i) => (
        <Text color={hex(mix(base, light, Math.max(0, 1 - Math.abs(i - head + 7) / 5)))}>{ch}</Text>
      ))}
    </Text>
  )
}

// A pill: a short label on a filled ground, the way Lip Gloss badges sit.
function pill(ui: Ui, text: string, fg: number, bg: number, bold = false) {
  const { Text } = ui
  return (
    <Text color={hex(fg)} backgroundColor={hex(bg)} bold={bold}>
      {` ${text} `}
    </Text>
  )
}

type Part = { node: RenderChildren; rows: number }
const GAP: Part = { node: '', rows: 1 }

// A panel, btop-style: the title sits in the top border (╭─┐TITLE┌──── meta ─╮),
// a blank row of air inside top and bottom, and a blank row before the next.
function panel(ui: Ui, width: number, title: string, meta: string, parts: Part[]) {
  const { Box, Text } = ui
  const inner = width - 4
  const line = hex(HAIRLINE)
  const metaText = meta ? ` ${meta} ` : ''
  const fill = Math.max(1, width - 6 - title.length - metaText.length - 1)
  const side = (rows: number) => <Text color={line}>{Array.from({ length: rows }, () => '│').join('\n')}</Text>
  return (
    <Box flexDirection="column" marginTop={1}>
      <Text>
        <Text color={line}>╭─┐</Text>
        <Text bold color={hex(SIGNATURE)}>
          {title}
        </Text>
        <Text color={line}>┌{'─'.repeat(fill)}</Text>
        <Text color={hex(SECONDARY)}>{metaText}</Text>
        <Text color={line}>─╮</Text>
      </Text>
      {[GAP, ...parts, GAP].map(p => (
        <Box>
          {side(p.rows)}
          <Box width={inner + 2} paddingX={1} flexDirection="column">
            {p.node === '' ? <Text> </Text> : p.node}
          </Box>
          {side(p.rows)}
        </Box>
      ))}
      <Text color={line}>╰{'─'.repeat(width - 2)}╯</Text>
    </Box>
  )
}

// The handoff line with its two ends named: SOURCES ──◆── CLAUDE.
function handoff(ui: Ui, key: string, cols: number, b: Board, tick: number, now: number) {
  const { Box, Text, Raster } = ui
  const lane = Math.max(8, cols - 16)
  const ends = hex(b.isWorking ? SECONDARY : HAIRLINE)
  return (
    <Box>
      <Text color={ends}>sources </Text>
      <Raster key={key} columns={lane} rows={1} cells={handoffCells(lane, tick, b.feed, now, b.isWorking)} />
      <Text color={ends}> claude</Text>
    </Box>
  )
}

// ── the cockpit ────────────────────────────────────────────────────────────
// The mod's other parts that follow main-loop turns. A plugin registers one
// hook per event, and the cockpit owns `turn.start` / `turn.complete`.
export type TurnObserver = {
  onTurnStart?: () => void
  onMainTurnComplete?: () => void
}

export function registerCockpit(
  on: On,
  turns: TurnObserver = {},
  options: { panel?: boolean; pulse?: boolean; cards?: boolean } = {},
): void {
  panelEnabled = options.panel !== false
  pulseEnabled = options.pulse !== false
  cardsEnabled = options.cards !== false
  // A matcher, because the mod's own `session.start` hook has none (one per
  // event and plugin); headless runs (`claude -p`) get no cockpit at all.
  on('session.start', { isInteractive: true }, async ($, e, next) => {
    try {
      await cockpitStart($)
    } catch {
      // the cockpit is cosmetic; the session starts regardless
    }
    return next(e)
  })

  on('command.run', { command: 'cockpit' }, async ($, e) => ({ text: await runCockpitCommand($, e.args) }))

  on('turn.start', async ($, e, next) => {
    turns.onTurnStart?.()
    try {
      isWorking = true
      const b = await readBoard($)
      await update($, board, x => ({
        ...x,
        isWorking: true,
        leanRunning: 0,
        turnStart: countersOf(b.snap),
        turnSeen: { sources: 0, governed: 0, structure: 0 },
      }))
      startFrames($)
    } catch {
      // the cockpit is cosmetic
    }
    return next(e)
  })

  on('turn.complete', async ($, e, next) => {
    if (e.agentId) return next(e)
    turns.onMainTurnComplete?.()
    isWorking = false
    const usage = 'usage' in e ? e.usage : undefined
    await update($, board, x => {
      let cacheHitPct = x.cacheHitPct
      if (usage) {
        const total = usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens
        if (total > 0) cacheHitPct = (usage.cache_read_input_tokens / total) * 100
      }
      return { ...x, isWorking: false, leanRunning: 0, cacheHitPct }
    })
    if (!e.isAborted) {
      const durationMs = e.durationMs
      $.clock.after(REFRESH_AFTER_TOOL_MS, () => void finishTurn($, durationMs).catch(() => {}))
    }
    return next(e)
  })

  // The row is stored first and handed back unchanged; the cockpit only reads it.
  on('session.append', async ($, e, next) => {
    const stored = await next(e)
    try {
      await observeRow($, e as unknown as Rec)
    } catch {
      // the cockpit is cosmetic
    }
    return stored
  })

  // ── above the prompt: the handoff line while working; a card when a turn ends or a milestone falls
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
    if (e.props.hasSurvey || e.surface !== 'terminal') return next(e)
    // Read the frame before any early return: a hidden band stays subscribed,
    // so `/cockpit pulse|cards on` shows it again at once.
    const f = await read($, frame)
    const b = await readBoard($)
    const tick = f?.tick ?? 0
    const now = Date.now()
    const o = b.overlay
    const ui = $.ui.resolve(e)
    const { Box, Text, Raster } = ui
    const cols = Math.max(24, e.props.bodyColumns)

    if (cardsEnabled && o?.kind === 'milestone' && now - o.at < MILESTONE_OVERLAY_MS && typeof o.title === 'string') {
      const title = `★  ${o.title}  ★`
      return (
        <Box flexDirection="column">
          <Raster key="spark-a" columns={cols} rows={1} cells={sparkleCells(cols, tick, 1)} />
          <Box justifyContent="center">{shimmer(ui, title, tick, SIGNATURE, FOREGROUND, true)}</Box>
          <Box justifyContent="center">
            <Text color={hex(SECONDARY)}>LeanCTX · {o.detail}</Text>
          </Box>
          <Raster key="spark-b" columns={cols} rows={1} cells={sparkleCells(cols, tick + 7, 2)} />
        </Box>
      )
    }

    if (cardsEnabled && o?.kind === 'turn' && now - o.at < TURN_OVERLAY_MS && !b.isWorking) {
      const t = o.turn
      const gap = <Text> </Text>
      return (
        <Box flexDirection="column">
          <Raster key="sweep" columns={cols} rows={1} cells={sweepCells(cols, Math.min(1, (now - o.at) / 900), tick)} />
          <Box>
            <Text bold color={hex(OK)}>
              ✓{' '}
            </Text>
            <Text bold color={hex(FOREGROUND)}>
              Turn {t.n}
            </Text>
            <Text color={hex(SECONDARY)}> · {clock(t.durationMs)}   </Text>
            {t.sources > 0 ? pill(ui, `Claude saw ${plural(t.sources, 'source', 'sources')}`, CANVAS, SIGNATURE, true) : null}
            {t.sources > 0 && t.structure > 0 ? gap : null}
            {t.sources > 0 && t.structure > 0 ? pill(ui, `${t.structure} as structure`, FOREGROUND, PANEL) : null}
            {t.saved > 0 ? gap : null}
            {t.saved > 0 ? pill(ui, `▲ ${compact(t.saved)} tokens kept out`, FOREGROUND, BLUE_FIELD, true) : null}
            {t.sources === 0 && t.saved === 0 ? pill(ui, plural(t.calls, 'lean-ctx call', 'lean-ctx calls'), FOREGROUND, PANEL) : null}
          </Box>
        </Box>
      )
    }

    // The handoff line sits on top; the rest of the chain still renders (#2142).
    if (pulseEnabled && b.isWorking) {
      const below = await next(e)
      return (
        <Box flexDirection="column">
          {handoff(ui, 'band-handoff', cols, b, tick, now)}
          {below}
        </Box>
      )
    }
    return next(e)
  })

  // ── the spinner row: brand spinner, shimmering words, the turn's live gateway work
  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
    if (e.surface !== 'terminal') return next(e)
    const f = await read($, frame)
    const b = await readBoard($)
    const ui = $.ui.resolve(e)
    const { Box, Text } = ui
    const tick = f?.tick ?? 0
    const words = `${e.props.message ?? e.props.word}${e.props.suffix}`
    const start = b.turnStart
    const gained = start && b.snap && start.sessionId === b.snap.sessionId ? Math.max(0, b.snap.tokensSaved - start.saved) : 0
    const seen = b.turnSeen.sources
    return (
      <Box>
        <Text bold color={hex(cycle(BRAND_SWEEP, tick / 30))}>
          {SPINNER[tick % SPINNER.length]}{' '}
        </Text>
        {shimmer(ui, words, tick, SECONDARY, ICE)}
        {seen > 0 || gained > 0 ? <Text>   </Text> : null}
        {seen > 0 ? pill(ui, `◆ ${plural(seen, 'source', 'sources')}`, FOREGROUND, PANEL) : null}
        {seen > 0 && gained > 0 ? <Text> </Text> : null}
        {gained > 0 ? pill(ui, `▲ ${compact(gained)} kept out`, FOREGROUND, BLUE_FIELD) : null}
      </Box>
    )
  })

  // ── the "done in" row: keep the engine's words, add what passed the gateway that turn
  on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
    if (e.surface !== 'terminal') return next(e)
    // Subscribed to the frame either way, so `/cockpit cards on` redraws it.
    await read($, frame)
    if (!cardsEnabled) return next(e)
    const b = await readBoard($)
    const turn = b.turns.find(t => Math.abs(t.durationMs - e.props.durationMs) < 1_000)
    if (!turn || (turn.saved === 0 && turn.calls === 0 && (turn.sources ?? 0) === 0)) return next(e)
    const { Box, Text } = $.ui.resolve(e)
    return (
      <Box>
        <Text color={hex(SECONDARY)}>
          ✻ {e.props.word} for {clock(e.props.durationMs)}
        </Text>
        <Text color={hex(HAIRLINE)}>  │  </Text>
        <Text bold color={hex(SIGNATURE)}>
          ◆ LeanCTX
        </Text>
        <Text color={hex(SECONDARY)}>
          {(turn.sources ?? 0) > 0 ? ` Claude saw ${plural(turn.sources, 'source', 'sources')}` : ''}
          {turn.saved > 0 ? (
            <Text>
              {(turn.sources ?? 0) > 0 ? ' ·' : ''} <Text bold color={hex(FOREGROUND)}>{compact(turn.saved)}</Text> tokens kept out
            </Text>
          ) : null}
        </Text>
      </Box>
    )
  })

  // ── the docked sidebar: what Claude saw, tokens, the two checks, receipts
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
    const b = await readBoard($)
    const f = await read($, frame)
    const tick = f?.tick ?? 0
    const s = b.snap
    const now = Date.now()
    const savedPct = s && s.tokensInput > 0 ? (s.tokensSaved / s.tokensInput) * 100 : undefined
    const sources = Object.values(b.sources)
    const governed = sources.filter(x => x.governed).length
    if (shown < 0 && s) {
      shown = 0
      target = s.tokensSaved
    }

    if (e.surface !== 'terminal') {
      const { Box, Text } = $.ui.resolve(e)
      return (
        <Box flexDirection="column">
          <Text bold color={hex(SIGNATURE)}>LeanCTX · Context Gateway</Text>
          <Text>Claude saw {sources.length} sources · {governed} through the gateway</Text>
          <Text>{compact(s?.tokensSaved ?? 0)} tokens kept out ({pct(savedPct)} leaner)</Text>
        </Box>
      )
    }

    const ui = $.ui.resolve(e)
    const { Box, Text, Raster } = ui
    const W = Math.max(40, e.props.bodyColumns)
    const inner = W - 4
    const pulse = (Math.sin(tick * 0.4) + 1) / 2
    const fg = hex(FOREGROUND)
    const sec = hex(SECONDARY)
    if (shown !== target) startFrames($)

    // ── CLAUDE SAW: sources, how deep, through the gateway or direct
    const counts = Object.fromEntries(DEPTHS.map(d => [d, sources.filter(x => x.depth === d).length])) as Record<Depth, number>
    const direct = sources.length - governed
    const legend = (
      <Text wrap="truncate">
        {DEPTHS.filter(d => counts[d] > 0).map(d => (
          <Text>
            <Text color={hex(DEPTH_COLOR[d])}>■ </Text>
            <Text color={sec}>
              {counts[d]} {d}
              {'   '}
            </Text>
          </Text>
        ))}
      </Text>
    )
    const latest = [...sources].sort((a, x) => x.last - a.last).slice(0, 3)
    const saw: Part[] = [
      {
        node: (
          <Text>
            <Text bold color={fg}>
              {sources.length}
            </Text>
            <Text color={sec}> sources   </Text>
            <Text bold color={hex(SIGNATURE)}>
              {governed}
            </Text>
            <Text color={sec}> through the gateway   </Text>
            <Text bold color={direct > 0 ? fg : sec}>
              {direct}
            </Text>
            <Text color={sec}> direct</Text>
          </Text>
        ),
        rows: 1,
      },
      GAP,
      { node: <Raster key="depth-mix" columns={inner} rows={1} cells={mixCells(inner, counts)} />, rows: 1 },
      { node: sources.length > 0 ? legend : <Text color={sec}>Sources appear as Claude reads them.</Text>, rows: 1 },
      GAP,
      ...latest.map(x => ({
        node: (
          <Box>
            <Box width={2}>
              <Text color={hex(DEPTH_COLOR[x.depth])}>■</Text>
            </Box>
            <Box flexGrow={1}>
              <Text color={fg} wrap="truncate">
                {x.label}
              </Text>
            </Box>
            <Text color={sec}>
              {' '}
              {x.depth}
              {x.governed ? '' : ' · direct'}
            </Text>
          </Box>
        ),
        rows: 1,
      })),
    ]

    // ── TOKENS: kept out of Claude's context, and the budget it used
    const figure = compact(Math.max(0, shown < 0 ? target : shown))
    const isCounting = shown !== target
    const figCols = Math.min(inner, Math.max(12, figureWidth(figure) + 1))
    const sideBySide = inner - figCols >= 24
    const sent = Math.max(0, (s?.tokensInput ?? 0) - (s?.tokensSaved ?? 0))
    const caption = (
      <Box flexDirection="column" marginLeft={sideBySide ? 3 : 0}>
        <Text bold color={fg}>
          tokens kept out
        </Text>
        <Text color={sec}>of Claude’s context</Text>
        <Text>
          <Text bold color={hex(SIGNATURE)}>
            {pct(savedPct)}
          </Text>
          <Text color={sec}> leaner</Text>
        </Text>
        <Text color={sec}>
          {compact(s?.tokensInput ?? 0)} raw → {compact(sent)} delivered
        </Text>
      </Box>
    )
    const figureNode = (
      <Raster key="figure" columns={figCols} rows={FIGURE_ROWS} cells={figureCells(figure, figCols, tick / 90, isCounting ? 0.45 : 0)} />
    )
    const points = b.history?.sessionId === s?.sessionId ? (b.history?.points ?? []) : []
    const nowMs = Date.now()
    const gain10 = recentGain(points, nowMs, 10 * 60_000)
    const axis = (
      <Box width={inner} justifyContent="space-between">
        <Text color={sec}>{points.length > 0 ? `${span(nowMs - (points[0]?.at ?? nowMs))} ago` : ''}</Text>
        <Text>
          <Text color={hex(SIGNATURE)}>█</Text>
          <Text color={sec}> kept out  </Text>
          <Text color={hex(mix(PANEL, BLUE_FIELD, 0.7))}>█</Text>
          <Text color={sec}> raw</Text>
        </Text>
        <Text color={sec}>
          {gain10 > 0 ? <Text color={hex(ICE)}>+{compact(gain10)} </Text> : ''}
          {gain10 > 0 ? '10m · now' : 'now'}
        </Text>
      </Box>
    )
    const ctxFrac = (b.contextPct ?? 0) / 100
    const budgetW = Math.max(8, inner - 16 - 6)
    const tokens: Part[] = [
      ...(sideBySide
        ? [{ node: <Box>{figureNode}{caption}</Box>, rows: FIGURE_ROWS }]
        : [{ node: figureNode, rows: FIGURE_ROWS }, GAP, { node: caption, rows: 4 }]),
      GAP,
      {
        node:
          points.length >= 2 ? (
            <Box flexDirection="column">
              <Raster key="history" columns={inner} rows={CURVE_ROWS} cells={curveCells(inner, CURVE_ROWS, points, nowMs, b.isWorking ? pulse : 0)} />
              {axis}
            </Box>
          ) : (
            <Text color={sec}>{'The curve starts with the next lean-ctx call.\n'}</Text>
          ),
        rows: points.length >= 2 ? CURVE_ROWS + 1 : 2,
      },
      GAP,
      {
        node: (
          <Box>
            <Box width={16}>
              <Text color={sec}>Context budget</Text>
            </Box>
            <Raster key="g-context" columns={budgetW} rows={1} cells={meterCells(budgetW, ctxFrac, () => pressure(ctxFrac))} />
            <Box width={6} justifyContent="flex-end">
              <Text bold color={hex(ctxFrac >= 0.7 ? pressure(ctxFrac) : FOREGROUND)}>
                {pct(b.contextPct)}
              </Text>
            </Box>
          </Box>
        ),
        rows: 1,
      },
      {
        node: (
          <Text color={sec} wrap="truncate">
            {b.contextTokens !== undefined && b.window
              ? `${compact(b.contextTokens)} / ${compact(b.window)} in the window · cache ${pct(b.cacheHitPct)}${b.costUsd !== undefined ? ` · $${b.costUsd.toFixed(2)}` : ''}`
              : 'The window fills in after the first reply'}
          </Text>
        ),
        rows: 1,
      },
    ]

    // ── TWO CHECKS: may it be read? may it be delivered?
    const half = Math.floor(inner / 2)
    const calls = s?.toolCalls ?? 0
    // A redacted result is still delivered, with the secret removed; only a
    // blocked read never reaches Claude.
    const blocked = (s?.shellBlocked ?? 0) + (s?.pathBlocked ?? 0)
    const check = (glyph: string, color: number, value: number, label: string) => (
      <Text>
        <Text color={hex(value > 0 ? color : HAIRLINE)}>{glyph} </Text>
        <Text bold color={value > 0 ? fg : sec}>
          {compact(value)}
        </Text>
        <Text color={sec}> {label}</Text>
      </Text>
    )
    const column = (title: string, rows: RenderChildren[]) => (
      <Box width={half} flexDirection="column">
        <Text bold color={fg}>
          {title}
        </Text>
        {rows}
      </Box>
    )
    const checks: Part[] = [
      {
        node: (
          <Box>
            {column('May it be read?', [
              check('✓', OK, Math.max(0, calls - blocked), 'allowed'),
              check('⊘', VIOLET, s?.pathBlocked ?? 0, 'path outside rules'),
              check('⊘', VIOLET, s?.shellBlocked ?? 0, 'command blocked'),
            ])}
            {column('May it be delivered?', [
              check('✓', OK, Math.max(0, calls - blocked), 'delivered'),
              check('⛨', VIOLET, s?.secretsRedacted ?? 0, 'secrets redacted'),
              check('⚑', VIOLET, s?.injectionFlagged ?? 0, 'injections flagged'),
            ])}
          </Box>
        ),
        rows: 4,
      },
    ]

    // ── RECEIPTS: both chains, and the last calls as source · depth · decision
    const p = b.proof
    const chainRow = (name: string, entries: number | undefined, intact: boolean | undefined) => (
      <Box>
        <Text bold color={hex(intact === undefined ? HAIRLINE : intact ? OK : DANGER)}>
          {intact === undefined ? '○' : intact ? '✓' : '✕'}{'  '}
        </Text>
        <Box width={15}>
          <Text color={fg}>{name}</Text>
        </Box>
        <Text color={sec} wrap="truncate">
          {entries !== undefined ? `${grouped(entries)} entries · ` : ''}
          {intact === undefined ? 'not checked yet' : intact ? 'intact' : 'BROKEN'}
        </Text>
      </Box>
    )
    const calls5 = b.feed.slice(-FEED_SHOWN).reverse()
    const receipts: Part[] = [
      { node: chainRow('Savings ledger', p?.ledgerEntries, p?.ledgerIntact), rows: 1 },
      { node: chainRow('Audit trail', p?.auditEntries, p?.auditIntact), rows: 1 },
      {
        node: p?.isRunning ? (
          <Text color={hex(mix(SIGNATURE, ICE, pulse))}>{SPINNER[tick % SPINNER.length]} verifying both chains…</Text>
        ) : (
          <Text color={sec} wrap="truncate">
            {p?.error ? `verification failed: ${p.error}` : p?.checkedAt ? `verified ${ago(now - p.checkedAt)} · lean-ctx value` : 'first check runs shortly'}
          </Text>
        ),
        rows: 1,
      },
      GAP,
      ...(calls5.length
        ? calls5.map((item, i) => {
            const isRunning = item.ms === undefined
            const isFresh = i === 0 && now - item.startedAt < 2_500
            const decision = isRunning ? 'at the gate' : item.isError ? 'failed' : item.governed ? 'delivered' : 'direct'
            const mark = isRunning ? SPINNER[tick % SPINNER.length] : item.isError ? '✕' : item.governed ? '✓' : '·'
            const markColor = isRunning ? mix(SIGNATURE, ICE, pulse) : item.isError ? DANGER : item.governed ? OK : SECONDARY
            return {
              node: (
                <Box>
                  <Box width={2}>
                    <Text bold color={hex(markColor)}>
                      {mark}
                    </Text>
                  </Box>
                  <Box flexGrow={1}>
                    <Text color={hex(isFresh ? ICE : FOREGROUND)} wrap="truncate">
                      {item.label || shortTool(item.tool)}
                    </Text>
                  </Box>
                  <Box width={11}>
                    <Text color={hex(item.depth ? DEPTH_COLOR[item.depth] : SECONDARY)} wrap="truncate">
                      {' '}
                      {item.depth ?? shortTool(item.tool)}
                    </Text>
                  </Box>
                  <Box width={12} justifyContent="flex-end">
                    <Text color={sec}>
                      {decision}
                      {isRunning ? '' : ` ${dur(item.ms ?? 0)}`}
                    </Text>
                  </Box>
                </Box>
              ),
              rows: 1,
            }
          })
        : [{ node: <Text color={sec}>No tool calls yet</Text>, rows: 1 }]),
    ]

    return (
      <Box flexDirection="column">
        <Box justifyContent="space-between">
          <Text wrap="truncate">
            {pill(ui, '◆ LeanCTX', CANVAS, SIGNATURE, true)}
            <Text color={sec}>  Context Gateway</Text>
          </Text>
          {b.isWorking
            ? pill(ui, `${SPINNER[tick % SPINNER.length]} LIVE`, FOREGROUND, mix(BLUE_FIELD, SIGNATURE, pulse * 0.5), true)
            : pill(ui, '○ IDLE', SECONDARY, PANEL)}
        </Box>
        <Text color={hex(HAIRLINE)}>Control what your AI can see.</Text>
        <Box marginTop={1}>{handoff(ui, 'pane-handoff', W, b, tick, now)}</Box>

        {panel(ui, W, 'CLAUDE SAW', 'this conversation', saw)}
        {panel(ui, W, 'TOKENS', s?.startedAt ? `session ${span(now - Date.parse(s.startedAt))}` : 'this session', tokens)}
        {panel(ui, W, 'TWO CHECKS', 'before the handoff', checks)}
        {panel(ui, W, 'RECEIPTS', p?.ledgerIntact === false || p?.auditIntact === false ? 'chain broken' : 'evidence you can check', receipts)}

        <Box marginTop={1}>
          <Text color={hex(HAIRLINE)} wrap="truncate">
            {s ? `Session ${s.sessionId.slice(-14)} · same numbers as the status line` : 'Looking for this conversation’s lean-ctx session…'}
          </Text>
        </Box>
      </Box>
    )
  })
}