@cesarus777/opencode-cgp 0.2.1 → 0.2.3

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.3` restores context usage for existing sessions when the plugin is enabled and keeps stale occupancy visible.
45
45
 
46
46
  ```sh
47
47
  opencode plugin add @cesarus777/opencode-cgp@0.2.1
@@ -96,13 +96,15 @@ 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
+ Enabling Context Guard after work has already started also shows the last reported prompt usage for idle sessions and subagents. Opening the sidebar or panel restores it from the public session context, preserving its original age and **STALE** label. Stale usage still shows tokens and occupancy percentage. Restoration respects model changes, reverts and completed compactions; unavailable provider usage stays unknown. These reads never request compaction, and transcript content is never retained or sent through the plugin RPC.
104
+
103
105
  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
106
 
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).
107
+ 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
108
 
107
109
  ## Inspect
108
110
 
package/dist/core.js CHANGED
@@ -748,13 +748,14 @@ function metric(snapshot, now, staleMs) {
748
748
  const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
749
749
  const t = snapshot?.policy.triggerTokens;
750
750
  const valid = freshness === "fresh" && m !== undefined;
751
+ const displayable = valid || freshness === "stale" && m !== undefined;
751
752
  const pressureRatio = valid && t !== undefined && t > 0 ? m.tokens / t : undefined;
752
753
  const crossed = valid && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
753
754
  return {
754
755
  freshness,
755
756
  pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
756
757
  ...pressureRatio === undefined ? {} : { pressureRatio },
757
- ...valid && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
758
+ ...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
758
759
  ...valid && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
759
760
  };
760
761
  }
package/dist/index.js CHANGED
@@ -1330,7 +1330,7 @@ function freezeSnapshot(row2) {
1330
1330
  }
1331
1331
 
1332
1332
  // src/snapshotserver.ts
