@cesarus777/opencode-cgp 0.2.1 → 0.2.2

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
@@ -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.1` includes performance metrics, history charts, and CLI reports.
44
+ Pin the exact published version in the existing entry's `package` field or use the manager. Version `0.2.2` adds live context-capacity charts, single-metric performance chart navigation, panel padding, and persistent close controls.
45
45
 
46
46
  ```sh
47
47
  opencode plugin add @cesarus777/opencode-cgp@0.2.1
@@ -96,13 +96,13 @@ 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. 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. 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
 
103
103
  Per-session live TPS, average model throughput, average session throughput and observed TTFT appear in the selected session and child rows. Live TPS and TTFT use `~` to mark estimates; completed rates use provider totals. Configurable local history defaults to seven days with a 64 MiB total cap, independently of diagnostic telemetry. See [metric definitions and history exports](docs/configuration.md#live-performance-and-retained-history).
104
104
 
105
- Press **c** in the Context Guard panel for retained-history terminal charts; use **[ / ]** to change the time window and **Up / Down** to select an agent instance. Generate a self-contained interactive browser report with `bun run charts /absolute/history-directory --output report.html`. It includes filters, UTC timestamp tooltips, request statistics and SVG export. Installed packages expose the same command as `context-guard charts ...` (Bun required). See [chart controls and limits](docs/configuration.md#performance-charts).
105
+ Press **c** in the Context Guard panel for retained-history terminal charts. View one metric at a time with **Left / Right**, change the time window with **[ / ]**, and select an agent instance with **Up / Down**. Press **c** to return or **q** to close the panel; controls stay visible below the scrollable content. Generate a self-contained interactive browser report with `bun run charts /absolute/history-directory --output report.html`. It includes filters, UTC timestamp tooltips, request statistics and SVG export. Installed packages expose the same command as `context-guard charts ...` (Bun required). See [chart controls and limits](docs/configuration.md#performance-charts).
106
106
 
107
107
  ## Inspect
108
108
 
package/dist/tui.js CHANGED
@@ -313,8 +313,8 @@ function uniqueRows(value) {
313
313
 
314
314
  // src/tui.tsx
315
315
  import { effect as _$effect5 } from "@opentui/solid";
316
- import { createTextNode as _$createTextNode4 } from "@opentui/solid";
317
- import { insertNode as _$insertNode4 } from "@opentui/solid";
316
+ import { createTextNode as _$createTextNode3 } from "@opentui/solid";
317
+ import { insertNode as _$insertNode5 } from "@opentui/solid";
318
318
  import { setProp as _$setProp5 } from "@opentui/solid";
319
319
  import { createElement as _$createElement5 } from "@opentui/solid";
320
320
  import { createComponent as _$createComponent5 } from "@opentui/solid";
@@ -1224,6 +1224,61 @@ function performanceLines(row2, width) {
1224
1224
  ];
1225
1225
  }
1226
1226
 
1227
+ // src/tui/contextchart.ts
1228
+ function updateContextTrace(previous, row2) {
1229
+ const snapshot = row2?.snapshot;
1230
+ const model = snapshot?.model ?? row2?.node.model;
1231
+ const capacity = snapshot?.capacityTokens;
1232
+ const sessionId = row2?.node.sessionId ?? "";
1233
+ const samples = previous?.sessionId === sessionId && previous.model === model && previous.capacity === capacity ? previous.samples : [];
1234
+ const measurement2 = snapshot?.measurement;
1235
+ const valid2 = (row2?.freshness === "fresh" || row2?.freshness === "stale") && measurement2 && Number.isFinite(measurement2.tokens) && measurement2.tokens >= 0;
1236
+ const at = measurement2?.at ?? snapshot?.observedAt;
1237
+ let next = samples;
1238
+ if (at !== undefined && Number.isFinite(at)) {
1239
+ const sample = {
1240
+ at,
1241
+ ...valid2 ? { tokens: measurement2.tokens } : {}
1242
+ };
1243
+ const last = samples.at(-1);
1244
+ if (!last || at > last.at)
1245
+ next = [...samples, sample].slice(-96);
1246
+ else if (at === last.at && sample.tokens !== last.tokens)
1247
+ next = [...samples.slice(0, -1), sample];
1248
+ }
1249
+ return { sessionId, model, capacity, samples: next };
1250
+ }
1251
+ function contextChart(samples, capacity, trigger, width) {
1252
+ if (capacity === undefined || !Number.isFinite(capacity) || capacity <= 0 || width < 4)
1253
+ return [];
1254
+ const height = 8;
1255
+ const gutter = width >= 16 ? 4 : 0;
1256
+ const size = Math.max(1, Math.min(96, width - gutter - 2));
1257
+ const values = samples.slice(-size);
1258
+ const offset = size - values.length;
1259
+ const thresholdRow = trigger !== undefined && Number.isFinite(trigger) && trigger >= 0 && trigger <= capacity ? Math.round((1 - trigger / capacity) * (height - 1)) : -1;
1260
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1261
+ const lines = Array.from({ length: height }, (_, row2) => {
1262
+ const threshold = row2 === thresholdRow;
1263
+ const axis = gutter ? (row2 === 0 ? "100%" : row2 === height - 1 ? "0%" : "").padStart(gutter) : "";
1264
+ const plot = Array.from({ length: size }, (_2, col) => {
1265
+ const value = values[col - offset]?.tokens;
1266
+ const fill = value === undefined ? 0 : Math.max(0, Math.min(8, Math.ceil((Math.min(value / capacity, 1) * height - (height - row2 - 1)) * 8)));
1267
+ if (threshold)
1268
+ return fill > 0 ? "\u2542" : "\u2504";
1269
+ if (value === 0 && row2 === height - 1)
1270
+ return "\u2581";
1271
+ return blocks[fill] ?? " ";
1272
+ }).join("");
1273
+ return { line: `${axis}\u2502${plot}\u2502`, threshold };
1274
+ });
1275
+ lines.push({
1276
+ line: `${" ".repeat(gutter)}\u2514${"\u2500".repeat(size)}\u2518`,
1277
+ threshold: false
1278
+ });
1279
+ return lines;
1280
+ }
1281
+
1227
1282
  // src/tui/compact.tsx
1228
1283
  import { createTextNode as _$createTextNode } from "@opentui/solid";
1229
1284
  import { insertNode as _$insertNode } from "@opentui/solid";
@@ -1258,7 +1313,7 @@ function progress(ratio, width) {
1258
1313
  const filled = valid2 ? Math.round(Math.min(1, ratio) * size) : 0;
1259
1314
  return clip("\u2588".repeat(filled) + "\u2591".repeat(size - filled) + " " + label, width);
1260
1315
  }
1261
- function mainSections(row2, width, age) {
1316
+ function mainSections(row2, width, age, samples) {
1262
1317
  if (!row2)
1263
1318
  return [{ tone: "base", lines: wrap("Loading session family...", width) }];
1264
1319
  const s = row2.snapshot, m = s?.measurement;
@@ -1279,7 +1334,14 @@ function mainSections(row2, width, age) {
1279
1334
  ...columns("Context", `${tokens(measured)} / ${tokens(s?.capacityTokens)}`, width)
1280
1335
  ]
1281
1336
  },
1282
- { tone: "pressure", lines: [progress(ratio(s?.capacityTokens), width)] },
1337
+ ...contextChart(samples ?? (m && valid2 ? [{ at: m.at, tokens: m.tokens }] : []), s?.capacityTokens, s?.policy.triggerTokens, width).map(({ line, threshold }) => ({
1338
+ tone: threshold ? "threshold" : "pressure",
1339
+ lines: [line]
1340
+ })),
1341
+ {
1342
+ tone: "base",
1343
+ lines: wrap(`${ratio(s?.capacityTokens) === undefined ? "?" : Math.round((ratio(s?.capacityTokens) ?? 0) * 100)}% used \xB7 recent samples \u2192 now`, width)
1344
+ },
1283
1345
  {
1284
1346
  tone: "base",
1285
1347
  lines: [
@@ -1290,7 +1352,8 @@ function mainSections(row2, width, age) {
1290
1352
  {
1291
1353
  tone: "pressure",
1292
1354
  lines: [
1293
- progress(ratio(s?.policy.triggerTokens), width),
1355
+ ...wrap(`${ratio(s?.policy.triggerTokens) === undefined ? "?" : Math.round((ratio(s?.policy.triggerTokens) ?? 0) * 100)}% of threshold \xB7 \u2504 trigger`, width),
1356
+ ...s?.capacityTokens !== undefined && s.policy.triggerTokens !== undefined && s.policy.triggerTokens > s.capacityTokens ? wrap("Threshold above chart capacity", width) : [],
1294
1357
  ...wrap(remaining === undefined ? "Compaction distance unknown" : remaining < 0 ? `${tokens(-remaining)} tokens over threshold` : `${tokens(remaining)} tokens until compaction`, width)
1295
1358
  ]
1296
1359
  },
@@ -1408,6 +1471,7 @@ function CompactSidebar(props) {
1408
1471
  setCollapsed(new Set);
1409
1472
  });
1410
1473
  const current = createMemo(() => props.rows.find((r) => r.node.sessionId === props.sessionID));
1474
+ const trace = createMemo((previous) => updateContextTrace(previous, current()));
1411
1475
  const tree = createMemo(() => subagentTree(props.rows, props.sessionID, collapsed(), hideCompleted(), byPressure()));
1412
1476
  const entries = createMemo(() => new Map(tree().visible.map((entry) => [entry.row.node.sessionId, entry])));
1413
1477
  const toggle = (id) => setCollapsed((previous) => {
@@ -1497,7 +1561,7 @@ function CompactSidebar(props) {
1497
1561
  });
1498
1562
  _$insert(_el$2, _$createComponent(Index, {
1499
1563
  get each() {
1500
- return mainSections(current(), width(), props.state.age(props.sessionID));
1564
+ return mainSections(current(), width(), props.state.age(props.sessionID), trace().samples);
1501
1565
  },
1502
1566
  children: (section2) => _$createComponent(Index, {
1503
1567
  get each() {
@@ -1506,7 +1570,7 @@ function CompactSidebar(props) {
1506
1570
  children: (line) => (() => {
1507
1571
  var _el$13 = _$createElement("text");
1508
1572
  _$insert(_el$13, () => line() || " ");
1509
- _$effect((_$p) => _$setProp(_el$13, "fg", section2().tone === "execution" ? executionColor(current()) : section2().tone === "pressure" ? pressureColor(current()) : section2().tone === "measurement" ? current()?.freshness === "fresh" ? colors().muted : colors().feedback.warning.base : colors().base, _$p));
1573
+ _$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));
1510
1574
  return _el$13;
1511
1575
  })()
1512
1576
  })
@@ -1652,6 +1716,8 @@ function GuardView(props) {
1652
1716
  onCleanup(() => consumer.release());
1653
1717
  const rows = createMemo2(() => props.family ?? props.state.rows([], !props.expanded));
1654
1718
  const current = createMemo2(() => rows().find((row2) => row2.node.sessionId === props.sessionID));
1719
+ const trace = createMemo2((previous) => updateContextTrace(previous, props.expanded ? current() : undefined));
1720
+ const chart = createMemo2(() => contextChart(trace().samples, current()?.snapshot?.capacityTokens, current()?.snapshot?.policy.triggerTokens, props.width));
1655
1721
  const detail = createMemo2(() => {
1656
1722
  const lines = detailLines(current(), props.width, props.expanded, props.state.age(props.sessionID));
1657
1723
  if (props.expanded) {
@@ -1719,26 +1785,48 @@ function GuardView(props) {
1719
1785
  _$insert2(_el$5, () => clip(`${props.state.connection()} / ${props.state.health()?.enforcement ?? "?"}`, props.width));
1720
1786
  _$insert2(_el$, _$createComponent2(For2, {
1721
1787
  get each() {
1722
- return detail();
1788
+ return chart();
1723
1789
  },
1724
- children: (line) => (() => {
1790
+ children: (item) => (() => {
1725
1791
  var _el$1 = _$createElement2("text");
1726
- _$insert2(_el$1, line);
1727
- _$effect2((_$p) => _$setProp2(_el$1, "fg", props.context.theme.text.base, _$p));
1792
+ _$insert2(_el$1, () => item.line);
1793
+ _$effect2((_$p) => _$setProp2(_el$1, "fg", item.threshold ? props.context.theme.text.feedback.warning.base : props.context.theme.text.muted, _$p));
1728
1794
  return _el$1;
1729
1795
  })()
1730
1796
  }), _el$6);
1731
1797
  _$insert2(_el$, _$createComponent2(For2, {
1732
1798
  get each() {
1733
- return props.state.health()?.reasons ?? [];
1799
+ return wrap("Recent context samples \u2192 now \xB7 \u2504 trigger", props.width);
1734
1800
  },
1735
- children: (reason) => (() => {
1801
+ children: (line) => (() => {
1736
1802
  var _el$10 = _$createElement2("text");
1737
- _$insert2(_el$10, () => clip(reason, props.width));
1738
- _$effect2((_$p) => _$setProp2(_el$10, "fg", props.context.theme.text.feedback.warning.base, _$p));
1803
+ _$insert2(_el$10, line);
1804
+ _$effect2((_$p) => _$setProp2(_el$10, "fg", props.context.theme.text.muted, _$p));
1739
1805
  return _el$10;
1740
1806
  })()
1741
1807
  }), _el$6);
1808
+ _$insert2(_el$, _$createComponent2(For2, {
1809
+ get each() {
1810
+ return detail();
1811
+ },
1812
+ children: (line) => (() => {
1813
+ var _el$11 = _$createElement2("text");
1814
+ _$insert2(_el$11, line);
1815
+ _$effect2((_$p) => _$setProp2(_el$11, "fg", props.context.theme.text.base, _$p));
1816
+ return _el$11;
1817
+ })()
1818
+ }), _el$6);
1819
+ _$insert2(_el$, _$createComponent2(For2, {
1820
+ get each() {
1821
+ return props.state.health()?.reasons ?? [];
1822
+ },
1823
+ children: (reason) => (() => {
1824
+ var _el$12 = _$createElement2("text");
1825
+ _$insert2(_el$12, () => clip(reason, props.width));
1826
+ _$effect2((_$p) => _$setProp2(_el$12, "fg", props.context.theme.text.feedback.warning.base, _$p));
1827
+ return _el$12;
1828
+ })()
1829
+ }), _el$6);
1742
1830
  _$insert2(_el$6, (() => {
1743
1831
  var _c$2 = _$memo2(() => props.state.health()?.mode === "observe");
1744
1832
  return () => _c$2() ? current()?.pressure === "crossed" ? "Observe: would compact" : "Observe: no admission" : "";
@@ -1752,9 +1840,9 @@ function GuardView(props) {
1752
1840
  return visible();
1753
1841
  },
1754
1842
  children: (row2) => (() => {
1755
- var _el$11 = _$createElement2("box");
1756
- _$setProp2(_el$11, "flexDirection", "column");
1757
- _$setProp2(_el$11, "onMouseDown", () => {
1843
+ var _el$13 = _$createElement2("box");
1844
+ _$setProp2(_el$13, "flexDirection", "column");
1845
+ _$setProp2(_el$13, "onMouseDown", () => {
1758
1846
  if (row2.hidden)
1759
1847
  props.state.expand(row2.node.sessionId);
1760
1848
  else {
@@ -1766,19 +1854,19 @@ function GuardView(props) {
1766
1854
  });
1767
1855
  }
1768
1856
  });
1769
- _$insert2(_el$11, _$createComponent2(For2, {
1857
+ _$insert2(_el$13, _$createComponent2(For2, {
1770
1858
  get each() {
1771
1859
  return rowLines(row2, Math.max(1, props.width - (props.expanded ? 2 : 0))).map((line) => props.expanded ? `${row2.node.sessionId === props.selectedId ? ">" : " "} ${line}` : line);
1772
1860
  },
1773
1861
  children: (line) => (() => {
1774
- var _el$12 = _$createElement2("text");
1775
- _$insert2(_el$12, line);
1776
- _$effect2((_$p) => _$setProp2(_el$12, "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));
1777
- return _el$12;
1862
+ var _el$14 = _$createElement2("text");
1863
+ _$insert2(_el$14, line);
1864
+ _$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));
1865
+ return _el$14;
1778
1866
  })()
1779
1867
  }));
1780
- _$effect2((_$p) => _$setProp2(_el$11, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1781
- return _el$11;
1868
+ _$effect2((_$p) => _$setProp2(_el$13, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1869
+ return _el$13;
1782
1870
  })()
1783
1871
  }), _el$0);
1784
1872
  _$setProp2(_el$0, "onMouseDown", () => props.context.ui.panel.open("context-guard.monitor"));
@@ -1810,18 +1898,18 @@ function GuardView(props) {
1810
1898
 
1811
1899
  // src/tui/panel.tsx
1812
1900
  import { effect as _$effect4 } from "@opentui/solid";
1813
- import { createTextNode as _$createTextNode3 } from "@opentui/solid";
1814
- import { insertNode as _$insertNode3 } from "@opentui/solid";
1815
1901
  import { memo as _$memo3 } from "@opentui/solid";
1902
+ import { insertNode as _$insertNode4 } from "@opentui/solid";
1816
1903
  import { insert as _$insert4 } from "@opentui/solid";
1817
1904
  import { createComponent as _$createComponent4 } from "@opentui/solid";
1818
- import { setProp as _$setProp4 } from "@opentui/solid";
1819
1905
  import { use as _$use } from "@opentui/solid";
1906
+ import { setProp as _$setProp4 } from "@opentui/solid";
1820
1907
  import { createElement as _$createElement4 } from "@opentui/solid";
1821
- import { createSignal as createSignal4, createMemo as createMemo3, createEffect as createEffect4, onCleanup as onCleanup3, Show as Show3 } from "solid-js";
1908
+ import { createSignal as createSignal4, createMemo as createMemo3, createEffect as createEffect4, onCleanup as onCleanup3, Show as Show3, For as For4 } from "solid-js";
1822
1909
 
1823
1910
  // src/tui/charts.tsx
1824
1911
  import { effect as _$effect3 } from "@opentui/solid";
1912
+ import { insertNode as _$insertNode3 } from "@opentui/solid";
1825
1913
  import { insert as _$insert3 } from "@opentui/solid";
1826
1914
  import { createComponent as _$createComponent3 } from "@opentui/solid";
1827
1915
  import { setProp as _$setProp3 } from "@opentui/solid";
@@ -1829,7 +1917,7 @@ import { createElement as _$createElement3 } from "@opentui/solid";
1829
1917
  import { createEffect as createEffect3, createSignal as createSignal3, onCleanup as onCleanup2, For as For3 } from "solid-js";
1830
1918
 
1831
1919
  // src/tui/chartformat.ts
1832
- function sparkline(points, from, to, width, gaps = []) {
1920
+ function columns2(points, from, to, width, gaps = []) {
1833
1921
  const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
1834
1922
  const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
1835
1923
  let min = Infinity, max = -Infinity;
@@ -1841,6 +1929,13 @@ function sparkline(points, from, to, width, gaps = []) {
1841
1929
  const i = column(point.time);
1842
1930
  values[i] = Math.max(values[i] ?? -Infinity, point.value);
1843
1931
  }
1932
+ for (const time of gaps)
1933
+ if (time >= from && time <= to)
1934
+ values[column(time)] = undefined;
1935
+ return { values, column, min, max };
1936
+ }
1937
+ function sparkline(points, from, to, width, gaps = []) {
1938
+ const { values, column, min, max } = columns2(points, from, to, width, gaps);
1844
1939
  const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1845
1940
  const chars = values.map((v) => v === undefined ? " " : bars[Math.min(7, Math.floor(v / Math.max(1, max) * 7))] ?? "\u2581");
1846
1941
  for (const time of gaps)
@@ -1848,20 +1943,58 @@ function sparkline(points, from, to, width, gaps = []) {
1848
1943
  chars[column(time)] = "\u2506";
1849
1944
  return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
1850
1945
  }
1851
- function historyChartLines(result, width, from, to) {
1946
+ var chartMetrics = [
1947
+ { label: "Live TPS ~", unit: "t/s", key: "liveTps", source: "points" },
1948
+ { label: "Avg model", unit: "t/s", key: "averageModelTps", source: "points" },
1949
+ {
1950
+ label: "Avg session",
1951
+ unit: "t/s",
1952
+ key: "averageSessionTps",
1953
+ source: "points"
1954
+ },
1955
+ { label: "TTFT ~", unit: "s", key: "ttftMs", source: "requests" },
1956
+ { label: "Request throughput", unit: "t/s", key: "tps", source: "requests" },
1957
+ {
1958
+ label: "Cumulative output",
1959
+ unit: "tokens",
1960
+ key: "sessionOutputTokens",
1961
+ source: "points"
1962
+ }
1963
+ ];
1964
+ function metricPlot(points, from, to, width, gaps = []) {
1965
+ const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
1966
+ const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
1967
+ const scale = Math.max(1, peak);
1968
+ const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
1969
+ const { values, column } = columns2(points, from, to, Math.max(1, width - gutter - 2), gaps);
1970
+ const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
1971
+ const height = 6;
1972
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1973
+ const lines = Array.from({ length: height }, (_, row2) => {
1974
+ const axis = gutter ? (row2 === 0 ? label(scale) : row2 === height - 1 ? "0.0" : "").padStart(gutter) : "";
1975
+ return `${axis}\u2502${values.map((value, i) => {
1976
+ if (gapColumns.has(i))
1977
+ return "\u2506";
1978
+ if (value === undefined)
1979
+ return " ";
1980
+ const fill = Math.max(0, Math.min(8, Math.ceil((value / scale * height - (height - row2 - 1)) * 8)));
1981
+ return value === 0 && row2 === height - 1 ? "\u2581" : blocks[fill];
1982
+ }).join("")}\u2502`;
1983
+ });
1984
+ lines.push(`${" ".repeat(gutter)}\u2514${"\u2500".repeat(values.length)}\u2518`);
1985
+ const start = new Date(from).toISOString().slice(11, 16), end = new Date(to).toISOString().slice(11, 16);
1986
+ const axisWidth = values.length + 2;
1987
+ if (axisWidth >= 11)
1988
+ lines.push(`${" ".repeat(gutter)}${start}${" ".repeat(axisWidth - 10)}${end}`);
1989
+ return lines;
1990
+ }
1991
+ function historyChartLines(result, width, from, to, metricIndex = 0) {
1852
1992
  if (!result.enabled)
1853
1993
  return wrap("History recording is disabled on this server.", width);
1854
1994
  const series = result.data.series[0];
1855
1995
  if (!series)
1856
1996
  return wrap("No retained measurements in this window.", width);
1857
1997
  const gaps = result.data.gaps.map((g) => g.time);
1858
- const chart = (label, unit, points) => {
1859
- const plotted = sparkline(points, from, to, width - 2, gaps);
1860
- return [
1861
- ...wrap(`${label}${plotted.min === undefined ? ": unavailable" : ` \xB7 ${plotted.min.toFixed(1)}\u2013${plotted.max?.toFixed(1)} ${unit}`}`, width),
1862
- `\u2502${plotted.line}\u2502`
1863
- ];
1864
- };
1865
1998
  const observations = (key) => series.points.map((p) => ({
1866
1999
  time: p.time,
1867
2000
  ...p[key] === undefined ? {} : { value: p[key] }
@@ -1870,20 +2003,17 @@ function historyChartLines(result, width, from, to) {
1870
2003
  time: p.time,
1871
2004
  ...p[key] === undefined ? {} : { value: key === "ttftMs" ? p[key] / 1000 : p[key] }
1872
2005
  }));
2006
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2007
+ const points = metric2.source === "points" ? observations(metric2.key) : requests(metric2.key);
2008
+ const plotted = sparkline(points, from, to, width - 2, gaps);
1873
2009
  return [
1874
- ...chart("Live TPS ~", "t/s", observations("liveTps")),
1875
- "",
1876
- ...chart("Avg model", "t/s", observations("averageModelTps")),
1877
- "",
1878
- ...chart("Avg session", "t/s", observations("averageSessionTps")),
1879
- "",
1880
- ...chart("TTFT ~", "s", requests("ttftMs")),
1881
- "",
1882
- ...chart("Request throughput", "t/s", requests("tps")),
2010
+ ...wrap(`\u2039 ${metric2.label} \u203A \xB7 ${metricIndex + 1}/${chartMetrics.length}`, width),
2011
+ ...wrap(plotted.min === undefined ? "No measurements for this metric." : `${plotted.min.toFixed(1)}\u2013${plotted.max?.toFixed(1)} ${metric2.unit} \xB7 observed range`, width),
1883
2012
  "",
1884
- ...chart("Cumulative output", "tokens", observations("sessionOutputTokens")),
2013
+ ...metricPlot(points, from, to, width, gaps),
1885
2014
  "",
1886
- ...wrap("UTC time \u2192 \xB7 observed peaks per column \xB7 blank = no sample \xB7 \u2506 = gap", width),
2015
+ ...wrap("UTC time \u2192 \xB7 peaks per column", width),
2016
+ ...wrap("blank = no sample \xB7 \u2506 = gap", width),
1887
2017
  ...wrap(`${new Date(from).toISOString()} \u2192 ${new Date(to).toISOString()}`, width),
1888
2018
  ...series.points.some((p) => p.partial) ? wrap("Partial timing coverage; averages include only matched model durations.", width) : [],
1889
2019
  ...result.truncated ? wrap("Downsampled/bounded history; some older requests or gap markers may be omitted.", width) : [],
@@ -1940,44 +2070,47 @@ function HistoryCharts(props) {
1940
2070
  });
1941
2071
  const lines = () => {
1942
2072
  const current = result();
1943
- return current ? historyChartLines(current, Math.max(4, props.width), range().from, range().to) : [];
2073
+ return current ? historyChartLines(current, Math.max(4, props.width), range().from, range().to, props.metricIndex) : [];
1944
2074
  };
1945
2075
  return (() => {
1946
- var _el$ = _$createElement3("box");
2076
+ var _el$ = _$createElement3("box"), _el$2 = _$createElement3("box");
2077
+ _$insertNode3(_el$, _el$2);
1947
2078
  _$setProp3(_el$, "flexDirection", "column");
1948
2079
  _$insert3(_el$, _$createComponent3(For3, {
1949
2080
  get each() {
1950
2081
  return wrap(`Performance charts \xB7 ${props.sessionId}`, props.width);
1951
2082
  },
1952
2083
  children: (line) => (() => {
1953
- var _el$2 = _$createElement3("text");
1954
- _$insert3(_el$2, line);
1955
- _$effect3((_$p) => _$setProp3(_el$2, "fg", props.context.theme.text.base, _$p));
1956
- return _el$2;
2084
+ var _el$3 = _$createElement3("text");
2085
+ _$insert3(_el$3, line);
2086
+ _$effect3((_$p) => _$setProp3(_el$3, "fg", props.context.theme.text.base, _$p));
2087
+ return _el$3;
1957
2088
  })()
1958
- }), null);
2089
+ }), _el$2);
1959
2090
  _$insert3(_el$, _$createComponent3(For3, {
1960
2091
  get each() {
1961
2092
  return wrap(status(), props.width);
1962
2093
  },
1963
2094
  children: (line) => (() => {
1964
- var _el$3 = _$createElement3("text");
1965
- _$insert3(_el$3, line);
1966
- _$effect3((_$p) => _$setProp3(_el$3, "fg", props.context.theme.text.muted, _$p));
1967
- return _el$3;
2095
+ var _el$4 = _$createElement3("text");
2096
+ _$insert3(_el$4, line);
2097
+ _$effect3((_$p) => _$setProp3(_el$4, "fg", props.context.theme.text.muted, _$p));
2098
+ return _el$4;
1968
2099
  })()
1969
- }), null);
1970
- _$insert3(_el$, _$createComponent3(For3, {
2100
+ }), _el$2);
2101
+ _$setProp3(_el$2, "flexDirection", "column");
2102
+ _$setProp3(_el$2, "paddingTop", 1);
2103
+ _$insert3(_el$2, _$createComponent3(For3, {
1971
2104
  get each() {
1972
2105
  return lines();
1973
2106
  },
1974
2107
  children: (line) => (() => {
1975
- var _el$4 = _$createElement3("text");
1976
- _$insert3(_el$4, line || " ");
1977
- _$effect3((_$p) => _$setProp3(_el$4, "fg", props.context.theme.text.base, _$p));
1978
- return _el$4;
2108
+ var _el$5 = _$createElement3("text");
2109
+ _$insert3(_el$5, line || " ");
2110
+ _$effect3((_$p) => _$setProp3(_el$5, "fg", props.context.theme.text.base, _$p));
2111
+ return _el$5;
1979
2112
  })()
1980
- }), null);
2113
+ }));
1981
2114
  return _el$;
1982
2115
  })();
1983
2116
  }
@@ -1985,6 +2118,8 @@ function HistoryCharts(props) {
1985
2118
  // src/tui/panel.tsx
1986
2119
  function GuardPanel(props) {
1987
2120
  const [showCharts, setShowCharts] = createSignal4(false);
2121
+ const [metricIndex, setMetricIndex] = createSignal4(0);
2122
+ const width = () => Math.max(4, props.input.width - 4);
1988
2123
  const windows = [300000, 3600000, 86400000, 7 * 86400000];
1989
2124
  const [windowIndex, setWindowIndex] = createSignal4(1);
1990
2125
  const [chartRefresh, setChartRefresh] = createSignal4(0);
@@ -2034,6 +2169,27 @@ function GuardPanel(props) {
2034
2169
  props.context.keymap.layer(() => ({
2035
2170
  enabled: () => props.input.focused,
2036
2171
  commands: [{
2172
+ id: "context-guard.close",
2173
+ title: "Close Context Guard",
2174
+ bind: "q",
2175
+ run: () => props.input.close()
2176
+ }, {
2177
+ id: "context-guard.chart-previous",
2178
+ title: "Previous chart metric",
2179
+ bind: "left",
2180
+ run: () => {
2181
+ if (showCharts())
2182
+ setMetricIndex((i) => (i + chartMetrics.length - 1) % chartMetrics.length);
2183
+ }
2184
+ }, {
2185
+ id: "context-guard.chart-next",
2186
+ title: "Next chart metric",
2187
+ bind: "right",
2188
+ run: () => {
2189
+ if (showCharts())
2190
+ setMetricIndex((i) => (i + 1) % chartMetrics.length);
2191
+ }
2192
+ }, {
2037
2193
  id: "context-guard.charts",
2038
2194
  title: "Toggle performance charts",
2039
2195
  bind: "c",
@@ -2104,20 +2260,26 @@ function GuardPanel(props) {
2104
2260
  }
2105
2261
  }]
2106
2262
  }));
2263
+ const hints = () => showCharts() ? ["c back \xB7 q close", "\u2190 / \u2192 metric \xB7 \u2191 / \u2193 session", `[ / ] window \xB7 ${["5 minutes", "1 hour", "24 hours", "7 days"][windowIndex()]}`, "r refresh \xB7 Enter open session"] : ["c charts \xB7 q close", `\u2191 / \u2193 select ${offset() + 1} \xB7 Enter open`, "space expand \xB7 i inspect", "r refresh \xB7 l load more"];
2107
2264
  return (() => {
2108
- var _el$ = _$createElement4("scrollbox"), _el$2 = _$createElement4("box"), _el$6 = _$createElement4("text"), _el$8 = _$createElement4("text"), _el$9 = _$createTextNode3(`up/down select `), _el$0 = _$createTextNode3(`; Enter open`), _el$1 = _$createElement4("text"), _el$11 = _$createElement4("text"), _el$13 = _$createElement4("text");
2109
- _$insertNode3(_el$, _el$2);
2110
- _$insertNode3(_el$, _el$6);
2111
- _$insertNode3(_el$, _el$8);
2112
- _$insertNode3(_el$, _el$1);
2113
- _$insertNode3(_el$, _el$11);
2114
- _$insertNode3(_el$, _el$13);
2265
+ var _el$ = _$createElement4("box"), _el$2 = _$createElement4("scrollbox"), _el$3 = _$createElement4("box"), _el$4 = _$createElement4("text"), _el$5 = _$createElement4("box");
2266
+ _$insertNode4(_el$, _el$2);
2267
+ _$insertNode4(_el$, _el$5);
2268
+ _$setProp4(_el$, "flexDirection", "column");
2269
+ _$setProp4(_el$, "flexGrow", 1);
2270
+ _$setProp4(_el$, "paddingLeft", 2);
2271
+ _$setProp4(_el$, "paddingRight", 2);
2272
+ _$setProp4(_el$, "paddingTop", 1);
2273
+ _$setProp4(_el$, "paddingBottom", 1);
2274
+ _$insertNode4(_el$2, _el$3);
2115
2275
  _$use((value) => {
2116
2276
  scroll = value;
2117
- }, _el$);
2118
- _$setProp4(_el$, "flexGrow", 1);
2119
- _$setProp4(_el$2, "flexDirection", "column");
2120
- _$insert4(_el$2, _$createComponent4(GuardView, {
2277
+ }, _el$2);
2278
+ _$setProp4(_el$2, "flexGrow", 1);
2279
+ _$setProp4(_el$2, "minHeight", 0);
2280
+ _$insertNode4(_el$3, _el$4);
2281
+ _$setProp4(_el$3, "flexDirection", "column");
2282
+ _$insert4(_el$3, _$createComponent4(GuardView, {
2121
2283
  get sessionID() {
2122
2284
  return props.input.sessionID;
2123
2285
  },
@@ -2128,7 +2290,7 @@ function GuardPanel(props) {
2128
2290
  return props.context;
2129
2291
  },
2130
2292
  get width() {
2131
- return Math.max(10, props.input.width - 4);
2293
+ return width();
2132
2294
  },
2133
2295
  expanded: true,
2134
2296
  get family() {
@@ -2140,24 +2302,28 @@ function GuardPanel(props) {
2140
2302
  get inspectId() {
2141
2303
  return inspectId();
2142
2304
  }
2143
- }));
2144
- _$insert4(_el$, _$createComponent4(Show3, {
2305
+ }), _el$4);
2306
+ _$insert4(_el$4, () => wrap("Guard input = prompt + caches. Native Context uses different totals.", width()).join("\n"));
2307
+ _$insert4(_el$2, _$createComponent4(Show3, {
2145
2308
  get when() {
2146
2309
  return showCharts();
2147
2310
  },
2148
2311
  get children() {
2149
- return [_$createComponent4(HistoryCharts, {
2312
+ return _$createComponent4(HistoryCharts, {
2150
2313
  get sessionId() {
2151
2314
  return selected();
2152
2315
  },
2153
2316
  get windowMs() {
2154
2317
  return windows[windowIndex()] ?? 3600000;
2155
2318
  },
2319
+ get metricIndex() {
2320
+ return metricIndex();
2321
+ },
2156
2322
  get refresh() {
2157
2323
  return chartRefresh();
2158
2324
  },
2159
2325
  get width() {
2160
- return Math.max(4, props.input.width - 6);
2326
+ return width();
2161
2327
  },
2162
2328
  get state() {
2163
2329
  return props.state;
@@ -2165,38 +2331,31 @@ function GuardPanel(props) {
2165
2331
  get context() {
2166
2332
  return props.context;
2167
2333
  }
2168
- }), (() => {
2169
- var _el$3 = _$createElement4("text"), _el$4 = _$createTextNode3(`[ / ] window \xB7 `);
2170
- _$insertNode3(_el$3, _el$4);
2171
- _$insert4(_el$3, () => ["5 minutes", "1 hour", "24 hours", "7 days"][windowIndex()], null);
2172
- _$effect4((_$p) => _$setProp4(_el$3, "fg", props.context.theme.text.muted, _$p));
2173
- return _el$3;
2174
- })()];
2334
+ });
2175
2335
  }
2176
- }), _el$6);
2177
- _$insertNode3(_el$6, _$createTextNode3(`r refresh / l load more`));
2178
- _$insertNode3(_el$8, _el$9);
2179
- _$insertNode3(_el$8, _el$0);
2180
- _$insert4(_el$8, () => offset() + 1, _el$0);
2181
- _$insertNode3(_el$1, _$createTextNode3(`space expand / i inspect selected / c charts`));
2182
- _$insertNode3(_el$11, _$createTextNode3(`Guard input = prompt + caches.`));
2183
- _$insertNode3(_el$13, _$createTextNode3(`Native Context uses different totals.`));
2336
+ }), null);
2337
+ _$setProp4(_el$5, "flexDirection", "column");
2338
+ _$setProp4(_el$5, "flexShrink", 0);
2339
+ _$setProp4(_el$5, "paddingTop", 1);
2340
+ _$insert4(_el$5, _$createComponent4(For4, {
2341
+ get each() {
2342
+ return hints().flatMap((hint) => wrap(hint, width()));
2343
+ },
2344
+ children: (line) => (() => {
2345
+ var _el$6 = _$createElement4("text");
2346
+ _$insert4(_el$6, line);
2347
+ _$effect4((_$p) => _$setProp4(_el$6, "fg", props.context.theme.text.muted, _$p));
2348
+ return _el$6;
2349
+ })()
2350
+ }));
2184
2351
  _$effect4((_p$) => {
2185
- var _v$ = !showCharts(), _v$2 = props.context.theme.text.muted, _v$3 = props.context.theme.text.muted, _v$4 = props.context.theme.text.muted, _v$5 = props.context.theme.text.muted, _v$6 = props.context.theme.text.muted;
2186
- _v$ !== _p$.e && (_p$.e = _$setProp4(_el$2, "visible", _v$, _p$.e));
2187
- _v$2 !== _p$.t && (_p$.t = _$setProp4(_el$6, "fg", _v$2, _p$.t));
2188
- _v$3 !== _p$.a && (_p$.a = _$setProp4(_el$8, "fg", _v$3, _p$.a));
2189
- _v$4 !== _p$.o && (_p$.o = _$setProp4(_el$1, "fg", _v$4, _p$.o));
2190
- _v$5 !== _p$.i && (_p$.i = _$setProp4(_el$11, "fg", _v$5, _p$.i));
2191
- _v$6 !== _p$.n && (_p$.n = _$setProp4(_el$13, "fg", _v$6, _p$.n));
2352
+ var _v$ = !showCharts(), _v$2 = props.context.theme.text.muted;
2353
+ _v$ !== _p$.e && (_p$.e = _$setProp4(_el$3, "visible", _v$, _p$.e));
2354
+ _v$2 !== _p$.t && (_p$.t = _$setProp4(_el$4, "fg", _v$2, _p$.t));
2192
2355
  return _p$;
2193
2356
  }, {
2194
2357
  e: undefined,
2195
- t: undefined,
2196
- a: undefined,
2197
- o: undefined,
2198
- i: undefined,
2199
- n: undefined
2358
+ t: undefined
2200
2359
  });
2201
2360
  return _el$;
2202
2361
  })();
@@ -2245,7 +2404,7 @@ var tui_default = Plugin.define({
2245
2404
  },
2246
2405
  get children() {
2247
2406
  var _el$ = _$createElement5("text");
2248
- _$insertNode4(_el$, _$createTextNode4(`Context Guard: open panel`));
2407
+ _$insertNode5(_el$, _$createTextNode3(`Context Guard: open panel`));
2249
2408
  _$setProp5(_el$, "onMouseDown", () => context.ui.panel.open("context-guard.monitor"));
2250
2409
  _$effect5((_$p) => _$setProp5(_el$, "fg", context.theme.text.muted, _$p));
2251
2410
  return _el$;
@@ -1,5 +1,4 @@
1
1
  import type { HistoryChartResult } from "../historycharts.js";
2
- /** Each cell contains an observed peak, never an interpolated or invented zero. */
3
2
  export declare function sparkline(points: readonly {
4
3
  time: number;
5
4
  value?: number;
@@ -8,4 +7,40 @@ export declare function sparkline(points: readonly {
8
7
  min?: number;
9
8
  max?: number;
10
9
  };
11
- export declare function historyChartLines(result: HistoryChartResult, width: number, from: number, to: number): string[];
10
+ export declare const chartMetrics: readonly [{
11
+ readonly label: "Live TPS ~";
12
+ readonly unit: "t/s";
13
+ readonly key: "liveTps";
14
+ readonly source: "points";
15
+ }, {
16
+ readonly label: "Avg model";
17
+ readonly unit: "t/s";
18
+ readonly key: "averageModelTps";
19
+ readonly source: "points";
20
+ }, {
21
+ readonly label: "Avg session";
22
+ readonly unit: "t/s";
23
+ readonly key: "averageSessionTps";
24
+ readonly source: "points";
25
+ }, {
26
+ readonly label: "TTFT ~";
27
+ readonly unit: "s";
28
+ readonly key: "ttftMs";
29
+ readonly source: "requests";
30
+ }, {
31
+ readonly label: "Request throughput";
32
+ readonly unit: "t/s";
33
+ readonly key: "tps";
34
+ readonly source: "requests";
35
+ }, {
36
+ readonly label: "Cumulative output";
37
+ readonly unit: "tokens";
38
+ readonly key: "sessionOutputTokens";
39
+ readonly source: "points";
40
+ }];
41
+ /** Plot only observed column peaks; leave missing data and gaps disconnected. */
42
+ export declare function metricPlot(points: readonly {
43
+ time: number;
44
+ value?: number;
45
+ }[], from: number, to: number, width: number, gaps?: readonly number[]): string[];
46
+ export declare function historyChartLines(result: HistoryChartResult, width: number, from: number, to: number, metricIndex?: number): string[];
@@ -3,6 +3,7 @@ import type { GuardState } from "./state.js";
3
3
  export declare function HistoryCharts(props: {
4
4
  sessionId: string;
5
5
  windowMs: number;
6
+ metricIndex: number;
6
7
  refresh: number;
7
8
  width: number;
8
9
  state: GuardState;
@@ -1,12 +1,13 @@
1
+ import { type ContextSample } from "./contextchart.js";
1
2
  import type { GuardRow } from "../family.js";
2
3
  export declare function tokens(value: number | undefined): string;
3
4
  export declare function columns(left: string, right: string, width: number): string[];
4
5
  export declare function progress(ratio: number | undefined, width: number): string;
5
6
  export interface MainSection {
6
- readonly tone: "base" | "execution" | "pressure" | "measurement";
7
+ readonly tone: "base" | "execution" | "pressure" | "measurement" | "threshold";
7
8
  readonly lines: readonly string[];
8
9
  }
9
- export declare function mainSections(row: GuardRow | undefined, width: number, age: number): MainSection[];
10
+ export declare function mainSections(row: GuardRow | undefined, width: number, age: number, samples?: readonly ContextSample[]): MainSection[];
10
11
  export declare function mainLines(row: GuardRow | undefined, width: number, age: number): string[];
11
12
  export declare const statusSymbol: {
12
13
  readonly running: "●";
@@ -0,0 +1,19 @@
1
+ import type { GuardRow } from "../family.js";
2
+ export interface ContextSample {
3
+ readonly at: number;
4
+ readonly tokens?: number;
5
+ }
6
+ export interface ContextTrace {
7
+ readonly sessionId: string;
8
+ readonly model: string | undefined;
9
+ readonly capacity: number | undefined;
10
+ readonly samples: readonly ContextSample[];
11
+ }
12
+ /** Keep only actual measurements, never copies made by age/render ticks. */
13
+ export declare function updateContextTrace(previous: ContextTrace | undefined, row: GuardRow | undefined): ContextTrace;
14
+ export interface ContextChartLine {
15
+ readonly line: string;
16
+ readonly threshold: boolean;
17
+ }
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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cesarus777/opencode-cgp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {