@cesarus777/opencode-cgp 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  Configurable early compaction for root sessions and nested agents, using OpenCode's native durable compaction API. No model-limit changes, prompt injection, custom summaries, or task/workflow rewrites. Observe mode is the default.
4
4
 
5
- Targets **`@opencode/plugin@2.0.29`**. The published SDK, official documentation, runner implementation, and upstream continuation test were inspected. The real 2.0.29 server and native TUI now pass [local verification with a synthetic provider](docs/validation/tui-2.0.29-2026-10-16.md); real-provider compaction and Superpowers validation remain unverified. See [compatibility](docs/compatibility.md).
5
+ Targets **`@opencode/plugin@2.0.29`**. The published SDK, official documentation, runner implementation, and upstream continuation test were inspected. The real 2.0.29 server and native TUI now pass [local verification with a synthetic provider](docs/validation/tui-2.0.29-2026-10-16.md); real-provider compaction and Superpowers validation remain unverified. OpenCode 2.1.2 also passes [local server/TUI validation](docs/validation/context-chart-2.1.2-2026-10-22.md). See [compatibility](docs/compatibility.md).
6
6
 
7
7
  ## Install
8
8
 
9
9
  The intended npm distribution is **`@cesarus777/opencode-cgp`**. It becomes available after this repository's first publication. The unscoped `opencode-context-guard` name belongs to another project; do not install it expecting this plugin.
10
10
 
11
- Minimum supported host: **OpenCode V2 2.0.29**. Only 2.0.29 has been tested; V1 is unsupported.
11
+ Minimum supported host: **OpenCode V2 2.0.29**. Versions 2.0.29 and 2.1.2 have been tested locally with a synthetic provider; V1 is unsupported.
12
12
 
13
13
  After this project's first publication, register the global package through the native manager:
14
14
 
