opencode-context-indicator 1.7.0 → 1.8.0
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/CHANGELOG.md +106 -12
- package/README.md +102 -96
- package/index.js +6 -6
- package/lib/contributors.js +5 -4
- package/lib/limits.js +27 -15
- package/lib/state.js +6 -5
- package/lib/tui-data.d.ts +0 -1
- package/lib/tui-data.js +5 -21
- package/lib/tui-h.ts +73 -0
- package/lib/{tui-panel.tsx → tui-panel.ts} +779 -713
- package/lib/tui-sidebar.ts +153 -0
- package/lib/v2.js +56 -10
- package/package.json +10 -12
- package/tui.ts +317 -0
- package/lib/tui-sidebar.tsx +0 -76
- package/tui.tsx +0 -206
package/lib/limits.js
CHANGED
|
@@ -10,17 +10,21 @@
|
|
|
10
10
|
// Compaction ceiling (opencode calculateCeiling): opencode's core
|
|
11
11
|
// calculateCeiling() in packages/core/src/session/compaction.ts derives the
|
|
12
12
|
// auto-compaction trigger ceiling as window - reserve, where
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
13
|
+
// window = input && context ? min(input, context) : input || context
|
|
14
|
+
// (null/zero falls through; since opencode 2.2.5 (#60660) an input limit ABOVE
|
|
15
|
+
// the context window is capped at the context window, because the context also
|
|
16
|
+
// holds the reply) and
|
|
17
|
+
// reserve = max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)
|
|
18
|
+
// `RESERVE_SHARE = 0.15` in every RELEASED host from 2.1.4 through 2.2.5
|
|
19
|
+
// (verified tag by tag; the `2 * RESERVE_MIN` gate is older and unchanged). The
|
|
20
|
+
// unreleased `v2` branch HEAD has an inline `0.1` instead - do NOT mirror a
|
|
21
|
+
// branch head: a wrong reserve makes every displayed percentage wrong.
|
|
20
22
|
// A compaction.buffer configured in opencode itself is NOT visible to the
|
|
21
23
|
// plugin (the plugin cannot read opencode's internal compaction config); the
|
|
22
24
|
// env var OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER overrides the reserve for
|
|
23
25
|
// testing and power users only (non-negative finite integer <= 1e9, ignore invalid).
|
|
26
|
+
// opencode 2.2.5 also accepts a percentage there ("buffer": "20%"), which is
|
|
27
|
+
// likewise invisible here, so a percentage buffer shows the default reserve.
|
|
24
28
|
// Like core, a buffer that is NOT smaller than the window is ignored: a buffer
|
|
25
29
|
// set for a larger model must never leave zero room.
|
|
26
30
|
|
|
@@ -85,11 +89,11 @@ function getModelLimit(providerID, modelID) {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
// Compute the reserve for a given window, matching opencode's
|
|
88
|
-
// packages/core/src/session/compaction.ts calculateCeiling()
|
|
89
|
-
//
|
|
92
|
+
// packages/core/src/session/compaction.ts calculateCeiling() as RELEASED
|
|
93
|
+
// (RESERVE_SHARE = 0.15 in 2.1.4 .. 2.2.5; the branch head is not a release).
|
|
90
94
|
// Returns null when
|
|
91
95
|
// window is not a finite positive number.
|
|
92
|
-
const RESERVE_SHARE = 0.
|
|
96
|
+
const RESERVE_SHARE = 0.15; // core: RESERVE_SHARE in every released 2.x
|
|
93
97
|
const RESERVE_MIN = 16_000; // core: RESERVE_MIN
|
|
94
98
|
function reserveFor(window, bufferOverride) {
|
|
95
99
|
if (
|
|
@@ -109,7 +113,7 @@ function reserveFor(window, bufferOverride) {
|
|
|
109
113
|
return bufferOverride;
|
|
110
114
|
}
|
|
111
115
|
// Default reserve formula (mirror of opencode core):
|
|
112
|
-
// max(floor(window * 0.
|
|
116
|
+
// max(floor(window * 0.15), window >= 2 * RESERVE_MIN ? RESERVE_MIN : 0)
|
|
113
117
|
// The threshold is 2 * RESERVE_MIN (32_000), not RESERVE_MIN.
|
|
114
118
|
return Math.max(
|
|
115
119
|
Math.floor(window * RESERVE_SHARE),
|
|
@@ -117,8 +121,8 @@ function reserveFor(window, bufferOverride) {
|
|
|
117
121
|
);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
// Native opencode compaction ceiling (calculateCeiling,
|
|
121
|
-
// window =
|
|
124
|
+
// Native opencode compaction ceiling (calculateCeiling, released 2.x):
|
|
125
|
+
// window = input && context ? min(input, context) : input || context
|
|
122
126
|
// reserve = reserveFor(window) [or bufferOverride when provided]
|
|
123
127
|
// usable = window - reserve
|
|
124
128
|
// Returns { usable, limit: window, maxOutput, reserved } or null when not
|
|
@@ -127,6 +131,11 @@ function reserveFor(window, bufferOverride) {
|
|
|
127
131
|
// participate in this calculation; opencode's internal buffer is not visible.
|
|
128
132
|
// The buffer override (env or argument) replaces the whole reserve only while
|
|
129
133
|
// it is smaller than the window — a buffer >= window falls back to the share.
|
|
134
|
+
//
|
|
135
|
+
// The min() is opencode 2.2.5 (#60660): a provider that reports an input limit
|
|
136
|
+
// ABOVE the context window cannot use it in full, because the context also holds
|
|
137
|
+
// the reply. Using input alone would overstate the denominator (and show a
|
|
138
|
+
// compact-at point the host never reaches).
|
|
130
139
|
function getUsableContext(providerID, modelID, bufferOverride) {
|
|
131
140
|
const rec = getModelLimitRecord(providerID, modelID);
|
|
132
141
|
if (!rec) return null;
|
|
@@ -135,8 +144,11 @@ function getUsableContext(providerID, modelID, bufferOverride) {
|
|
|
135
144
|
const input =
|
|
136
145
|
typeof rec.input === "number" && rec.input > 0 ? rec.input : null;
|
|
137
146
|
if (input == null && context == null) return null;
|
|
138
|
-
// Mirror core's
|
|
139
|
-
|
|
147
|
+
// Mirror core's inputWindow(): min(input, context) when both are known.
|
|
148
|
+
// The `??` is core's `||` here: a non-positive input was normalized to null
|
|
149
|
+
// above, so there is no 0 to fall through.
|
|
150
|
+
const window =
|
|
151
|
+
input != null && context != null ? Math.min(input, context) : input ?? context;
|
|
140
152
|
if (window == null || !(window > 0)) return null;
|
|
141
153
|
|
|
142
154
|
// When bufferOverride is omitted, read it from the env var once per call.
|
package/lib/state.js
CHANGED
|
@@ -60,7 +60,7 @@ const LOG_FILE =
|
|
|
60
60
|
process.env.OPENCODE_CONTEXT_INDICATOR_LOG_FILE ||
|
|
61
61
|
join(tmpdir(), "context-breakdown.log");
|
|
62
62
|
|
|
63
|
-
// Machine-readable snapshot consumed by the optional TUI sidebar (./tui.
|
|
63
|
+
// Machine-readable snapshot consumed by the optional TUI sidebar (./tui.ts,
|
|
64
64
|
// package entry "./tui"). Written next to each log update — same moment, same
|
|
65
65
|
// data. See writeStateFile() below.
|
|
66
66
|
// State file path — overridable via env for test isolation.
|
|
@@ -417,7 +417,7 @@ function renameWithRetry(from, to, attempts = 3, delayMs = 50) {
|
|
|
417
417
|
// TUI state bridge (opencode-context-indicator)
|
|
418
418
|
// ---------------------------------------------------------------------------
|
|
419
419
|
// Persist a tiny machine-readable snapshot of the CURRENT breakdown for one
|
|
420
|
-
// session so the optional TUI sidebar (./tui.
|
|
420
|
+
// session so the optional TUI sidebar (./tui.ts, package entry "./tui") can
|
|
421
421
|
// render it without talking to the server. The file is one JSON object keyed by
|
|
422
422
|
// sessionID, rewritten atomically (temp + rename) at every log write — same
|
|
423
423
|
// moment, same data. Strictly additive: any failure is swallowed and must never
|
|
@@ -460,9 +460,10 @@ function writeStateFile(
|
|
|
460
460
|
try {
|
|
461
461
|
const bd = breakdownCache.get(sessionID) || null;
|
|
462
462
|
const usableInfo = getUsableContext(providerID, modelID);
|
|
463
|
-
// Persist the EFFECTIVE window (
|
|
464
|
-
// window the ceiling was derived from),
|
|
465
|
-
//
|
|
463
|
+
// Persist the EFFECTIVE window (min(input, context) when both are known,
|
|
464
|
+
// else input || context: the same window the ceiling was derived from),
|
|
465
|
+
// not the raw context window: on a cold instance
|
|
466
|
+
// hydrateModelLimitsFromState seeds context = limit, so a
|
|
466
467
|
// mismatched limit would re-derive a WRONG (too large) ceiling for models
|
|
467
468
|
// where limit.input < limit.context.
|
|
468
469
|
const limit =
|
package/lib/tui-data.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export interface StateEntry {
|
|
|
54
54
|
|
|
55
55
|
export declare const STATE_FILE: string
|
|
56
56
|
export declare const POLL_MS: number
|
|
57
|
-
export declare function isNodeModulesInstall(): boolean
|
|
58
57
|
export declare function readSnapshot(): Record<string, StateEntry>
|
|
59
58
|
export declare function collectFamily(
|
|
60
59
|
snapshot: Record<string, StateEntry>,
|
package/lib/tui-data.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// lib/tui-data.js
|
|
2
2
|
//
|
|
3
|
-
// Data layer for the TUI entry (tui.
|
|
3
|
+
// Data layer for the TUI entry (tui.ts): the state.json bridge (readSnapshot /
|
|
4
4
|
// isEmptyEntry / collectFamily), the shared formatting helpers (fmt / num / fit
|
|
5
|
-
// / updatedAt) and the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// / updatedAt) and the family walk. Plain JS with JSDoc — same shape as
|
|
6
|
+
// lib/contributors.js — so tui.ts stays a thin setup shell and the node tooling
|
|
7
|
+
// (resolve gate, unit tests) can import it without a transform.
|
|
8
8
|
//
|
|
9
9
|
// These helpers must stay free of solid-js / @opentui imports: the TUI host
|
|
10
|
-
// bridges those modules only for the
|
|
10
|
+
// bridges those modules only for the plugin bundle, and keeping this file pure
|
|
11
11
|
// means it also loads anywhere node does.
|
|
12
12
|
//
|
|
13
13
|
// state.json shape (written by writeStateFile() in index.js, untrusted input):
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
23
|
import { isSafeSessionKey, sumCategories } from "./estimate.js";
|
|
25
24
|
import { fmt, num } from "./format.js";
|
|
26
25
|
|
|
@@ -38,21 +37,6 @@ export const STATE_FILE =
|
|
|
38
37
|
|
|
39
38
|
export const POLL_MS = 1000;
|
|
40
39
|
|
|
41
|
-
// True when this package was loaded from an npm install, i.e. its own file
|
|
42
|
-
// path contains a `node_modules` segment (OpenCode's TUI loader skips the host
|
|
43
|
-
// Solid transform for such paths → slots mount once but never live-update; see
|
|
44
|
-
// the Graceful mode note in tui.tsx). Normalised to forward slashes for
|
|
45
|
-
// Windows paths. ANY file of the package yields the same answer, so checking
|
|
46
|
-
// from lib/ is equivalent to checking from the tui.tsx entry.
|
|
47
|
-
export function isNodeModulesInstall() {
|
|
48
|
-
try {
|
|
49
|
-
const selfPath = fileURLToPath(import.meta.url).replace(/\\/g, "/").toLowerCase()
|
|
50
|
-
return selfPath.includes("/node_modules/")
|
|
51
|
-
} catch {
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
40
|
// Whole state.json snapshot ({ sessionID: entry }) or {} on any error. The
|
|
57
41
|
// sidebar resolves one entry from it and the breakdown panel walks the
|
|
58
42
|
// parentID family over it. Never throws — a missing / empty / corrupt
|
package/lib/tui-h.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// lib/tui-h.ts
|
|
2
|
+
//
|
|
3
|
+
// The two tree builders every TUI surface in this package uses instead of JSX.
|
|
4
|
+
//
|
|
5
|
+
// WHY NO JSX (do not "simplify" this back):
|
|
6
|
+
// OpenCode installs npm plugins inside a `node_modules` tree
|
|
7
|
+
// (~/.cache/opencode/npm/<name>@<version>/<ts>/node_modules/<name>). For such
|
|
8
|
+
// paths the host deliberately skips its Solid/Babel JSX transform (sourceFilter
|
|
9
|
+
// in @opentui/solid/scripts/solid-plugin.js excludes node_modules, upstream
|
|
10
|
+
// anomalyco/opencode#33884). A JSX-compiled file in that position also needs
|
|
11
|
+
// `@opentui/solid` to be installed next to it just to resolve its emitted
|
|
12
|
+
// jsx-runtime import. Files WITHOUT JSX syntax are unaffected: their literal
|
|
13
|
+
// `@opentui/solid/jsx-runtime` import IS rewritten by the host to the
|
|
14
|
+
// in-process OpenTUI module (`nodeModulesRuntimeSpecifiers: true` in
|
|
15
|
+
// DEFAULT_RUNTIME_PLUGIN_REWRITE_OPTIONS of @opentui/core), so the tree stays
|
|
16
|
+
// reactive and the package needs no runtime dependency at all.
|
|
17
|
+
//
|
|
18
|
+
// Therefore: no JSX syntax anywhere in this package's TUI files, and no
|
|
19
|
+
// `@jsxImportSource` pragma. Build the tree with jsx() and mark every DYNAMIC
|
|
20
|
+
// prop as a getter — that is exactly what the Solid compiler emits for
|
|
21
|
+
// `prop={expr}` in JSX, so the runtime semantics are identical:
|
|
22
|
+
//
|
|
23
|
+
// <text fg={base()}>{line}</text>
|
|
24
|
+
// -> h("text", { get fg() { return base() }, get children() { return line } })
|
|
25
|
+
//
|
|
26
|
+
// A plain (non-getter) value is read ONCE, like a literal attribute in JSX.
|
|
27
|
+
// Children follow the same rule: an array of elements is fine (it is diffed by
|
|
28
|
+
// the runtime), a function child is a reactive accessor.
|
|
29
|
+
//
|
|
30
|
+
// RELATIVE IMPORTS OF .ts FILES MUST CARRY THE EXTENSION (`./tui-h.ts`, not
|
|
31
|
+
// `./tui-h`): the host decides which files get the runtime-specifier rewrite by
|
|
32
|
+
// walking the import graph with `import.meta.resolve(specifier, importer)`,
|
|
33
|
+
// which does not probe extensions. An extensionless import is simply not
|
|
34
|
+
// followed, so that file keeps the literal `@opentui/solid/jsx-runtime` import
|
|
35
|
+
// and the npm install dies with `Cannot find package '@opentui/solid'`.
|
|
36
|
+
// test/prepublish.mjs gate (b) enforces this.
|
|
37
|
+
//
|
|
38
|
+
// This module is the only place that imports the jsx runtime, so the allowlist
|
|
39
|
+
// in test/prepublish.mjs stays short.
|
|
40
|
+
//
|
|
41
|
+
// NEVER pass a `key` prop to h()/c(). The jsx runtime's normalizeProps() sees
|
|
42
|
+
// `key`, strips it with `{ ...rest }`, and the spread turns every getter into a
|
|
43
|
+
// plain value read ONCE — the whole subtree freezes with no error. (No list in
|
|
44
|
+
// this package is keyed; if you ever need one, do not add `key` here.)
|
|
45
|
+
|
|
46
|
+
import { jsx } from "@opentui/solid/jsx-runtime"
|
|
47
|
+
import type { JSX } from "@opentui/solid/jsx-runtime"
|
|
48
|
+
|
|
49
|
+
/** An intrinsic OpenTUI element: `h("text", { fg, children })`. */
|
|
50
|
+
export function h<K extends string>(
|
|
51
|
+
tag: K,
|
|
52
|
+
props: K extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[K] : never,
|
|
53
|
+
): JSX.Element {
|
|
54
|
+
return jsx(tag, props as unknown as Record<string, unknown>)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A component: `c(Show, { get when() {...}, get fallback() {...} })`.
|
|
59
|
+
*
|
|
60
|
+
* The component's own prop type is intentionally not enforced here: host
|
|
61
|
+
* components (Show, and any OpenTUI component) are generic and their unions do
|
|
62
|
+
* not survive inference through a wrapper, while the shapes used in this
|
|
63
|
+
* package are small and covered by the e2e liveness check.
|
|
64
|
+
*/
|
|
65
|
+
export function c<C extends (props: any) => unknown, P extends object>(
|
|
66
|
+
component: C,
|
|
67
|
+
props: P,
|
|
68
|
+
): JSX.Element {
|
|
69
|
+
return jsx(
|
|
70
|
+
component as unknown as (props: Record<string, unknown>) => unknown,
|
|
71
|
+
props as unknown as Record<string, unknown>,
|
|
72
|
+
)
|
|
73
|
+
}
|