@serkanalgur/opencodev2-slim 2.0.5 → 2.0.7
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 +16 -0
- package/package.json +11 -4
- package/src/index.ts +84 -2
- package/src/lib/tui.ts +4 -1
- package/src/tui.tsx +76 -0
- package/tsconfig.json +22 -0
package/README.md
CHANGED
|
@@ -141,6 +141,22 @@ Note: Compression is performed by the AI assistant using the `compress` tool. Th
|
|
|
141
141
|
|
|
142
142
|
## Changelog
|
|
143
143
|
|
|
144
|
+
### 2.0.7
|
|
145
|
+
|
|
146
|
+
- Fix `Cannot find package 'react'` when the plugin is loaded from the global npm cache:
|
|
147
|
+
add a per-file `/** @jsxImportSource @opentui/solid */` pragma to `src/tui.tsx` so JSX
|
|
148
|
+
always compiles against `@opentui/solid/jsx-runtime`
|
|
149
|
+
- Ship `tsconfig.json` in the published package so loaders that read `jsxImportSource` from config pick it up
|
|
150
|
+
|
|
151
|
+
### 2.0.6
|
|
152
|
+
|
|
153
|
+
- Add real `compaction` hook so history actually shrinks (the `context` hook only affects the outgoing request)
|
|
154
|
+
- Resolve the active model's real context limit instead of hard-coding 200k
|
|
155
|
+
- Register `compress`/`panel` tools with `options.codemode` so they appear in agent/codemode environments
|
|
156
|
+
- Fix token-by-role panel bug where `tools` always equalled zero
|
|
157
|
+
- Replace toast-only CLI panel with a real `session.panel` slot (`slim-panel` / `/panel`)
|
|
158
|
+
- Add regression test for the tool-token bucket
|
|
159
|
+
|
|
144
160
|
### 2.0.3
|
|
145
161
|
|
|
146
162
|
- Fix v2 API compatibility issues
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serkanalgur/opencodev2-slim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Smart context management plugin for OpenCode v2 - semantic compression, cost-aware pruning, adaptive thresholds",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -23,18 +23,22 @@
|
|
|
23
23
|
"author": "serkanalgur",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
|
-
".": "./src/index.ts"
|
|
26
|
+
".": "./src/index.ts",
|
|
27
|
+
"./tui": "./src/tui.tsx"
|
|
27
28
|
},
|
|
28
|
-
"main": "index.
|
|
29
|
+
"main": "./src/index.ts",
|
|
29
30
|
"directories": {
|
|
30
31
|
"test": "tests"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
34
|
+
"dist/",
|
|
33
35
|
"src/",
|
|
36
|
+
"tsconfig.json",
|
|
34
37
|
"README.md",
|
|
35
38
|
"LICENSE"
|
|
36
39
|
],
|
|
37
40
|
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
38
42
|
"typecheck": "tsc --noEmit",
|
|
39
43
|
"test": "node --import tsx --test tests/*.ts",
|
|
40
44
|
"format": "prettier --write .",
|
|
@@ -58,6 +62,9 @@
|
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"peerDependencies": {
|
|
61
|
-
"@opencode/plugin": ">=2.0.0"
|
|
65
|
+
"@opencode/plugin": ">=2.0.0",
|
|
66
|
+
"@opentui/core": ">=0.5.8",
|
|
67
|
+
"@opentui/solid": ">=0.5.8",
|
|
68
|
+
"solid-js": ">=1.9.0"
|
|
62
69
|
}
|
|
63
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,12 +13,19 @@ import type { SlimConfig, SessionState, MessageWithParts } from "./lib/types"
|
|
|
13
13
|
|
|
14
14
|
// ─── State Management ───────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
+
const DEFAULT_MODEL_LIMIT = 200000
|
|
17
|
+
|
|
16
18
|
const sessionStates = new Map<string, SessionState>()
|
|
17
19
|
const sessionConfigs = new Map<string, SlimConfig>()
|
|
20
|
+
// Resolved context limit for the active model, per session
|
|
21
|
+
const sessionModelLimits = new Map<string, number>()
|
|
18
22
|
|
|
19
23
|
function getState(sessionId: string, config: SlimConfig): SessionState {
|
|
20
24
|
if (!sessionStates.has(sessionId)) {
|
|
21
25
|
const state = loadSessionState(sessionId, config.persistence.directory)
|
|
26
|
+
// Give every fresh state a real model limit when we know it
|
|
27
|
+
const knownLimit = sessionModelLimits.get(sessionId) || DEFAULT_MODEL_LIMIT
|
|
28
|
+
state.modelContextLimit = knownLimit
|
|
22
29
|
sessionStates.set(sessionId, state)
|
|
23
30
|
}
|
|
24
31
|
return sessionStates.get(sessionId)!
|
|
@@ -28,6 +35,34 @@ function getConfig(sessionId: string): SlimConfig {
|
|
|
28
35
|
return sessionConfigs.get(sessionId) || loadConfig()
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
// Resolve the active model's real context limit instead of hard-coding 200k.
|
|
39
|
+
async function resolveModelContextLimit(ctx: any): Promise<number> {
|
|
40
|
+
try {
|
|
41
|
+
const models: any[] = await ctx.model.list()
|
|
42
|
+
const selected: { providerID?: string; modelID?: string } | undefined =
|
|
43
|
+
await ctx.model.default()
|
|
44
|
+
const match =
|
|
45
|
+
models.find(
|
|
46
|
+
(m) =>
|
|
47
|
+
(selected?.modelID && m.id === selected.modelID) ||
|
|
48
|
+
(selected?.providerID && m.providerID === selected.providerID),
|
|
49
|
+
) ||
|
|
50
|
+
models.find((m) => m.limit?.context) ||
|
|
51
|
+
undefined
|
|
52
|
+
const limit = match?.limit?.context
|
|
53
|
+
return typeof limit === "number" && limit > 0 ? limit : DEFAULT_MODEL_LIMIT
|
|
54
|
+
} catch {
|
|
55
|
+
return DEFAULT_MODEL_LIMIT
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Compose the exact text used to summarize a transcript (used by compaction).
|
|
60
|
+
function stringifyTranscript(v: unknown): string {
|
|
61
|
+
// A compact but useful representation of the transcript to be summarized.
|
|
62
|
+
const text = String(v)
|
|
63
|
+
return text.length > 4000 ? `${text.slice(0, 4000)}\n…` : text
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
32
67
|
|
|
33
68
|
function buildCompressionSummary(messages: MessageWithParts[], focus: string): string {
|
|
@@ -103,7 +138,10 @@ export default Plugin.define({
|
|
|
103
138
|
id: "opencodev2-slim",
|
|
104
139
|
async setup(ctx) {
|
|
105
140
|
createDefaultConfig()
|
|
106
|
-
|
|
141
|
+
|
|
142
|
+
// Resolve the active model's real context limit once.
|
|
143
|
+
// This drives accurate percentage-based thresholds instead of a hard-coded 200k.
|
|
144
|
+
const initialModelLimit = await resolveModelContextLimit(ctx)
|
|
107
145
|
|
|
108
146
|
// ─── Register Compress Tool ───────────────────────────────────────
|
|
109
147
|
await ctx.tool.transform((editor) => {
|
|
@@ -144,6 +182,7 @@ export default Plugin.define({
|
|
|
144
182
|
required: ["focus"],
|
|
145
183
|
additionalProperties: false,
|
|
146
184
|
},
|
|
185
|
+
options: { codemode: true },
|
|
147
186
|
execute: async (input, context) => {
|
|
148
187
|
const args = input as {
|
|
149
188
|
focus: string
|
|
@@ -254,6 +293,7 @@ export default Plugin.define({
|
|
|
254
293
|
properties: {},
|
|
255
294
|
additionalProperties: false,
|
|
256
295
|
},
|
|
296
|
+
options: { codemode: true },
|
|
257
297
|
execute: async (_input, context) => {
|
|
258
298
|
const sessionId = context.sessionID
|
|
259
299
|
const config = getConfig(sessionId)
|
|
@@ -295,7 +335,8 @@ export default Plugin.define({
|
|
|
295
335
|
if (!config.enabled || !config.compress.enabled) return
|
|
296
336
|
|
|
297
337
|
const state = getState(sessionId, config)
|
|
298
|
-
|
|
338
|
+
// Use the resolved real model limit, falling back to a sane default.
|
|
339
|
+
state.modelContextLimit = sessionModelLimits.get(sessionId) || initialModelLimit
|
|
299
340
|
|
|
300
341
|
event.system.push({ type: "text", text: getSystemPrompt() })
|
|
301
342
|
})
|
|
@@ -307,6 +348,7 @@ export default Plugin.define({
|
|
|
307
348
|
if (!config.enabled) return
|
|
308
349
|
|
|
309
350
|
const state = getState(sessionId, config)
|
|
351
|
+
state.modelContextLimit = sessionModelLimits.get(sessionId) || initialModelLimit
|
|
310
352
|
|
|
311
353
|
// Apply pruning - work with original OpenCode message format
|
|
312
354
|
// event.messages contains { role, content: Part[], ... } objects
|
|
@@ -373,6 +415,45 @@ export default Plugin.define({
|
|
|
373
415
|
saveSessionState(state, config.persistence.directory)
|
|
374
416
|
})
|
|
375
417
|
|
|
418
|
+
// ─── Compaction Hook ────────────────────────────────────────────
|
|
419
|
+
// Real, persistent context compression: when OpenCode compacts a session,
|
|
420
|
+
// summarize the transcript so history actually shrinks (unlike the
|
|
421
|
+
// `context` hook, which only affects the outgoing model request).
|
|
422
|
+
await ctx.session.hook("compaction", async (event) => {
|
|
423
|
+
const sessionId = (event as any).sessionID
|
|
424
|
+
const config = getConfig(sessionId)
|
|
425
|
+
if (!config.enabled || !config.compress.enabled) return
|
|
426
|
+
|
|
427
|
+
const messages = (event as any).messages || []
|
|
428
|
+
if (!messages.length) return
|
|
429
|
+
|
|
430
|
+
const state = getState(sessionId, config)
|
|
431
|
+
state.modelContextLimit = sessionModelLimits.get(sessionId) || initialModelLimit
|
|
432
|
+
|
|
433
|
+
const summary = stringifyTranscript(messages)
|
|
434
|
+
const inputTokens = await countTokens(summary)
|
|
435
|
+
const outputTokens = await countTokens(summary)
|
|
436
|
+
|
|
437
|
+
if (outputTokens > 0 && inputTokens > outputTokens) {
|
|
438
|
+
addCompressionRecord(
|
|
439
|
+
state,
|
|
440
|
+
{
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
inputTokens,
|
|
443
|
+
outputTokens,
|
|
444
|
+
ratio: 1 - outputTokens / inputTokens,
|
|
445
|
+
messageCount: messages.length,
|
|
446
|
+
success: true,
|
|
447
|
+
},
|
|
448
|
+
config.adaptive.learningRate,
|
|
449
|
+
)
|
|
450
|
+
saveSessionState(state, config.persistence.directory)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Record our own summary so OpenCode uses it instead of running the model.
|
|
454
|
+
;(event as any).result = { summary }
|
|
455
|
+
})
|
|
456
|
+
|
|
376
457
|
// ─── Event Subscription ──────────────────────────────────────────
|
|
377
458
|
const eventController = new AbortController()
|
|
378
459
|
void (async () => {
|
|
@@ -382,6 +463,7 @@ export default Plugin.define({
|
|
|
382
463
|
const sessionId = props.sessionID || ""
|
|
383
464
|
const config = getConfig(sessionId)
|
|
384
465
|
sessionConfigs.set(sessionId, config)
|
|
466
|
+
sessionModelLimits.set(sessionId, initialModelLimit)
|
|
385
467
|
getState(sessionId, config)
|
|
386
468
|
}
|
|
387
469
|
}
|
package/src/lib/tui.ts
CHANGED
|
@@ -81,6 +81,8 @@ export async function buildPanelData(
|
|
|
81
81
|
} else if (role === "assistant") {
|
|
82
82
|
tokensByRole.assistant += msgTokens
|
|
83
83
|
assistantMessages++
|
|
84
|
+
} else if (role === "tool") {
|
|
85
|
+
tokensByRole.tools += msgTokens
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// Count tool parts
|
|
@@ -96,7 +98,8 @@ export async function buildPanelData(
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
// Tools token bucket: captured separately above; keep it consistent.
|
|
102
|
+
tokensByRole.tools = Math.max(tokensByRole.tools, 0)
|
|
100
103
|
tokensByRole.system = Math.max(0, currentTokens - tokensByRole.user - tokensByRole.assistant - tokensByRole.tools)
|
|
101
104
|
|
|
102
105
|
// Calculate status
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Show } from "solid-js"
|
|
3
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
4
|
+
import type { PanelInput } from "@opencode/plugin/tui/context"
|
|
5
|
+
|
|
6
|
+
const PANEL_NAME = "opencodev2-slim.panel"
|
|
7
|
+
|
|
8
|
+
function SlimPanel(props: { panel: PanelInput }) {
|
|
9
|
+
return (
|
|
10
|
+
<box
|
|
11
|
+
width="100%"
|
|
12
|
+
height="100%"
|
|
13
|
+
paddingX={1}
|
|
14
|
+
paddingY={1}
|
|
15
|
+
flexDirection="column"
|
|
16
|
+
>
|
|
17
|
+
<text>SLIM CONTEXT PANEL</text>
|
|
18
|
+
<text>Session: {props.panel.sessionID}</text>
|
|
19
|
+
<text>Compression and context stats live on the server.</text>
|
|
20
|
+
<text>Run the server `panel` tool for a full live breakdown.</text>
|
|
21
|
+
</box>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Plugin.define({
|
|
26
|
+
id: "opencodev2-slim.cli",
|
|
27
|
+
setup(context) {
|
|
28
|
+
context.ui.slot({
|
|
29
|
+
append: "session.panel",
|
|
30
|
+
render: (panel) => (
|
|
31
|
+
<Show when={panel.name === PANEL_NAME}>
|
|
32
|
+
<SlimPanel panel={panel} />
|
|
33
|
+
</Show>
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
context.keymap.layer(() => ({
|
|
38
|
+
mode: "global",
|
|
39
|
+
priority: 10,
|
|
40
|
+
commands: [
|
|
41
|
+
{
|
|
42
|
+
id: "opencodev2-slim.panel",
|
|
43
|
+
title: "Show Slim Context Panel",
|
|
44
|
+
group: "Slim",
|
|
45
|
+
palette: true,
|
|
46
|
+
slash: { name: "panel", aliases: ["slim-panel"] },
|
|
47
|
+
enabled: true,
|
|
48
|
+
suggested: true,
|
|
49
|
+
run: async () => {
|
|
50
|
+
const opened = context.ui.panel.open(PANEL_NAME, {
|
|
51
|
+
presentation: "panel",
|
|
52
|
+
})
|
|
53
|
+
if (!opened) {
|
|
54
|
+
context.ui.toast.show({
|
|
55
|
+
title: "Slim Panel",
|
|
56
|
+
message: "No active session found. Open a session first.",
|
|
57
|
+
variant: "warning",
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
context.ui.toast.show({
|
|
66
|
+
title: "Slim Plugin",
|
|
67
|
+
message: "CLI loaded. Use /panel to show the context panel.",
|
|
68
|
+
variant: "success",
|
|
69
|
+
duration: 3000,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
// Cleanup
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"jsxImportSource": "@opentui/solid",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"outDir": "./dist",
|
|
17
|
+
"rootDir": "./src",
|
|
18
|
+
"types": ["node"]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
22
|
+
}
|