@@ -41,7 +41,7 @@ Global configuration is `~/.config/opencode/opencode.jsonc` (or the OpenCode con
41
41
 
42
42
  Restart/reload OpenCode and look for `context-guard: observe; diagnostics ...`. Observe mode records policy decisions and never requests compaction; OpenCode's own automatic compaction remains active. Inspect diagnostics first, then set `mode` to `enforce` to admit native compaction at runtime step boundaries. The complete [example](examples/opencode.jsonc) includes model, role, and agent policies. **Last matching rule wins**; selectors within a rule are ANDed.
43
43
 
44
- Pin the exact published version in the existing entry's `package` field or use the manager. Version `0.2.3` restores context usage for existing sessions when the plugin is enabled and keeps stale occupancy visible.
44
+ Pin the exact published version in the existing entry's `package` field or use the manager. Version `0.2.5` keeps the context chart continuous through pauses, retains its history between sidebar and panel, and shows confirmed compaction drops. It also adds local OpenCode 2.1.2 validation.
45
45
 
46
46
  ```sh
47
47
  opencode plugin add @cesarus777/opencode-cgp@0.2.1
@@ -96,7 +96,7 @@ A provider measurement refers to its **completed request**, not the next assembl
96
96
 
97
97
  ## Native TUI
98
98
 
99
- Open `/context-guard` or **Open Context Guard** in the command palette for current prompt occupancy, effective input capacity, trigger/headroom, measurement freshness, compaction phase and nested agent status. A native sidebar section appears where the host mounts its sidebar; the panel also works on child routes and narrow terminals. The main session includes a live context chart scaled to its full effective input capacity, with a horizontal compaction threshold and recent usage samples. Key values stay visible as text. Estimates, unknown values, stale measurements and uncertain compaction remain explicit.
99
+ Open `/context-guard` or **Open Context Guard** in the command palette for current prompt occupancy, effective input capacity, trigger/headroom, measurement freshness, compaction phase and nested agent status. A native sidebar section appears where the host mounts its sidebar; the panel also works on child routes and narrow terminals. The main session includes a live context chart scaled to its full effective input capacity, with a horizontal compaction threshold and recent usage samples. Pauses and temporarily lower values retain the previous chart level in gray with a dashed top; a confirmed compaction followed by reported usage permits a visible drop within the same trace. Key values stay visible as text. Estimates, unknown values, stale measurements and uncertain compaction remain explicit.
100
100
 
101
101
  The TUI reads bounded snapshots over public OpenCode RPC, including when telemetry is disabled. Remote clients also need a client-side package registration in `cli.json`; see [the example](examples/cli.jsonc) and [controls, installation and bounds](docs/configuration.md#native-context-guard-view). It targets the inspected 2.0.29 API. The actual local host passes wide root, child and narrow grandchild panel checks. Use **Context Guard: open panel** above a child composer, where the host hides its usual prompt and palette shortcut. tmux, Neovim and cross-machine remote validation remain open.
102
102
 
package/dist/tui.js CHANGED
@@ -427,6 +427,131 @@ function executionStatus(known, nativeRunning, evidence) {
427
427
  return known ? "idle" : "unknown";
428
428
  }
429
429
 
430
+ // src/tui/contextchart.ts
431
+ function updateContextTrace(previous, row2) {
432
+ const snapshot = row2?.snapshot;
433
+ const sessionId = row2?.node.sessionId ?? "";
434
+ const sameSession = previous?.sessionId === sessionId;
435
+ const model = snapshot?.model ?? row2?.node.model ?? (sameSession ? previous?.model : undefined);
436
+ const capacity = snapshot?.capacityTokens ?? (sameSession && model === previous?.model ? previous?.capacity : undefined);
437
+ const compatible = sameSession && previous?.model === model && previous.capacity === capacity;
438
+ const samples = compatible ? previous.samples : [];
439
+ const completed = snapshot?.compaction.lastCompleted ?? (compatible ? previous.lastCompleted : undefined);
440
+ let pending = compatible ? previous.pendingCompleted : undefined;
441
+ if (completed !== undefined && completed !== previous?.lastCompleted)
442
+ pending = completed;
443
+ const measurement2 = snapshot?.measurement;
444
+ const valid2 = (row2?.freshness === "fresh" || row2?.freshness === "stale") && measurement2 && Number.isFinite(measurement2.tokens) && measurement2.tokens >= 0;
445
+ const at = measurement2?.at ?? snapshot?.observedAt;
446
+ let next = samples;
447
+ if (at !== undefined && Number.isFinite(at)) {
448
+ const last = samples.at(-1);
449
+ if (!last || at >= last.at) {
450
+ const reset = valid2 && (at === last?.at && last.compacted === true || pending !== undefined && measurement2.at >= pending && measurement2.confidence === "reported" && measurement2.scope === "completed-request" && snapshot?.compaction.phase !== "running");
451
+ const waiting = pending !== undefined && !reset;
452
+ const tokens = valid2 && !waiting ? Math.max(reset ? 0 : last?.tokens ?? 0, measurement2.tokens) : last?.tokens;
453
+ const held = tokens !== undefined && (!valid2 || waiting || tokens !== measurement2.tokens);
454
+ const sample = {
455
+ at,
456
+ ...tokens === undefined ? {} : { tokens },
457
+ ...held ? { held: true } : {},
458
+ ...reset || at === last?.at && last.compacted ? { compacted: true } : {}
459
+ };
460
+ if (reset)
461
+ pending = undefined;
462
+ if (!last || at > last.at) {
463
+ const bridge = last?.tokens !== undefined && at - last.at > 2500 ? [{ at: at - 1, tokens: last.tokens, held: true }] : [];
464
+ next = [...samples, ...bridge, sample].slice(-96);
465
+ } else if (sample.tokens !== last.tokens || sample.held !== last.held || sample.compacted !== last.compacted) {
466
+ next = [...samples.slice(0, -1), sample];
467
+ }
468
+ }
469
+ }
470
+ return {
471
+ sessionId,
472
+ model,
473
+ capacity,
474
+ samples: next,
475
+ ...completed === undefined ? {} : { lastCompleted: completed },
476
+ ...pending === undefined ? {} : { pendingCompleted: pending }
477
+ };
478
+ }
479
+
480
+ class ContextTraceStore {
481
+ traces = new Map;
482
+ update(row2) {
483
+ if (!row2)
484
+ return updateContextTrace(undefined, undefined);
485
+ const id = row2.node.sessionId;
486
+ const trace = updateContextTrace(this.traces.get(id), row2);
487
+ this.traces.delete(id);
488
+ this.traces.set(id, trace);
489
+ if (this.traces.size > 128) {
490
+ const oldest = this.traces.keys().next().value;
491
+ if (oldest !== undefined)
492
+ this.traces.delete(oldest);
493
+ }
494
+ return trace;
495
+ }
496
+ clear() {
497
+ this.traces.clear();
498
+ }
499
+ }
500
+ function contextChart(samples, capacity, trigger, width, now) {
501
+ if (capacity === undefined || !Number.isFinite(capacity) || capacity <= 0 || width < 4)
502
+ return [];
503
+ const height = 8;
504
+ const gutter = width >= 16 ? 4 : 0;
505
+ const size = Math.max(1, Math.min(96, width - gutter - 2));
506
+ const last = samples.at(-1);
507
+ const tail = now !== undefined && last?.tokens !== undefined && now - last.at > 2500 ? [{ at: now, tokens: last.tokens, held: true }] : [];
508
+ const values = [...samples, ...tail].slice(-size);
509
+ const offset = size - values.length;
510
+ const thresholdRow = trigger !== undefined && Number.isFinite(trigger) && trigger >= 0 && trigger <= capacity ? Math.round((1 - trigger / capacity) * (height - 1)) : -1;
511
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
512
+ const lines = Array.from({ length: height }, (_, row2) => {
513
+ const threshold = row2 === thresholdRow;
514
+ const axis = gutter ? (row2 === 0 ? "100%" : row2 === height - 1 ? "0%" : "").padStart(gutter) : "";
515
+ const plot = Array.from({ length: size }, (_2, col) => {
516
+ const sample = values[col - offset];
517
+ const value = sample?.tokens;
518
+ const fill = value === undefined ? 0 : Math.max(0, Math.min(8, Math.ceil((Math.min(value / capacity, 1) * height - (height - row2 - 1)) * 8)));
519
+ if (sample?.held && value !== undefined) {
520
+ const top = Math.min(height - 1, Math.max(0, height - Math.ceil(Math.min(value / capacity, 1) * height)));
521
+ if (row2 === top)
522
+ return "\u2508";
523
+ if (fill > 0)
524
+ return "\u2591";
525
+ }
526
+ if (threshold)
527
+ return fill > 0 ? "\u2542" : "\u2504";
528
+ if (value === 0 && row2 === height - 1)
529
+ return "\u2581";
530
+ return blocks[fill] ?? " ";
531
+ });
532
+ const line = `${axis}\u2502${plot.join("")}\u2502`;
533
+ if (!values.some((sample) => sample.held))
534
+ return { line, threshold };
535
+ const segments = [{ text: `${axis}\u2502`, held: false }];
536
+ for (let col = 0;col < plot.length; col++) {
537
+ const text2 = plot[col] ?? " ";
538
+ const held = values[col - offset]?.held === true;
539
+ const previous = segments.at(-1);
540
+ if (previous?.held === held)
541
+ segments[segments.length - 1] = { text: previous.text + text2, held };
542
+ else
543
+ segments.push({ text: text2, held });
544
+ }
545
+ segments.push({ text: "\u2502", held: false });
546
+ return { line, threshold, segments };
547
+ });
548
+ lines.push({
549
+ line: `${" ".repeat(gutter)}\u2514${"\u2500".repeat(size)}\u2518`,
550
+ threshold: false
551
+ });
552
+ return lines;
553
+ }
554
+
430
555
  // src/tui/transport.ts
431
556
  class SnapshotClient {
432
557
  transport;
@@ -804,6 +929,7 @@ async function discoverFamily(client, currentId, signal, cached, maxNodes = 1024
804
929
 
805
930
  // src/tui/state.ts
806
931
  function createGuardState(context) {
932
+ const contextTraces = new ContextTraceStore;
807
933
  const [version, setVersion] = createSignal(0);
808
934
  const notify = () => {
809
935
  setVersion((n) => n + 1);
@@ -1050,7 +1176,11 @@ function createGuardState(context) {
1050
1176
  return result;
1051
1177
  },
1052
1178
  rows,
1053
- dispose: stop,
1179
+ contextTrace: (row2) => contextTraces.update(row2),
1180
+ dispose() {
1181
+ stop();
1182
+ contextTraces.clear();
1183
+ },
1054
1184
  age(id) {
1055
1185
  version();
1056
1186
  const c = [...clients.values()].find((c2) => c2.rows.has(id));
@@ -1225,61 +1355,6 @@ function performanceLines(row2, width) {
1225
1355
  ];
1226
1356
  }
1227
1357
 
1228
- // src/tui/contextchart.ts
1229
- function updateContextTrace(previous, row2) {
1230
- const snapshot = row2?.snapshot;
1231
- const model = snapshot?.model ?? row2?.node.model;
1232
- const capacity = snapshot?.capacityTokens;
1233
- const sessionId = row2?.node.sessionId ?? "";
1234
- const samples = previous?.sessionId === sessionId && previous.model === model && previous.capacity === capacity ? previous.samples : [];
1235
- const measurement2 = snapshot?.measurement;
1236
- const valid2 = (row2?.freshness === "fresh" || row2?.freshness === "stale") && measurement2 && Number.isFinite(measurement2.tokens) && measurement2.tokens >= 0;
1237
- const at = measurement2?.at ?? snapshot?.observedAt;
1238
- let next = samples;
1239
- if (at !== undefined && Number.isFinite(at)) {
1240
- const sample = {
1241
- at,
1242
- ...valid2 ? { tokens: measurement2.tokens } : {}
1243
- };
1244
- const last = samples.at(-1);
1245
- if (!last || at > last.at)
1246
- next = [...samples, sample].slice(-96);
1247
- else if (at === last.at && sample.tokens !== last.tokens)
1248
- next = [...samples.slice(0, -1), sample];
1249
- }
1250
- return { sessionId, model, capacity, samples: next };
1251
- }
1252
- function contextChart(samples, capacity, trigger, width) {
1253
- if (capacity === undefined || !Number.isFinite(capacity) || capacity <= 0 || width < 4)
1254
- return [];
1255
- const height = 8;
1256
- const gutter = width >= 16 ? 4 : 0;
1257
- const size = Math.max(1, Math.min(96, width - gutter - 2));
1258
- const values = samples.slice(-size);
1259
- const offset = size - values.length;
1260
- const thresholdRow = trigger !== undefined && Number.isFinite(trigger) && trigger >= 0 && trigger <= capacity ? Math.round((1 - trigger / capacity) * (height - 1)) : -1;
1261
- const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1262
- const lines = Array.from({ length: height }, (_, row2) => {
1263
- const threshold = row2 === thresholdRow;
1264
- const axis = gutter ? (row2 === 0 ? "100%" : row2 === height - 1 ? "0%" : "").padStart(gutter) : "";
1265
- const plot = Array.from({ length: size }, (_2, col) => {
1266
- const value = values[col - offset]?.tokens;
1267
- const fill = value === undefined ? 0 : Math.max(0, Math.min(8, Math.ceil((Math.min(value / capacity, 1) * height - (height - row2 - 1)) * 8)));
1268
- if (threshold)
1269
- return fill > 0 ? "\u2542" : "\u2504";
1270
- if (value === 0 && row2 === height - 1)
1271
- return "\u2581";
1272
- return blocks[fill] ?? " ";
1273
- }).join("");
1274
- return { line: `${axis}\u2502${plot}\u2502`, threshold };
1275
- });
1276
- lines.push({
1277
- line: `${" ".repeat(gutter)}\u2514${"\u2500".repeat(size)}\u2518`,
1278
- threshold: false
1279
- });
1280
- return lines;
1281
- }
1282
-
1283
1358
  // src/tui/compact.tsx
1284
1359
  import { createTextNode as _$createTextNode } from "@opentui/solid";
1285
1360
  import { insertNode as _$insertNode } from "@opentui/solid";
@@ -1314,7 +1389,7 @@ function progress(ratio, width) {
1314
1389
  const filled = valid2 ? Math.round(Math.min(1, ratio) * size) : 0;
1315
1390
  return clip("\u2588".repeat(filled) + "\u2591".repeat(size - filled) + " " + label, width);
1316
1391
  }
1317
- function mainSections(row2, width, age, samples) {
1392
+ function mainSections(row2, width, age, samples, chartCapacity) {
1318
1393
  if (!row2)
1319
1394
  return [{ tone: "base", lines: wrap("Loading session family...", width) }];
1320
1395
  const s = row2.snapshot, m = s?.measurement;
@@ -1335,10 +1410,17 @@ function mainSections(row2, width, age, samples) {
1335
1410
  ...columns("Context", `${tokens(measured)} / ${tokens(s?.capacityTokens)}`, width)
1336
1411
  ]
1337
1412
  },
1338
- ...contextChart(samples ?? (m && valid2 ? [{ at: m.at, tokens: m.tokens }] : []), s?.capacityTokens, s?.policy.triggerTokens, width).map(({ line, threshold }) => ({
1413
+ ...contextChart(samples ?? (m && valid2 ? [{ at: m.at, tokens: m.tokens }] : []), s?.capacityTokens ?? chartCapacity, s?.policy.triggerTokens, width, m ? m.at + age : s?.observedAt === undefined ? undefined : s.observedAt + age).map(({ line, threshold, segments }) => ({
1339
1414
  tone: threshold ? "threshold" : "pressure",
1340
- lines: [line]
1415
+ lines: [line],
1416
+ ...segments ? { segments } : {}
1341
1417
  })),
1418
+ ...samples?.some((sample) => sample.held) || m && age > 2500 ? [
1419
+ {
1420
+ tone: "measurement",
1421
+ lines: wrap("\u2508 held \xB7 last known context", width)
1422
+ }
1423
+ ] : [],
1342
1424
  {
1343
1425
  tone: "base",
1344
1426
  lines: wrap(`${ratio(s?.capacityTokens) === undefined ? "?" : Math.round((ratio(s?.capacityTokens) ?? 0) * 100)}% used \xB7 recent samples \u2192 now`, width)
@@ -1472,7 +1554,7 @@ function CompactSidebar(props) {
1472
1554
  setCollapsed(new Set);
1473
1555
  });
1474
1556
  const current = createMemo(() => props.rows.find((r) => r.node.sessionId === props.sessionID));
1475
- const trace = createMemo((previous) => updateContextTrace(previous, current()));
1557
+ const trace = createMemo(() => props.state.contextTrace(current()));
1476
1558
  const tree = createMemo(() => subagentTree(props.rows, props.sessionID, collapsed(), hideCompleted(), byPressure()));
1477
1559
  const entries = createMemo(() => new Map(tree().visible.map((entry) => [entry.row.node.sessionId, entry])));
1478
1560
  const toggle = (id) => setCollapsed((previous) => {
@@ -1538,7 +1620,7 @@ function CompactSidebar(props) {
1538
1620
  const head = cells(`\u2500\u2500 ${title} `) > width() ? title : `\u2500\u2500 ${title} `;
1539
1621
  return clip(head, width()) + "\u2500".repeat(Math.max(0, width() - cells(head)));
1540
1622
  };
1541
- const notices = createMemo(() => [...props.context.app.version !== "2.0.29" ? wrap(`Runtime ${props.context.app.version}: unverified`, width()) : [], ...props.state.connection() !== "live" ? wrap(`Connection: ${props.state.connection()}`, width()) : [], ...props.state.health()?.mode === "observe" ? ["Observe mode"] : [], ...props.state.health()?.enforcement === "disabled" || props.state.health()?.enforcement === "suspended" ? wrap(`Guard: ${props.state.health()?.enforcement}`, width()) : [], ...(props.state.health()?.reasons ?? []).flatMap((reason) => wrap(reason, width()))]);
1623
+ const notices = createMemo(() => [...!["2.0.29", "2.1.2"].includes(props.context.app.version) ? wrap(`Runtime ${props.context.app.version}: unverified`, width()) : [], ...props.state.connection() !== "live" ? wrap(`Connection: ${props.state.connection()}`, width()) : [], ...props.state.health()?.mode === "observe" ? ["Observe mode"] : [], ...props.state.health()?.enforcement === "disabled" || props.state.health()?.enforcement === "suspended" ? wrap(`Guard: ${props.state.health()?.enforcement}`, width()) : [], ...(props.state.health()?.reasons ?? []).flatMap((reason) => wrap(reason, width()))]);
1542
1624
  return (() => {
1543
1625
  var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createElement("text"), _el$0 = _$createElement("text"), _el$1 = _$createElement("text"), _el$11 = _$createElement("text"), _el$12 = _$createElement("text");
1544
1626
  _$insertNode(_el$2, _el$3);
@@ -1562,7 +1644,7 @@ function CompactSidebar(props) {
1562
1644
  });
1563
1645
  _$insert(_el$2, _$createComponent(Index, {
1564
1646
  get each() {
1565
- return mainSections(current(), width(), props.state.age(props.sessionID), trace().samples);
1647
+ return mainSections(current(), width(), props.state.age(props.sessionID), trace().samples, trace().capacity);
1566
1648
  },
1567
1649
  children: (section2) => _$createComponent(Index, {
1568
1650
  get each() {
@@ -1570,7 +1652,29 @@ function CompactSidebar(props) {
1570
1652
  },
1571
1653
  children: (line) => (() => {
1572
1654
  var _el$13 = _$createElement("text");
1573
- _$insert(_el$13, () => line() || " ");
1655
+ _$insert(_el$13, _$createComponent(Show, {
1656
+ get when() {
1657
+ return section2().segments;
1658
+ },
1659
+ get fallback() {
1660
+ return line() || " ";
1661
+ },
1662
+ get children() {
1663
+ return _$createComponent(For, {
1664
+ get each() {
1665
+ return section2().segments;
1666
+ },
1667
+ children: (part) => (() => {
1668
+ var _el$14 = _$createElement("span");
1669
+ _$insert(_el$14, () => part.text);
1670
+ _$effect((_$p) => _$setProp(_el$14, "style", {
1671
+ fg: part.held ? colors().muted : section2().tone === "threshold" ? colors().feedback.warning.base : pressureColor(current())
1672
+ }, _$p));
1673
+ return _el$14;
1674
+ })()
1675
+ });
1676
+ }
1677
+ }));
1574
1678
  _$effect((_$p) => _$setProp(_el$13, "fg", section2().tone === "execution" ? executionColor(current()) : section2().tone === "threshold" ? colors().feedback.warning.base : section2().tone === "pressure" ? pressureColor(current()) : section2().tone === "measurement" ? current()?.freshness === "fresh" ? colors().muted : colors().feedback.warning.base : colors().base, _$p));
1575
1679
  return _el$13;
1576
1680
  })()
@@ -1581,10 +1685,10 @@ function CompactSidebar(props) {
1581
1685
  return notices();
1582
1686
  },
1583
1687
  children: (line) => (() => {
1584
- var _el$14 = _$createElement("text");
1585
- _$insert(_el$14, line);
1586
- _$effect((_$p) => _$setProp(_el$14, "fg", colors().feedback.warning.base, _$p));
1587
- return _el$14;
1688
+ var _el$15 = _$createElement("text");
1689
+ _$insert(_el$15, line);
1690
+ _$effect((_$p) => _$setProp(_el$15, "fg", colors().feedback.warning.base, _$p));
1691
+ return _el$15;
1588
1692
  })()
1589
1693
  }), _el$3);
1590
1694
  _$insertNode(_el$3, _$createTextNode(` `));
@@ -1610,60 +1714,60 @@ function CompactSidebar(props) {
1610
1714
  return row2();
1611
1715
  },
1612
1716
  children: (item) => (() => {
1613
- var _el$15 = _$createElement("box"), _el$16 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("span"), _el$20 = _$createElement("span"), _el$21 = _$createElement("text");
1614
- _$insertNode(_el$15, _el$16);
1615
- _$insertNode(_el$15, _el$21);
1616
- _$setProp(_el$15, "flexDirection", "column");
1617
- _$setProp(_el$15, "flexShrink", 0);
1618
- _$insertNode(_el$16, _el$18);
1619
- _$setProp(_el$16, "flexDirection", "row");
1717
+ var _el$16 = _$createElement("box"), _el$17 = _$createElement("box"), _el$19 = _$createElement("text"), _el$20 = _$createElement("span"), _el$21 = _$createElement("span"), _el$22 = _$createElement("text");
1718
+ _$insertNode(_el$16, _el$17);
1719
+ _$insertNode(_el$16, _el$22);
1720
+ _$setProp(_el$16, "flexDirection", "column");
1620
1721
  _$setProp(_el$16, "flexShrink", 0);
1621
- _$insert(_el$16, _$createComponent(Show, {
1722
+ _$insertNode(_el$17, _el$19);
1723
+ _$setProp(_el$17, "flexDirection", "row");
1724
+ _$setProp(_el$17, "flexShrink", 0);
1725
+ _$insert(_el$17, _$createComponent(Show, {
1622
1726
  get when() {
1623
1727
  return prefix();
1624
1728
  },
1625
1729
  get children() {
1626
- var _el$17 = _$createElement("text");
1627
- _$setProp(_el$17, "onMouseDown", () => toggle(id));
1628
- _$insert(_el$17, prefix);
1629
- _$effect((_$p) => _$setProp(_el$17, "fg", colors().muted, _$p));
1630
- return _el$17;
1730
+ var _el$18 = _$createElement("text");
1731
+ _$setProp(_el$18, "onMouseDown", () => toggle(id));
1732
+ _$insert(_el$18, prefix);
1733
+ _$effect((_$p) => _$setProp(_el$18, "fg", colors().muted, _$p));
1734
+ return _el$18;
1631
1735
  }
1632
- }), _el$18);
1633
- _$insertNode(_el$18, _el$19);
1634
- _$insertNode(_el$18, _el$20);
1635
- _$setProp(_el$18, "onMouseDown", () => props.context.ui.router.navigate({
1736
+ }), _el$19);
1737
+ _$insertNode(_el$19, _el$20);
1738
+ _$insertNode(_el$19, _el$21);
1739
+ _$setProp(_el$19, "onMouseDown", () => props.context.ui.router.navigate({
1636
1740
  type: "session",
1637
1741
  sessionID: id
1638
1742
  }));
1639
- _$insert(_el$19, () => agentHeading(item(), nameWidth()).slice(0, 1));
1640
- _$insert(_el$20, () => agentHeading(item(), nameWidth()).slice(1));
1641
- _$insert(_el$15, _$createComponent(Index, {
1743
+ _$insert(_el$20, () => agentHeading(item(), nameWidth()).slice(0, 1));
1744
+ _$insert(_el$21, () => agentHeading(item(), nameWidth()).slice(1));
1745
+ _$insert(_el$16, _$createComponent(Index, {
1642
1746
  get each() {
1643
1747
  return agentLines(item(), Math.max(1, width() - cells(indent()) - 2));
1644
1748
  },
1645
1749
  children: (line) => (() => {
1646
- var _el$23 = _$createElement("text"), _el$24 = _$createTextNode(` `);
1647
- _$insertNode(_el$23, _el$24);
1648
- _$insert(_el$23, indent, _el$24);
1649
- _$insert(_el$23, line, null);
1650
- _$effect((_$p) => _$setProp(_el$23, "fg", rowColor(item()), _$p));
1651
- return _el$23;
1750
+ var _el$24 = _$createElement("text"), _el$25 = _$createTextNode(` `);
1751
+ _$insertNode(_el$24, _el$25);
1752
+ _$insert(_el$24, indent, _el$25);
1753
+ _$insert(_el$24, line, null);
1754
+ _$effect((_$p) => _$setProp(_el$24, "fg", rowColor(item()), _$p));
1755
+ return _el$24;
1652
1756
  })()
1653
- }), _el$21);
1654
- _$insertNode(_el$21, _$createTextNode(` `));
1757
+ }), _el$22);
1758
+ _$insertNode(_el$22, _$createTextNode(` `));
1655
1759
  _$effect((_p$) => {
1656
1760
  var _v$0 = rowColor(item()), _v$1 = {
1657
1761
  fg: executionColor(item())
1658
1762
  };
1659
- _v$0 !== _p$.e && (_p$.e = _$setProp(_el$18, "fg", _v$0, _p$.e));
1660
- _v$1 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$1, _p$.t));
1763
+ _v$0 !== _p$.e && (_p$.e = _$setProp(_el$19, "fg", _v$0, _p$.e));
1764
+ _v$1 !== _p$.t && (_p$.t = _$setProp(_el$20, "style", _v$1, _p$.t));
1661
1765
  return _p$;
1662
1766
  }, {
1663
1767
  e: undefined,
1664
1768
  t: undefined
1665
1769
  });
1666
- return _el$15;
1770
+ return _el$16;
1667
1771
  })()
1668
1772
  });
1669
1773
  }
@@ -1717,8 +1821,8 @@ function GuardView(props) {
1717
1821
  onCleanup(() => consumer.release());
1718
1822
  const rows = createMemo2(() => props.family ?? props.state.rows([], !props.expanded));
1719
1823
  const current = createMemo2(() => rows().find((row2) => row2.node.sessionId === props.sessionID));
1720
- const trace = createMemo2((previous) => updateContextTrace(previous, props.expanded ? current() : undefined));
1721
- const chart = createMemo2(() => contextChart(trace().samples, current()?.snapshot?.capacityTokens, current()?.snapshot?.policy.triggerTokens, props.width));
1824
+ const trace = createMemo2(() => props.state.contextTrace(props.expanded ? current() : undefined));
1825
+ const chart = createMemo2(() => contextChart(trace().samples, current()?.snapshot?.capacityTokens ?? trace().capacity, current()?.snapshot?.policy.triggerTokens, props.width, current()?.snapshot?.measurement ? (current()?.snapshot?.measurement?.at ?? 0) + props.state.age(props.sessionID) : undefined));
1722
1826
  const detail = createMemo2(() => {
1723
1827
  const lines = detailLines(current(), props.width, props.expanded, props.state.age(props.sessionID));
1724
1828
  if (props.expanded) {
@@ -1780,8 +1884,8 @@ function GuardView(props) {
1780
1884
  _$insertNode2(_el$2, _el$3);
1781
1885
  _$insert2(_el$2, () => props.state.health()?.mode ?? "?", null);
1782
1886
  _$insert2(_el$4, (() => {
1783
- var _c$ = _$memo2(() => props.context.app.version === "2.0.29");
1784
- return () => _c$() ? "" : clip(`Runtime ${props.context.app.version}: unverified (2.0.29 target)`, props.width);
1887
+ var _c$ = _$memo2(() => !!["2.0.29", "2.1.2"].includes(props.context.app.version));
1888
+ return () => _c$() ? "" : clip(`Runtime ${props.context.app.version}: unverified (tested: 2.0.29, 2.1.2)`, props.width);
1785
1889
  })());
1786
1890
  _$insert2(_el$5, () => clip(`${props.state.connection()} / ${props.state.health()?.enforcement ?? "?"}`, props.width));
1787
1891
  _$insert2(_el$, _$createComponent2(For2, {
@@ -1790,20 +1894,42 @@ function GuardView(props) {
1790
1894
  },
1791
1895
  children: (item) => (() => {
1792
1896
  var _el$1 = _$createElement2("text");
1793
- _$insert2(_el$1, () => item.line);
1897
+ _$insert2(_el$1, _$createComponent2(Show2, {
1898
+ get when() {
1899
+ return item.segments;
1900
+ },
1901
+ get fallback() {
1902
+ return item.line;
1903
+ },
1904
+ get children() {
1905
+ return _$createComponent2(For2, {
1906
+ get each() {
1907
+ return item.segments;
1908
+ },
1909
+ children: (part) => (() => {
1910
+ var _el$10 = _$createElement2("span");
1911
+ _$insert2(_el$10, () => part.text);
1912
+ _$effect2((_$p) => _$setProp2(_el$10, "style", {
1913
+ fg: part.held ? props.context.theme.text.muted : item.threshold ? props.context.theme.text.feedback.warning.base : props.context.theme.text.base
1914
+ }, _$p));
1915
+ return _el$10;
1916
+ })()
1917
+ });
1918
+ }
1919
+ }));
1794
1920
  _$effect2((_$p) => _$setProp2(_el$1, "fg", item.threshold ? props.context.theme.text.feedback.warning.base : props.context.theme.text.muted, _$p));
1795
1921
  return _el$1;
1796
1922
  })()
1797
1923
  }), _el$6);
1798
1924
  _$insert2(_el$, _$createComponent2(For2, {
1799
1925
  get each() {
1800
- return wrap("Recent context samples \u2192 now \xB7 \u2504 trigger", props.width);
1926
+ return wrap("Recent context samples \u2192 now \xB7 \u2504 trigger \xB7 \u2508 held", props.width);
1801
1927
  },
1802
1928
  children: (line) => (() => {
1803
- var _el$10 = _$createElement2("text");
1804
- _$insert2(_el$10, line);
1805
- _$effect2((_$p) => _$setProp2(_el$10, "fg", props.context.theme.text.muted, _$p));
1806
- return _el$10;
1929
+ var _el$11 = _$createElement2("text");
1930
+ _$insert2(_el$11, line);
1931
+ _$effect2((_$p) => _$setProp2(_el$11, "fg", props.context.theme.text.muted, _$p));
1932
+ return _el$11;
1807
1933
  })()
1808
1934
  }), _el$6);
1809
1935
  _$insert2(_el$, _$createComponent2(For2, {
@@ -1811,10 +1937,10 @@ function GuardView(props) {
1811
1937
  return detail();
1812
1938
  },
1813
1939
  children: (line) => (() => {
1814
- var _el$11 = _$createElement2("text");
1815
- _$insert2(_el$11, line);
1816
- _$effect2((_$p) => _$setProp2(_el$11, "fg", props.context.theme.text.base, _$p));
1817
- return _el$11;
1940
+ var _el$12 = _$createElement2("text");
1941
+ _$insert2(_el$12, line);
1942
+ _$effect2((_$p) => _$setProp2(_el$12, "fg", props.context.theme.text.base, _$p));
1943
+ return _el$12;
1818
1944
  })()
1819
1945
  }), _el$6);
1820
1946
  _$insert2(_el$, _$createComponent2(For2, {
@@ -1822,10 +1948,10 @@ function GuardView(props) {
1822
1948
  return props.state.health()?.reasons ?? [];
1823
1949
  },
1824
1950
  children: (reason) => (() => {
1825
- var _el$12 = _$createElement2("text");
1826
- _$insert2(_el$12, () => clip(reason, props.width));
1827
- _$effect2((_$p) => _$setProp2(_el$12, "fg", props.context.theme.text.feedback.warning.base, _$p));
1828
- return _el$12;
1951
+ var _el$13 = _$createElement2("text");
1952
+ _$insert2(_el$13, () => clip(reason, props.width));
1953
+ _$effect2((_$p) => _$setProp2(_el$13, "fg", props.context.theme.text.feedback.warning.base, _$p));
1954
+ return _el$13;
1829
1955
  })()
1830
1956
  }), _el$6);
1831
1957
  _$insert2(_el$6, (() => {
@@ -1841,9 +1967,9 @@ function GuardView(props) {
1841
1967
  return visible();
1842
1968
  },
1843
1969
  children: (row2) => (() => {
1844
- var _el$13 = _$createElement2("box");
1845
- _$setProp2(_el$13, "flexDirection", "column");
1846
- _$setProp2(_el$13, "onMouseDown", () => {
1970
+ var _el$14 = _$createElement2("box");
1971
+ _$setProp2(_el$14, "flexDirection", "column");
1972
+ _$setProp2(_el$14, "onMouseDown", () => {
1847
1973
  if (row2.hidden)
1848
1974
  props.state.expand(row2.node.sessionId);
1849
1975
  else {
@@ -1855,19 +1981,19 @@ function GuardView(props) {
1855
1981
  });
1856
1982
  }
1857
1983
  });
1858
- _$insert2(_el$13, _$createComponent2(For2, {
1984
+ _$insert2(_el$14, _$createComponent2(For2, {
1859
1985
  get each() {
1860
1986
  return rowLines(row2, Math.max(1, props.width - (props.expanded ? 2 : 0))).map((line) => props.expanded ? `${row2.node.sessionId === props.selectedId ? ">" : " "} ${line}` : line);
1861
1987
  },
1862
1988
  children: (line) => (() => {
1863
- var _el$14 = _$createElement2("text");
1864
- _$insert2(_el$14, line);
1865
- _$effect2((_$p) => _$setProp2(_el$14, "fg", row2.pressure === "crossed" ? props.context.theme.text.feedback.error.base : row2.pressure === "near" ? props.context.theme.text.feedback.warning.base : props.context.theme.text.base, _$p));
1866
- return _el$14;
1989
+ var _el$15 = _$createElement2("text");
1990
+ _$insert2(_el$15, line);
1991
+ _$effect2((_$p) => _$setProp2(_el$15, "fg", row2.pressure === "crossed" ? props.context.theme.text.feedback.error.base : row2.pressure === "near" ? props.context.theme.text.feedback.warning.base : props.context.theme.text.base, _$p));
1992
+ return _el$15;
1867
1993
  })()
1868
1994
  }));
1869
- _$effect2((_$p) => _$setProp2(_el$13, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1870
- return _el$13;
1995
+ _$effect2((_$p) => _$setProp2(_el$14, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1996
+ return _el$14;
1871
1997
  })()
1872
1998
  }), _el$0);
1873
1999
  _$setProp2(_el$0, "onMouseDown", () => props.context.ui.panel.open("context-guard.monitor"));
@@ -2120,7 +2246,8 @@ function HistoryCharts(props) {
2120
2246
  function GuardPanel(props) {
2121
2247
  const [showCharts, setShowCharts] = createSignal4(false);
2122
2248
  const [metricIndex, setMetricIndex] = createSignal4(0);
2123
- const width = () => Math.max(4, props.input.width - 4);
2249
+ const [viewportWidth, setViewportWidth] = createSignal4();
2250
+ const width = () => Math.max(4, Math.min(props.input.width - 4, viewportWidth() ?? Infinity));
2124
2251
  const windows = [300000, 3600000, 86400000, 7 * 86400000];
2125
2252
  const [windowIndex, setWindowIndex] = createSignal4(1);
2126
2253
  const [chartRefresh, setChartRefresh] = createSignal4(0);
@@ -2276,6 +2403,11 @@ function GuardPanel(props) {
2276
2403
  _$use((value) => {
2277
2404
  scroll = value;
2278
2405
  }, _el$2);
2406
+ _$setProp4(_el$2, "viewportOptions", {
2407
+ onSizeChange: function() {
2408
+ setViewportWidth(this.width);
2409
+ }
2410
+ });
2279
2411
  _$setProp4(_el$2, "flexGrow", 1);
2280
2412
  _$setProp4(_el$2, "minHeight", 0);
2281
2413
  _$insertNode4(_el$3, _el$4);
@@ -1,4 +1,4 @@
1
- import { type ContextSample } from "./contextchart.js";
1
+ import { type ContextSample, type ContextChartSegment } from "./contextchart.js";
2
2
  import type { GuardRow } from "../family.js";
3
3
  export declare function tokens(value: number | undefined): string;
4
4
  export declare function columns(left: string, right: string, width: number): string[];
@@ -6,8 +6,9 @@ export declare function progress(ratio: number | undefined, width: number): stri
6
6
  export interface MainSection {
7
7
  readonly tone: "base" | "execution" | "pressure" | "measurement" | "threshold";
8
8
  readonly lines: readonly string[];
9
+ readonly segments?: readonly ContextChartSegment[];
9
10
  }
10
- export declare function mainSections(row: GuardRow | undefined, width: number, age: number, samples?: readonly ContextSample[]): MainSection[];
11
+ export declare function mainSections(row: GuardRow | undefined, width: number, age: number, samples?: readonly ContextSample[], chartCapacity?: number): MainSection[];
11
12
  export declare function mainLines(row: GuardRow | undefined, width: number, age: number): string[];
12
13
  export declare const statusSymbol: {
13
14
  readonly running: "●";
@@ -2,18 +2,34 @@ import type { GuardRow } from "../family.js";
2
2
  export interface ContextSample {
3
3
  readonly at: number;
4
4
  readonly tokens?: number;
5
+ /** Display-only carry-forward; never a new measurement. */
6
+ readonly held?: boolean;
7
+ readonly compacted?: boolean;
5
8
  }
6
9
  export interface ContextTrace {
7
10
  readonly sessionId: string;
8
11
  readonly model: string | undefined;
9
12
  readonly capacity: number | undefined;
10
13
  readonly samples: readonly ContextSample[];
14
+ readonly lastCompleted?: number;
15
+ readonly pendingCompleted?: number;
11
16
  }
12
- /** Keep only actual measurements, never copies made by age/render ticks. */
17
+ /** Bounded display history. Raw usage and enforcement remain in the snapshot. */
13
18
  export declare function updateContextTrace(previous: ContextTrace | undefined, row: GuardRow | undefined): ContextTrace;
19
+ /** Share traces across sidebar/panel mounts without persisting provider data. */
20
+ export declare class ContextTraceStore {
21
+ private readonly traces;
22
+ update(row: GuardRow | undefined): ContextTrace;
23
+ clear(): void;
24
+ }
25
+ export interface ContextChartSegment {
26
+ readonly text: string;
27
+ readonly held: boolean;
28
+ }
14
29
  export interface ContextChartLine {
15
30
  readonly line: string;
16
31
  readonly threshold: boolean;
32
+ readonly segments?: readonly ContextChartSegment[];
17
33
  }
18
- /** Fixed capacity scale and observation order; no interpolation across missing samples. */
19
- export declare function contextChart(samples: readonly ContextSample[], capacity: number | undefined, trigger: number | undefined, width: number): ContextChartLine[];
34
+ /** Fixed capacity scale; held levels are dashed and rendered in a muted color. */
35
+ export declare function contextChart(samples: readonly ContextSample[], capacity: number | undefined, trigger: number | undefined, width: number, now?: number): ContextChartLine[];
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui";
2
+ import { type GuardRow } from "../family.js";
2
3
  export declare function createGuardState(context: Plugin.Context): {
3
4
  version: import("solid-js").Accessor<number>;
4
5
  acquire: (id: string) => {
@@ -6,8 +7,9 @@ export declare function createGuardState(context: Plugin.Context): {
6
7
  release(): void;
7
8
  };
8
9
  history(id: string, from: number, to: number, signal: AbortSignal): Promise<import("../historycharts.js").HistoryChartResult>;
9
- rows: (stableOrder?: readonly string[], allDescendants?: boolean) => import("../family.js").GuardRow[];
10
- dispose: () => void;
10
+ rows: (stableOrder?: readonly string[], allDescendants?: boolean) => GuardRow[];
11
+ contextTrace: (row: GuardRow | undefined) => import("./contextchart.js").ContextTrace;
12
+ dispose(): void;
11
13
  age(id: string): number;
12
14
  health(): import("../snapshot.js").MonitorHealth | undefined;
13
15
  connection(): "unavailable" | "loading" | "live" | "reconnecting" | "incompatible";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cesarus777/opencode-cgp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {