opencode-context-indicator 1.1.6 → 1.2.1
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 +95 -39
- package/index.js +8 -5
- package/lib/commands.js +15 -0
- package/lib/contributors.js +288 -0
- package/lib/limits.js +80 -23
- package/lib/state.js +16 -1
- package/package.json +3 -3
- package/tui.tsx +502 -30
package/lib/limits.js
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
// client.config.providers(), ensureModelLimits for V2 via ctx.model.list()).
|
|
7
7
|
// hydrateModelLimitsFromState() seeds the maps from the shared state.json
|
|
8
8
|
// snapshot via ./state.js. Extracted 1:1 (code + WHY comments) from index.js.
|
|
9
|
+
//
|
|
10
|
+
// Compaction ceiling (v2.0.23): opencode's core calculateCeiling() in
|
|
11
|
+
// packages/core/src/session/compaction.ts derives the auto-compaction trigger
|
|
12
|
+
// ceiling as window - reserve, where window = limit.input || limit.context
|
|
13
|
+
// (null/zero falls through to the context window) and
|
|
14
|
+
// reserve = max(floor(window * 0.1), window >= 32_000 ? 16_000 : 0)
|
|
15
|
+
// A compaction.buffer configured in opencode itself is NOT visible to the
|
|
16
|
+
// plugin (the plugin cannot read opencode's internal compaction config); the
|
|
17
|
+
// env var OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER overrides the reserve for
|
|
18
|
+
// testing and power users only (positive finite integer <= 1e9, ignore invalid).
|
|
9
19
|
|
|
10
20
|
import { finitePos, withTimeout } from "./estimate.js";
|
|
11
21
|
import { logErr, readStateSnapshot } from "./state.js";
|
|
@@ -67,32 +77,73 @@ function getModelLimit(providerID, modelID) {
|
|
|
67
77
|
return typeof rec.context === "number" && rec.context > 0 ? rec.context : null;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
// Compute the reserve for a given window, matching opencode v2.0.23
|
|
81
|
+
// packages/core/src/session/compaction.ts calculateCeiling(). Returns null when
|
|
82
|
+
// window is not a finite positive number.
|
|
83
|
+
function reserveFor(window, bufferOverride) {
|
|
84
|
+
if (
|
|
85
|
+
typeof window !== "number" ||
|
|
86
|
+
!Number.isFinite(window) ||
|
|
87
|
+
window <= 0
|
|
88
|
+
) return null;
|
|
89
|
+
// bufferOverride: accept only finite, non-negative integers <= 1e9.
|
|
90
|
+
if (
|
|
91
|
+
typeof bufferOverride === "number" &&
|
|
92
|
+
Number.isFinite(bufferOverride) &&
|
|
93
|
+
bufferOverride >= 0 &&
|
|
94
|
+
bufferOverride <= 1e9
|
|
95
|
+
) {
|
|
96
|
+
return bufferOverride;
|
|
97
|
+
}
|
|
98
|
+
// Default reserve formula (mirror of opencode core):
|
|
99
|
+
// max(floor(window * 0.1), window >= 32_000 ? 16_000 : 0)
|
|
100
|
+
return Math.max(
|
|
101
|
+
Math.floor(window * 0.1),
|
|
102
|
+
window >= 32_000 ? 16_000 : 0,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Native opencode compaction ceiling (v2.0.23 calculateCeiling):
|
|
107
|
+
// window = limit.input || limit.context (zero/null falls through to context)
|
|
108
|
+
// reserve = reserveFor(window) [or bufferOverride when provided]
|
|
109
|
+
// usable = window - reserve
|
|
110
|
+
// Returns { usable, limit: window, maxOutput, reserved } or null when not
|
|
111
|
+
// derivable (callers then degrade to count / limit.context, or no percentage).
|
|
112
|
+
// The per-model compaction.reserved and the old min(20000, maxOutput) no longer
|
|
113
|
+
// participate in this calculation; opencode's internal buffer is not visible.
|
|
114
|
+
function getUsableContext(providerID, modelID, bufferOverride) {
|
|
77
115
|
const rec = getModelLimitRecord(providerID, modelID);
|
|
78
116
|
if (!rec) return null;
|
|
79
117
|
const context =
|
|
80
118
|
typeof rec.context === "number" && rec.context > 0 ? rec.context : null;
|
|
81
119
|
const input =
|
|
82
120
|
typeof rec.input === "number" && rec.input > 0 ? rec.input : null;
|
|
121
|
+
if (input == null && context == null) return null;
|
|
122
|
+
// Mirror core's limit.input || limit.context (zero/null falls through to context).
|
|
123
|
+
const window = input != null && input > 0 ? input : context;
|
|
124
|
+
if (window == null || !(window > 0)) return null;
|
|
125
|
+
|
|
126
|
+
// When bufferOverride is omitted, read it from the env var once per call.
|
|
127
|
+
let resolvedBuffer = bufferOverride;
|
|
128
|
+
if (resolvedBuffer === undefined) {
|
|
129
|
+
const raw = process.env.OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER;
|
|
130
|
+
// Strict digits-only: reject "5000abc", "5e3", " 5000", "", "-1" and other
|
|
131
|
+
// partial parses (parseInt/parseFloat stop at the first non-digit).
|
|
132
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) {
|
|
133
|
+
const parsed = parseInt(raw, 10);
|
|
134
|
+
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1e9) {
|
|
135
|
+
resolvedBuffer = parsed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const reserve = reserveFor(window, resolvedBuffer);
|
|
141
|
+
if (reserve == null) return null;
|
|
142
|
+
const usable = window - reserve;
|
|
143
|
+
if (usable <= 0) return null;
|
|
83
144
|
const maxOutput =
|
|
84
145
|
typeof rec.output === "number" && rec.output > 0 ? rec.output : 0;
|
|
85
|
-
|
|
86
|
-
// Mirrors opencode overflow.ts: reserved = min(20000, maxOutput), i.e. 0 when
|
|
87
|
-
// the model declares no output limit (NOT a 20000 default — that would
|
|
88
|
-
// understate the usable window).
|
|
89
|
-
const reserved =
|
|
90
|
-
typeof rec.reserved === "number" && rec.reserved >= 0
|
|
91
|
-
? rec.reserved
|
|
92
|
-
: Math.min(20000, maxOutput);
|
|
93
|
-
const usable = input != null ? input - reserved : context - maxOutput;
|
|
94
|
-
if (!(usable > 0)) return null;
|
|
95
|
-
return { usable, limit: context, maxOutput, reserved };
|
|
146
|
+
return { usable, limit: window, maxOutput, reserved: reserve };
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
// Store a model limit record under `<providerID>:<modelID>`. Written by
|
|
@@ -366,13 +417,18 @@ function hydrateModelLimitsFromState() {
|
|
|
366
417
|
}
|
|
367
418
|
}
|
|
368
419
|
|
|
369
|
-
// Build the record:
|
|
370
|
-
//
|
|
420
|
+
// Build the record: seed the raw window (context, preferring limit when
|
|
421
|
+
// present) so the NEW formula derives the ceiling fresh on every call.
|
|
422
|
+
// The old precise-usable-as-input seeding (which caused double-reserve
|
|
423
|
+
// under the new formula) is retired: getUsableContext is now a pure
|
|
424
|
+
// function of the window and needs no pre-derived state.
|
|
371
425
|
let rec = null;
|
|
372
|
-
if (best.
|
|
373
|
-
rec = { context: best.limit, input: best.usable, output: null, reserved: 0 };
|
|
374
|
-
} else if (best.limit != null) {
|
|
426
|
+
if (best.limit != null) {
|
|
375
427
|
rec = { context: best.limit, input: null, output: null, reserved: null };
|
|
428
|
+
} else if (best.usable != null) {
|
|
429
|
+
// No limit in snapshot: fall back to persisted usable as a best-effort
|
|
430
|
+
// window (it was the ceiling before the formula change, close enough).
|
|
431
|
+
rec = { context: best.usable, input: null, output: null, reserved: null };
|
|
376
432
|
}
|
|
377
433
|
if (rec == null) continue;
|
|
378
434
|
modelLimits.set(key, rec);
|
|
@@ -396,6 +452,7 @@ export {
|
|
|
396
452
|
modelKey,
|
|
397
453
|
getModelLimit,
|
|
398
454
|
getUsableContext,
|
|
455
|
+
reserveFor,
|
|
399
456
|
setModelLimit,
|
|
400
457
|
ensureProviderLimits,
|
|
401
458
|
ensureModelLimits,
|
package/lib/state.js
CHANGED
|
@@ -331,14 +331,23 @@ function writeStateFile(
|
|
|
331
331
|
}
|
|
332
332
|
try {
|
|
333
333
|
const bd = breakdownCache.get(sessionID) || null;
|
|
334
|
-
const limit = getModelLimit(providerID, modelID);
|
|
335
334
|
const usableInfo = getUsableContext(providerID, modelID);
|
|
335
|
+
// Persist the EFFECTIVE window (limit.input || limit.context — the same
|
|
336
|
+
// window the ceiling was derived from), not the raw context window: on a
|
|
337
|
+
// cold instance hydrateModelLimitsFromState seeds context = limit, so a
|
|
338
|
+
// mismatched limit would re-derive a WRONG (too large) ceiling for models
|
|
339
|
+
// where limit.input < limit.context.
|
|
340
|
+
const limit =
|
|
341
|
+
usableInfo != null && usableInfo.limit != null
|
|
342
|
+
? usableInfo.limit
|
|
343
|
+
: getModelLimit(providerID, modelID);
|
|
336
344
|
const denom =
|
|
337
345
|
usableInfo != null
|
|
338
346
|
? usableInfo.usable
|
|
339
347
|
: limit != null && limit > 0
|
|
340
348
|
? limit
|
|
341
349
|
: null;
|
|
350
|
+
const reserve = usableInfo != null ? usableInfo.reserved : null;
|
|
342
351
|
const total =
|
|
343
352
|
typeof ctxTokens === "number" && ctxTokens > 0
|
|
344
353
|
? ctxTokens
|
|
@@ -433,6 +442,12 @@ function writeStateFile(
|
|
|
433
442
|
: sameModelAsPrev && finitePos(prevEntry.usable)
|
|
434
443
|
? prevEntry.usable
|
|
435
444
|
: null,
|
|
445
|
+
reserve:
|
|
446
|
+
reserve != null
|
|
447
|
+
? reserve
|
|
448
|
+
: sameModelAsPrev && finitePos(prevEntry.reserve)
|
|
449
|
+
? prevEntry.reserve
|
|
450
|
+
: null,
|
|
436
451
|
limit:
|
|
437
452
|
limit != null
|
|
438
453
|
? limit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context-indicator",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Real-time OpenCode context-window usage indicator: per-category token breakdown written to a log file, plus an optional TUI sidebar. Dual V1 + V2 plugin entry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@opentui/solid": "0.5.
|
|
59
|
+
"@opentui/solid": "0.5.16"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@opencode/plugin": "^2.0.19",
|
|
63
|
-
"@opentui/core": "0.5.
|
|
63
|
+
"@opentui/core": "0.5.16",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"node-pty": "^1.1.0",
|
|
66
66
|
"solid-js": "^1.9.12"
|