@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 +6 -4
- package/dist/core.js +2 -1
- package/dist/index.js +80 -8
- package/dist/tui.js +261 -128
- package/dist/types/adapter.d.ts +1 -1
- package/dist/types/snapshotserver.d.ts +1 -1
- package/dist/types/tui/compactformat.d.ts +3 -2
- package/dist/types/tui/contextchart.d.ts +19 -3
- package/dist/types/tui/state.d.ts +4 -2
- package/package.json +1 -1
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**.
|
|
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.
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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((
|
|
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(() => [
|
|
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, (
|
|
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$
|
|
1584
|
-
_$insert(_el$
|
|
1585
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1586
|
-
return _el$
|
|
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$
|
|
1613
|
-
_$insertNode(_el$
|
|
1614
|
-
_$insertNode(_el$
|
|
1615
|
-
_$setProp(_el$
|
|
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
|
-
_$
|
|
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$
|
|
1626
|
-
_$setProp(_el$
|
|
1627
|
-
_$insert(_el$
|
|
1628
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1629
|
-
return _el$
|
|
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$
|
|
1632
|
-
_$insertNode(_el$
|
|
1633
|
-
_$insertNode(_el$
|
|
1634
|
-
_$setProp(_el$
|
|
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$
|
|
1639
|
-
_$insert(_el$
|
|
1640
|
-
_$insert(_el$
|
|
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$
|
|
1646
|
-
_$insertNode(_el$
|
|
1647
|
-
_$insert(_el$
|
|
1648
|
-
_$insert(_el$
|
|
1649
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1650
|
-
return _el$
|
|
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$
|
|
1653
|
-
_$insertNode(_el$
|
|
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$
|
|
1659
|
-
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
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$
|
|
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((
|
|
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(() =>
|
|
1783
|
-
return () => _c$() ? "" : clip(`Runtime ${props.context.app.version}: unverified (2.0.29
|
|
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, (
|
|
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$
|
|
1803
|
-
_$insert2(_el$
|
|
1804
|
-
_$effect2((_$p) => _$setProp2(_el$
|
|
1805
|
-
return _el$
|
|
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$
|
|
1814
|
-
_$insert2(_el$
|
|
1815
|
-
_$effect2((_$p) => _$setProp2(_el$
|
|
1816
|
-
return _el$
|
|
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$
|
|
1825
|
-
_$insert2(_el$
|
|
1826
|
-
_$effect2((_$p) => _$setProp2(_el$
|
|
1827
|
-
return _el$
|
|
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$
|
|
1844
|
-
_$setProp2(_el$
|
|
1845
|
-
_$setProp2(_el$
|
|
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$
|
|
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$
|
|
1863
|
-
_$insert2(_el$
|
|
1864
|
-
_$effect2((_$p) => _$setProp2(_el$
|
|
1865
|
-
return _el$
|
|
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$
|
|
1869
|
-
return _el$
|
|
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
|
|
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);
|
package/dist/types/adapter.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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) =>
|
|
10
|
-
|
|
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";
|