@cesarus777/opencode-cgp 0.2.2 → 0.2.5

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