1333
- async function serveSnapshots(rpc, store, performance = false) {
1333
+ async function serveSnapshots(rpc, store, performance = false, prepareRead) {
1334
1334
  if (!rpc)
1335
1335
  return () => {
1336
1336
  };
@@ -1341,9 +1341,10 @@ async function serveSnapshots(rpc, store, performance = false) {
1341
1341
  const batchValid = performance ? isPerformanceBatch : isBatch;
1342
1342
  const resultValid = performance ? isPerformanceResult : isResult;
1343
1343
  const registration = await rpc.register(performance ? performanceRpc : monitorRpc, {
1344
- read: async (input) => {
1344
+ read: async (input, context) => {
1345
1345
  if (!isRead(input))
1346
1346
  throw new Error("Invalid snapshot request");
1347
+ await prepareRead?.(input.sessionIds, context.signal);
1347
1348
  const raw = store.read(input);
1348
1349
  const result = performance ? raw : stripPerformance(raw);
1349
1350
  if (!resultValid(result))
@@ -2053,7 +2054,10 @@ async function setup(ctx, injectedSink) {
2053
2054
  diagnostics.emit(event);
2054
2055
  }
2055
2056
  };
2056
- const stopV1 = await serveSnapshots(ctx.rpc, store).catch(() => {
2057
+ let prepareRead = async (_ids, _signal) => {
2058
+ };
2059
+ const onRead = (ids, signal) => prepareRead(ids, signal);
2060
+ const stopV1 = await serveSnapshots(ctx.rpc, store, false, onRead).catch(() => {
2057
2061
  diagnostics.emit({
2058
2062
  time: Date.now(),
2059
2063
  type: "snapshot-service-unavailable"
@@ -2061,7 +2065,7 @@ async function setup(ctx, injectedSink) {
2061
2065
  return () => {
2062
2066
  };
2063
2067
  });
2064
- const stopV2 = await serveSnapshots(ctx.rpc, store, true).catch(() => {
2068
+ const stopV2 = await serveSnapshots(ctx.rpc, store, true, onRead).catch(() => {
2065
2069
  diagnostics.emit({
2066
2070
  time: Date.now(),
2067
2071
  type: "performance-service-unavailable"
@@ -2179,8 +2183,73 @@ async function setup(ctx, injectedSink) {
2179
2183
  }, sink);
2180
2184
  monitor = new ContextMonitor(config, scheduler, sink);
2181
2185
  const activeMonitor = monitor;
2186
+ const usageReads = new Map;
2187
+ prepareRead = (ids, signal) => serialized(async () => {
2188
+ if (stopped || signal.aborted || !ctx.session.context || !ids.length)
2189
+ return;
2190
+ const readSignal = AbortSignal.any([
2191
+ signal,
2192
+ controller.signal,
2193
+ AbortSignal.timeout(5000)
2194
+ ]);
2195
+ for (const id of ids) {
2196
+ if (readSignal.aborted)
2197
+ break;
2198
+ const existing = activeMonitor.sessions.get(id);
2199
+ if (existing?.usage.current || existing?.active || (existing?.usage.modelRequests ?? 0) > 0 || recoveries.has(id))
2200
+ continue;
2201
+ if (existing && (existing.compaction.phase !== "idle" || existing.compaction.lastAttempt !== undefined || existing.compaction.lastCompleted !== undefined || existing.compaction.failures > 0))
2202
+ continue;
2203
+ if (Date.now() - (usageReads.get(id) ?? 0) < 30000)
2204
+ continue;
2205
+ if (usageReads.size >= config.safety.maxSessions) {
2206
+ const oldest = usageReads.keys().next().value;
2207
+ if (oldest !== undefined)
2208
+ usageReads.delete(oldest);
2209
+ }
2210
+ usageReads.set(id, Date.now());
2211
+ try {
2212
+ const info = await ctx.session.get({ sessionID: id }, { signal: readSignal });
2213
+ if (info.location.directory !== ctx.location.directory)
2214
+ continue;
2215
+ const messages = await ctx.session.context({ sessionID: id }, { signal: readSignal });
2216
+ const boundary = info.revert?.messageID;
2217
+ const end = boundary ? messages.findIndex((m) => m.id === boundary) : messages.length;
2218
+ if (end < 0)
2219
+ continue;
2220
+ const compacted = messages.findLastIndex((m, index) => index < end && m.type === "compaction" && m.status === "completed");
2221
+ const message = messages.findLast((m, index) => index > compacted && index < end && m.type === "assistant" && m.tokens !== undefined);
2222
+ if (message?.type !== "assistant" || !message.tokens)
2223
+ continue;
2224
+ const tokens = promptTokens(message.tokens);
2225
+ const model = `${message.model.providerID}/${message.model.id}`;
2226
+ const at = message.time.completed ?? message.time.streamed;
2227
+ if (tokens === undefined || at === undefined || !Number.isFinite(at) || at < 0 || at > Date.now())
2228
+ continue;
2229
+ if (info.model && model !== `${info.model.providerID}/${info.model.id}`)
2230
+ continue;
2231
+ await refreshModels(!models.has(model), readSignal);
2232
+ const s = await discover(id, readSignal);
2233
+ if (!s || s.model && s.model !== model || s.usage.current || readSignal.aborted)
2234
+ continue;
2235
+ activeMonitor.touch({ sessionId: id, model }, models.get(model));
2236
+ s.usage.current = {
2237
+ tokens,
2238
+ at,
2239
+ model,
2240
+ provenance: "provider-normalized",
2241
+ confidence: "reported",
2242
+ scope: "completed-request",
2243
+ requestId: message.id
2244
+ };
2245
+ s.usage.peak = Math.max(s.usage.peak, tokens);
2246
+ store.mark(id);
2247
+ } catch {
2248
+ }
2249
+ }
2250
+ });
2182
2251
  let models = new Map, modelAt = 0, modelRetryAt = 0, modelRetryMs = 1000;
2183
- async function refreshModels(retryMissing = false) {
2252
+ async function refreshModels(retryMissing = false, signal = controller.signal) {
2184
2253
  const now = Date.now();
2185
2254
  if (now - modelAt < 60000 && (!retryMissing || now < modelRetryAt))
2186
2255
  return;
@@ -2188,7 +2257,10 @@ async function setup(ctx, injectedSink) {
2188
2257
  modelRetryAt = now + modelRetryMs;
2189
2258
  modelRetryMs = Math.min(60000, modelRetryMs * 2);
2190
2259
  try {
2191
- models = new Map((await ctx.model.list(undefined, { signal: controller.signal })).data.map((m) => [`${m.providerID}/${m.id}`, { ...m.limit }]));
2260
+ models = new Map((await ctx.model.list(undefined, { signal })).data.map((m) => [
2261
+ `${m.providerID}/${m.id}`,
2262
+ { ...m.limit }
2263
+ ]));
2192
2264
  let missing = false;
2193
2265
  for (const s of activeMonitor.sessions.values()) {
2194
2266
  const limits = s.model ? models.get(s.model) : undefined;
@@ -2212,8 +2284,8 @@ async function setup(ctx, injectedSink) {
2212
2284
  sink.emit({ time: Date.now(), type: "model-metadata-unavailable" });
2213
2285
  }
2214
2286
  }
2215
- async function discover(sessionId) {
2216
- const info = await ctx.session.get({ sessionID: sessionId }, { signal: controller.signal });
2287
+ async function discover(sessionId, signal = controller.signal) {
2288
+ const info = await ctx.session.get({ sessionID: sessionId }, { signal });
2217
2289
  if (info.location.directory !== ctx.location.directory)
2218
2290
  return;
2219
2291
  const identity = {
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";
@@ -330,13 +330,14 @@ function metric(snapshot, now, staleMs) {
330
330
  const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
331
331
  const t = snapshot?.policy.triggerTokens;
332
332
  const valid2 = freshness === "fresh" && m !== undefined;
333
+ const displayable = valid2 || freshness === "stale" && m !== undefined;
333
334
  const pressureRatio = valid2 && t !== undefined && t > 0 ? m.tokens / t : undefined;
334
335
  const crossed = valid2 && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
335
336
  return {
336
337
  freshness,
337
338
  pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
338
339
  ...pressureRatio === undefined ? {} : { pressureRatio },
339
- ...valid2 && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
340
+ ...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
340
341
  ...valid2 && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
341
342
  };
342
343
  }
@@ -1224,6 +1225,61 @@ function performanceLines(row2, width) {
1224
1225
  ];
1225
1226
  }
1226
1227
 
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
+
1227
1283
  // src/tui/compact.tsx
1228
1284
  import { createTextNode as _$createTextNode } from "@opentui/solid";
1229
1285
  import { insertNode as _$insertNode } from "@opentui/solid";
@@ -1258,7 +1314,7 @@ function progress(ratio, width) {
1258
1314
  const filled = valid2 ? Math.round(Math.min(1, ratio) * size) : 0;
1259
1315
  return clip("\u2588".repeat(filled) + "\u2591".repeat(size - filled) + " " + label, width);
1260
1316
  }
1261
- function mainSections(row2, width, age) {
1317
+ function mainSections(row2, width, age, samples) {
1262
1318
  if (!row2)
1263
1319
  return [{ tone: "base", lines: wrap("Loading session family...", width) }];
1264
1320
  const s = row2.snapshot, m = s?.measurement;
@@ -1279,7 +1335,14 @@ function mainSections(row2, width, age) {
1279
1335
  ...columns("Context", `${tokens(measured)} / ${tokens(s?.capacityTokens)}`, width)
1280
1336
  ]
1281
1337
  },
1282
- { tone: "pressure", lines: [progress(ratio(s?.capacityTokens), width)] },
1338
+ ...contextChart(samples ?? (m && valid2 ? [{ at: m.at, tokens: m.tokens }] : []), s?.capacityTokens, s?.policy.triggerTokens, width).map(({ line, threshold }) => ({
1339
+ tone: threshold ? "threshold" : "pressure",
1340
+ lines: [line]
1341
+ })),
1342
+ {
1343
+ tone: "base",
1344
+ lines: wrap(`${ratio(s?.capacityTokens) === undefined ? "?" : Math.round((ratio(s?.capacityTokens) ?? 0) * 100)}% used \xB7 recent samples \u2192 now`, width)
1345
+ },
1283
1346
  {
1284
1347
  tone: "base",
1285
1348
  lines: [
@@ -1290,7 +1353,8 @@ function mainSections(row2, width, age) {
1290
1353
  {
1291
1354
  tone: "pressure",
1292
1355
  lines: [
1293
- progress(ratio(s?.policy.triggerTokens), width),
1356
+ ...wrap(`${ratio(s?.policy.triggerTokens) === undefined ? "?" : Math.round((ratio(s?.policy.triggerTokens) ?? 0) * 100)}% of threshold \xB7 \u2504 trigger`, width),
1357
+ ...s?.capacityTokens !== undefined && s.policy.triggerTokens !== undefined && s.policy.triggerTokens > s.capacityTokens ? wrap("Threshold above chart capacity", width) : [],
1294
1358
  ...wrap(remaining === undefined ? "Compaction distance unknown" : remaining < 0 ? `${tokens(-remaining)} tokens over threshold` : `${tokens(remaining)} tokens until compaction`, width)
1295
1359
  ]
1296
1360
  },
@@ -1408,6 +1472,7 @@ function CompactSidebar(props) {
1408
1472
  setCollapsed(new Set);
1409
1473
  });
1410
1474
  const current = createMemo(() => props.rows.find((r) => r.node.sessionId === props.sessionID));
1475
+ const trace = createMemo((previous) => updateContextTrace(previous, current()));
1411
1476
  const tree = createMemo(() => subagentTree(props.rows, props.sessionID, collapsed(), hideCompleted(), byPressure()));
1412
1477
  const entries = createMemo(() => new Map(tree().visible.map((entry) => [entry.row.node.sessionId, entry])));
1413
1478
  const toggle = (id) => setCollapsed((previous) => {
@@ -1497,7 +1562,7 @@ function CompactSidebar(props) {
1497
1562
  });
1498
1563
  _$insert(_el$2, _$createComponent(Index, {
1499
1564
  get each() {
1500
- return mainSections(current(), width(), props.state.age(props.sessionID));
1565
+ return mainSections(current(), width(), props.state.age(props.sessionID), trace().samples);
1501
1566
  },
1502
1567
  children: (section2) => _$createComponent(Index, {
1503
1568
  get each() {
@@ -1506,7 +1571,7 @@ function CompactSidebar(props) {
1506
1571
  children: (line) => (() => {
1507
1572
  var _el$13 = _$createElement("text");
1508
1573
  _$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));
1574
+ _$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
1575
  return _el$13;
1511
1576
  })()
1512
1577
  })
@@ -1652,6 +1717,8 @@ function GuardView(props) {
1652
1717
  onCleanup(() => consumer.release());
1653
1718
  const rows = createMemo2(() => props.family ?? props.state.rows([], !props.expanded));
1654
1719
  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));
1655
1722
  const detail = createMemo2(() => {
1656
1723
  const lines = detailLines(current(), props.width, props.expanded, props.state.age(props.sessionID));
1657
1724
  if (props.expanded) {
@@ -1719,26 +1786,48 @@ function GuardView(props) {
1719
1786
  _$insert2(_el$5, () => clip(`${props.state.connection()} / ${props.state.health()?.enforcement ?? "?"}`, props.width));
1720
1787
  _$insert2(_el$, _$createComponent2(For2, {
1721
1788
  get each() {
1722
- return detail();
1789
+ return chart();
1723
1790
  },
1724
- children: (line) => (() => {
1791
+ children: (item) => (() => {
1725
1792
  var _el$1 = _$createElement2("text");
1726
- _$insert2(_el$1, line);
1727
- _$effect2((_$p) => _$setProp2(_el$1, "fg", props.context.theme.text.base, _$p));
1793
+ _$insert2(_el$1, () => item.line);
1794
+ _$effect2((_$p) => _$setProp2(_el$1, "fg", item.threshold ? props.context.theme.text.feedback.warning.base : props.context.theme.text.muted, _$p));
1728
1795
  return _el$1;
1729
1796
  })()
1730
1797
  }), _el$6);
1731
1798
  _$insert2(_el$, _$createComponent2(For2, {
1732
1799
  get each() {
1733
- return props.state.health()?.reasons ?? [];
1800
+ return wrap("Recent context samples \u2192 now \xB7 \u2504 trigger", props.width);
1734
1801
  },
1735
- children: (reason) => (() => {
1802
+ children: (line) => (() => {
1736
1803
  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));
1804
+ _$insert2(_el$10, line);
1805
+ _$effect2((_$p) => _$setProp2(_el$10, "fg", props.context.theme.text.muted, _$p));
1739
1806
  return _el$10;
1740
1807
  })()
1741
1808
  }), _el$6);
1809
+ _$insert2(_el$, _$createComponent2(For2, {
1810
+ get each() {
1811
+ return detail();
1812
+ },
1813
+ 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;
1818
+ })()
1819
+ }), _el$6);
1820
+ _$insert2(_el$, _$createComponent2(For2, {
1821
+ get each() {
1822
+ return props.state.health()?.reasons ?? [];
1823
+ },
1824
+ 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;
1829
+ })()
1830
+ }), _el$6);
1742
1831
  _$insert2(_el$6, (() => {
1743
1832
  var _c$2 = _$memo2(() => props.state.health()?.mode === "observe");
1744
1833
  return () => _c$2() ? current()?.pressure === "crossed" ? "Observe: would compact" : "Observe: no admission" : "";
@@ -1752,9 +1841,9 @@ function GuardView(props) {
1752
1841
  return visible();
1753
1842
  },
1754
1843
  children: (row2) => (() => {
1755
- var _el$11 = _$createElement2("box");
1756
- _$setProp2(_el$11, "flexDirection", "column");
1757
- _$setProp2(_el$11, "onMouseDown", () => {
1844
+ var _el$13 = _$createElement2("box");
1845
+ _$setProp2(_el$13, "flexDirection", "column");
1846
+ _$setProp2(_el$13, "onMouseDown", () => {
1758
1847
  if (row2.hidden)
1759
1848
  props.state.expand(row2.node.sessionId);
1760
1849
  else {
@@ -1766,19 +1855,19 @@ function GuardView(props) {
1766
1855
  });
1767
1856
  }
1768
1857
  });
1769
- _$insert2(_el$11, _$createComponent2(For2, {
1858
+ _$insert2(_el$13, _$createComponent2(For2, {
1770
1859
  get each() {
1771
1860
  return rowLines(row2, Math.max(1, props.width - (props.expanded ? 2 : 0))).map((line) => props.expanded ? `${row2.node.sessionId === props.selectedId ? ">" : " "} ${line}` : line);
1772
1861
  },
1773
1862
  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;
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;
1778
1867
  })()
1779
1868
  }));
1780
- _$effect2((_$p) => _$setProp2(_el$11, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1781
- return _el$11;
1869
+ _$effect2((_$p) => _$setProp2(_el$13, "id", `context-guard.row.${row2.node.sessionId}`, _$p));
1870
+ return _el$13;
1782
1871
  })()
1783
1872
  }), _el$0);
1784
1873
  _$setProp2(_el$0, "onMouseDown", () => props.context.ui.panel.open("context-guard.monitor"));
@@ -1810,18 +1899,18 @@ function GuardView(props) {
1810
1899
 
1811
1900
  // src/tui/panel.tsx
1812
1901
  import { effect as _$effect4 } from "@opentui/solid";
1813
- import { createTextNode as _$createTextNode3 } from "@opentui/solid";
1814
- import { insertNode as _$insertNode3 } from "@opentui/solid";
1815
1902
  import { memo as _$memo3 } from "@opentui/solid";
1903
+ import { insertNode as _$insertNode4 } from "@opentui/solid";
1816
1904
  import { insert as _$insert4 } from "@opentui/solid";
1817
1905
  import { createComponent as _$createComponent4 } from "@opentui/solid";
1818
- import { setProp as _$setProp4 } from "@opentui/solid";
1819
1906
  import { use as _$use } from "@opentui/solid";
1907
+ import { setProp as _$setProp4 } from "@opentui/solid";
1820
1908
  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";
1909
+ import { createSignal as createSignal4, createMemo as createMemo3, createEffect as createEffect4, onCleanup as onCleanup3, Show as Show3, For as For4 } from "solid-js";
1822
1910
 
1823
1911
  // src/tui/charts.tsx
1824
1912
  import { effect as _$effect3 } from "@opentui/solid";
1913
+ import { insertNode as _$insertNode3 } from "@opentui/solid";
1825
1914
  import { insert as _$insert3 } from "@opentui/solid";
1826
1915
  import { createComponent as _$createComponent3 } from "@opentui/solid";
1827
1916
  import { setProp as _$setProp3 } from "@opentui/solid";
@@ -1829,7 +1918,7 @@ import { createElement as _$createElement3 } from "@opentui/solid";
1829
1918
  import { createEffect as createEffect3, createSignal as createSignal3, onCleanup as onCleanup2, For as For3 } from "solid-js";
1830
1919
 
1831
1920
  // src/tui/chartformat.ts
1832
- function sparkline(points, from, to, width, gaps = []) {
1921
+ function columns2(points, from, to, width, gaps = []) {
1833
1922
  const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
1834
1923
  const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
1835
1924
  let min = Infinity, max = -Infinity;
@@ -1841,6 +1930,13 @@ function sparkline(points, from, to, width, gaps = []) {
1841
1930
  const i = column(point.time);
1842
1931
  values[i] = Math.max(values[i] ?? -Infinity, point.value);
1843
1932
  }
1933
+ for (const time of gaps)
1934
+ if (time >= from && time <= to)
1935
+ values[column(time)] = undefined;
1936
+ return { values, column, min, max };
1937
+ }
1938
+ function sparkline(points, from, to, width, gaps = []) {
1939
+ const { values, column, min, max } = columns2(points, from, to, width, gaps);
1844
1940
  const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1845
1941
  const chars = values.map((v) => v === undefined ? " " : bars[Math.min(7, Math.floor(v / Math.max(1, max) * 7))] ?? "\u2581");
1846
1942
  for (const time of gaps)
@@ -1848,20 +1944,58 @@ function sparkline(points, from, to, width, gaps = []) {
1848
1944
  chars[column(time)] = "\u2506";
1849
1945
  return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
1850
1946
  }
1851
- function historyChartLines(result, width, from, to) {
1947
+ var chartMetrics = [
1948
+ { label: "Live TPS ~", unit: "t/s", key: "liveTps", source: "points" },
1949
+ { label: "Avg model", unit: "t/s", key: "averageModelTps", source: "points" },
1950
+ {
1951
+ label: "Avg session",
1952
+ unit: "t/s",
1953
+ key: "averageSessionTps",
1954
+ source: "points"
1955
+ },
1956
+ { label: "TTFT ~", unit: "s", key: "ttftMs", source: "requests" },
1957
+ { label: "Request throughput", unit: "t/s", key: "tps", source: "requests" },
1958
+ {
1959
+ label: "Cumulative output",
1960
+ unit: "tokens",
1961
+ key: "sessionOutputTokens",
1962
+ source: "points"
1963
+ }
1964
+ ];
1965
+ function metricPlot(points, from, to, width, gaps = []) {
1966
+ const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
1967
+ const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
1968
+ const scale = Math.max(1, peak);
1969
+ const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
1970
+ const { values, column } = columns2(points, from, to, Math.max(1, width - gutter - 2), gaps);
1971
+ const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
1972
+ const height = 6;
1973
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1974
+ const lines = Array.from({ length: height }, (_, row2) => {
1975
+ const axis = gutter ? (row2 === 0 ? label(scale) : row2 === height - 1 ? "0.0" : "").padStart(gutter) : "";
1976
+ return `${axis}\u2502${values.map((value, i) => {
1977
+ if (gapColumns.has(i))
1978
+ return "\u2506";
1979
+ if (value === undefined)
1980
+ return " ";
1981
+ const fill = Math.max(0, Math.min(8, Math.ceil((value / scale * height - (height - row2 - 1)) * 8)));
1982
+ return value === 0 && row2 === height - 1 ? "\u2581" : blocks[fill];
1983
+ }).join("")}\u2502`;
1984
+ });
1985
+ lines.push(`${" ".repeat(gutter)}\u2514${"\u2500".repeat(values.length)}\u2518`);
1986
+ const start = new Date(from).toISOString().slice(11, 16), end = new Date(to).toISOString().slice(11, 16);
1987
+ const axisWidth = values.length + 2;
1988
+ if (axisWidth >= 11)
1989
+ lines.push(`${" ".repeat(gutter)}${start}${" ".repeat(axisWidth - 10)}${end}`);
1990
+ return lines;
1991
+ }
1992
+ function historyChartLines(result, width, from, to, metricIndex = 0) {
1852
1993
  if (!result.enabled)
1853
1994
  return wrap("History recording is disabled on this server.", width);
1854
1995
  const series = result.data.series[0];
1855
1996
  if (!series)
1856
1997
  return wrap("No retained measurements in this window.", width);
1857
1998
  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
1999
  const observations = (key) => series.points.map((p) => ({
1866
2000
  time: p.time,
1867
2001
  ...p[key] === undefined ? {} : { value: p[key] }
@@ -1870,20 +2004,17 @@ function historyChartLines(result, width, from, to) {
1870
2004
  time: p.time,
1871
2005
  ...p[key] === undefined ? {} : { value: key === "ttftMs" ? p[key] / 1000 : p[key] }
1872
2006
  }));
2007
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2008
+ const points = metric2.source === "points" ? observations(metric2.key) : requests(metric2.key);
2009
+ const plotted = sparkline(points, from, to, width - 2, gaps);
1873
2010
  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")),
2011
+ ...wrap(`\u2039 ${metric2.label} \u203A \xB7 ${metricIndex + 1}/${chartMetrics.length}`, width),
2012
+ ...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
2013
  "",
1884
- ...chart("Cumulative output", "tokens", observations("sessionOutputTokens")),
2014
+ ...metricPlot(points, from, to, width, gaps),
1885
2015
  "",
1886
- ...wrap("UTC time \u2192 \xB7 observed peaks per column \xB7 blank = no sample \xB7 \u2506 = gap", width),
2016
+ ...wrap("UTC time \u2192 \xB7 peaks per column", width),
2017
+ ...wrap("blank = no sample \xB7 \u2506 = gap", width),
1887
2018
  ...wrap(`${new Date(from).toISOString()} \u2192 ${new Date(to).toISOString()}`, width),
1888
2019
  ...series.points.some((p) => p.partial) ? wrap("Partial timing coverage; averages include only matched model durations.", width) : [],
1889
2020
  ...result.truncated ? wrap("Downsampled/bounded history; some older requests or gap markers may be omitted.", width) : [],
@@ -1940,44 +2071,47 @@ function HistoryCharts(props) {
1940
2071
  });
1941
2072
  const lines = () => {
1942
2073
  const current = result();
1943
- return current ? historyChartLines(current, Math.max(4, props.width), range().from, range().to) : [];
2074
+ return current ? historyChartLines(current, Math.max(4, props.width), range().from, range().to, props.metricIndex) : [];
1944
2075
  };
1945
2076
  return (() => {
1946
- var _el$ = _$createElement3("box");
2077
+ var _el$ = _$createElement3("box"), _el$2 = _$createElement3("box");
2078
+ _$insertNode3(_el$, _el$2);
1947
2079
  _$setProp3(_el$, "flexDirection", "column");
1948
2080
  _$insert3(_el$, _$createComponent3(For3, {
1949
2081
  get each() {
1950
2082
  return wrap(`Performance charts \xB7 ${props.sessionId}`, props.width);
1951
2083
  },
1952
2084
  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;
2085
+ var _el$3 = _$createElement3("text");
2086
+ _$insert3(_el$3, line);
2087
+ _$effect3((_$p) => _$setProp3(_el$3, "fg", props.context.theme.text.base, _$p));
2088
+ return _el$3;
1957
2089
  })()
1958
- }), null);
2090
+ }), _el$2);
1959
2091
  _$insert3(_el$, _$createComponent3(For3, {
1960
2092
  get each() {
1961
2093
  return wrap(status(), props.width);
1962
2094
  },
1963
2095
  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;
2096
+ var _el$4 = _$createElement3("text");
2097
+ _$insert3(_el$4, line);
2098
+ _$effect3((_$p) => _$setProp3(_el$4, "fg", props.context.theme.text.muted, _$p));
2099
+ return _el$4;
1968
2100
  })()
1969
- }), null);
1970
- _$insert3(_el$, _$createComponent3(For3, {
2101
+ }), _el$2);
2102
+ _$setProp3(_el$2, "flexDirection", "column");
2103
+ _$setProp3(_el$2, "paddingTop", 1);
2104
+ _$insert3(_el$2, _$createComponent3(For3, {
1971
2105
  get each() {
1972
2106
  return lines();
1973
2107
  },
1974
2108
  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;
2109
+ var _el$5 = _$createElement3("text");
2110
+ _$insert3(_el$5, line || " ");
2111
+ _$effect3((_$p) => _$setProp3(_el$5, "fg", props.context.theme.text.base, _$p));
2112
+ return _el$5;
1979
2113
  })()
1980
- }), null);
2114
+ }));
1981
2115
  return _el$;
1982
2116
  })();
1983
2117
  }
@@ -1985,6 +2119,8 @@ function HistoryCharts(props) {
1985
2119
  // src/tui/panel.tsx
1986
2120
  function GuardPanel(props) {
1987
2121
  const [showCharts, setShowCharts] = createSignal4(false);
2122
+ const [metricIndex, setMetricIndex] = createSignal4(0);
2123
+ const width = () => Math.max(4, props.input.width - 4);
1988
2124
  const windows = [300000, 3600000, 86400000, 7 * 86400000];
1989
2125
  const [windowIndex, setWindowIndex] = createSignal4(1);
1990
2126
  const [chartRefresh, setChartRefresh] = createSignal4(0);
@@ -2034,6 +2170,27 @@ function GuardPanel(props) {
2034
2170
  props.context.keymap.layer(() => ({
2035
2171
  enabled: () => props.input.focused,
2036
2172
  commands: [{
2173
+ id: "context-guard.close",
2174
+ title: "Close Context Guard",
2175
+ bind: "q",
2176
+ run: () => props.input.close()
2177
+ }, {
2178
+ id: "context-guard.chart-previous",
2179
+ title: "Previous chart metric",
2180
+ bind: "left",
2181
+ run: () => {
2182
+ if (showCharts())
2183
+ setMetricIndex((i) => (i + chartMetrics.length - 1) % chartMetrics.length);
2184
+ }
2185
+ }, {
2186
+ id: "context-guard.chart-next",
2187
+ title: "Next chart metric",
2188
+ bind: "right",
2189
+ run: () => {
2190
+ if (showCharts())
2191
+ setMetricIndex((i) => (i + 1) % chartMetrics.length);
2192
+ }
2193
+ }, {
2037
2194
  id: "context-guard.charts",
2038
2195
  title: "Toggle performance charts",
2039
2196
  bind: "c",
@@ -2104,20 +2261,26 @@ function GuardPanel(props) {
2104
2261
  }
2105
2262
  }]
2106
2263
  }));
2264
+ 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
2265
  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);
2266
+ var _el$ = _$createElement4("box"), _el$2 = _$createElement4("scrollbox"), _el$3 = _$createElement4("box"), _el$4 = _$createElement4("text"), _el$5 = _$createElement4("box");
2267
+ _$insertNode4(_el$, _el$2);
2268
+ _$insertNode4(_el$, _el$5);
2269
+ _$setProp4(_el$, "flexDirection", "column");
2270
+ _$setProp4(_el$, "flexGrow", 1);
2271
+ _$setProp4(_el$, "paddingLeft", 2);
2272
+ _$setProp4(_el$, "paddingRight", 2);
2273
+ _$setProp4(_el$, "paddingTop", 1);
2274
+ _$setProp4(_el$, "paddingBottom", 1);
2275
+ _$insertNode4(_el$2, _el$3);
2115
2276
  _$use((value) => {
2116
2277
  scroll = value;
2117
- }, _el$);
2118
- _$setProp4(_el$, "flexGrow", 1);
2119
- _$setProp4(_el$2, "flexDirection", "column");
2120
- _$insert4(_el$2, _$createComponent4(GuardView, {
2278
+ }, _el$2);
2279
+ _$setProp4(_el$2, "flexGrow", 1);
2280
+ _$setProp4(_el$2, "minHeight", 0);
2281
+ _$insertNode4(_el$3, _el$4);
2282
+ _$setProp4(_el$3, "flexDirection", "column");
2283
+ _$insert4(_el$3, _$createComponent4(GuardView, {
2121
2284
  get sessionID() {
2122
2285
  return props.input.sessionID;
2123
2286
  },
@@ -2128,7 +2291,7 @@ function GuardPanel(props) {
2128
2291
  return props.context;
2129
2292
  },
2130
2293
  get width() {
2131
- return Math.max(10, props.input.width - 4);
2294
+ return width();
2132
2295
  },
2133
2296
  expanded: true,
2134
2297
  get family() {
@@ -2140,24 +2303,28 @@ function GuardPanel(props) {
2140
2303
  get inspectId() {
2141
2304
  return inspectId();
2142
2305
  }
2143
- }));
2144
- _$insert4(_el$, _$createComponent4(Show3, {
2306
+ }), _el$4);
2307
+ _$insert4(_el$4, () => wrap("Guard input = prompt + caches. Native Context uses different totals.", width()).join("\n"));
2308
+ _$insert4(_el$2, _$createComponent4(Show3, {
2145
2309
  get when() {
2146
2310
  return showCharts();
2147
2311
  },
2148
2312
  get children() {
2149
- return [_$createComponent4(HistoryCharts, {
2313
+ return _$createComponent4(HistoryCharts, {
2150
2314
  get sessionId() {
2151
2315
  return selected();
2152
2316
  },
2153
2317
  get windowMs() {
2154
2318
  return windows[windowIndex()] ?? 3600000;
2155
2319
  },
2320
+ get metricIndex() {
2321
+ return metricIndex();
2322
+ },
2156
2323
  get refresh() {
2157
2324
  return chartRefresh();
2158
2325
  },
2159
2326
  get width() {
2160
- return Math.max(4, props.input.width - 6);
2327
+ return width();
2161
2328
  },
2162
2329
  get state() {
2163
2330
  return props.state;
@@ -2165,38 +2332,31 @@ function GuardPanel(props) {
2165
2332
  get context() {
2166
2333
  return props.context;
2167
2334
  }
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
- })()];
2335
+ });
2175
2336
  }
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.`));
2337
+ }), null);
2338
+ _$setProp4(_el$5, "flexDirection", "column");
2339
+ _$setProp4(_el$5, "flexShrink", 0);
2340
+ _$setProp4(_el$5, "paddingTop", 1);
2341
+ _$insert4(_el$5, _$createComponent4(For4, {
2342
+ get each() {
2343
+ return hints().flatMap((hint) => wrap(hint, width()));
2344
+ },
2345
+ children: (line) => (() => {
2346
+ var _el$6 = _$createElement4("text");
2347
+ _$insert4(_el$6, line);
2348
+ _$effect4((_$p) => _$setProp4(_el$6, "fg", props.context.theme.text.muted, _$p));
2349
+ return _el$6;
2350
+ })()
2351
+ }));
2184
2352
  _$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));
2353
+ var _v$ = !showCharts(), _v$2 = props.context.theme.text.muted;
2354
+ _v$ !== _p$.e && (_p$.e = _$setProp4(_el$3, "visible", _v$, _p$.e));
2355
+ _v$2 !== _p$.t && (_p$.t = _$setProp4(_el$4, "fg", _v$2, _p$.t));
2192
2356
  return _p$;
2193
2357
  }, {
2194
2358
  e: undefined,
2195
- t: undefined,
2196
- a: undefined,
2197
- o: undefined,
2198
- i: undefined,
2199
- n: undefined
2359
+ t: undefined
2200
2360
  });
2201
2361
  return _el$;
2202
2362
  })();
@@ -2245,7 +2405,7 @@ var tui_default = Plugin.define({
2245
2405
  },
2246
2406
  get children() {
2247
2407
  var _el$ = _$createElement5("text");
2248
- _$insertNode4(_el$, _$createTextNode4(`Context Guard: open panel`));
2408
+ _$insertNode5(_el$, _$createTextNode3(`Context Guard: open panel`));
2249
2409
  _$setProp5(_el$, "onMouseDown", () => context.ui.panel.open("context-guard.monitor"));
2250
2410
  _$effect5((_$p) => _$setProp5(_el$, "fg", context.theme.text.muted, _$p));
2251
2411
  return _el$;
@@ -12,7 +12,7 @@ export interface Host {
12
12
  location: {
13
13
  directory: string;
14
14
  };
15
- session: Pick<Plugin.Context["session"], "get" | "compact" | "hook">;
15
+ session: Pick<Plugin.Context["session"], "get" | "compact" | "hook"> & Partial<Pick<Plugin.Context["session"], "context">>;
16
16
  model: Pick<Plugin.Context["model"], "list">;
17
17
  event: Plugin.Context["event"];
18
18
  storage: Pick<Plugin.Context["storage"], "get" | "set">;
@@ -4,4 +4,4 @@ export interface SnapshotRpcPort {
4
4
  }
5
5
  import { monitorRpc, performanceRpc } from "./rpc.js";
6
6
  import type { SnapshotStore } from "./snapshot.js";
7
- export declare function serveSnapshots(rpc: SnapshotRpcPort | undefined, store: SnapshotStore, performance?: boolean): Promise<() => void>;
7
+ export declare function serveSnapshots(rpc: SnapshotRpcPort | undefined, store: SnapshotStore, performance?: boolean, prepareRead?: (ids: readonly string[], signal: AbortSignal) => Promise<void>): Promise<() => void>;
@@ -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.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {