@serkanalgur/opencodev2-slim 2.0.2 → 2.0.4
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 +7 -0
- package/package.json +6 -30
- package/src/index.ts +88 -102
- package/src/lib/compress.ts +17 -11
- package/src/lib/types.ts +2 -2
- package/src/lib/tui/data.ts +0 -90
- package/src/lib/tui/dialogs.tsx +0 -467
- package/src/lib/tui/modals.tsx +0 -12
- package/src/tui.tsx +0 -61
package/README.md
CHANGED
|
@@ -141,6 +141,13 @@ Note: Compression is performed by the AI assistant using the `compress` tool. Th
|
|
|
141
141
|
|
|
142
142
|
## Changelog
|
|
143
143
|
|
|
144
|
+
### 2.0.3
|
|
145
|
+
|
|
146
|
+
- Fix v2 API compatibility issues
|
|
147
|
+
- Remove namespace from tool registration
|
|
148
|
+
- Handle both v1 and v2 message part types
|
|
149
|
+
- Make context hooks synchronous
|
|
150
|
+
|
|
144
151
|
### 2.0.2
|
|
145
152
|
|
|
146
153
|
- Update README documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serkanalgur/opencodev2-slim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Smart context management plugin for OpenCode v2 - semantic compression, cost-aware pruning, adaptive thresholds",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
"author": "serkanalgur",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
|
-
".": "./src/index.ts"
|
|
27
|
-
"./tui": "./src/tui.tsx"
|
|
26
|
+
".": "./src/index.ts"
|
|
28
27
|
},
|
|
29
28
|
"main": "index.js",
|
|
30
29
|
"directories": {
|
|
@@ -48,40 +47,17 @@
|
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@opencode/plugin": "^2.0.0",
|
|
50
49
|
"@opencode-ai/sdk": "^1.18.29",
|
|
51
|
-
"@opencode-ai/plugin": "^1.18.29",
|
|
52
|
-
"@opentui/core": "^0.5.10",
|
|
53
|
-
"@opentui/keymap": "^0.5.10",
|
|
54
|
-
"@opentui/solid": "^0.5.10",
|
|
55
50
|
"@types/node": "^22.0.0",
|
|
56
51
|
"prettier": "^3.4.0",
|
|
57
|
-
"solid-js": "1.9.12",
|
|
58
52
|
"tsx": "^4.19.0",
|
|
59
53
|
"typescript": "^5.7.0"
|
|
60
54
|
},
|
|
61
|
-
"peerDependencies": {
|
|
62
|
-
"@opencode/plugin": ">=2.0.0",
|
|
63
|
-
"@opencode-ai/sdk": ">=1.18.29",
|
|
64
|
-
"@opencode-ai/plugin": ">=1.18.29",
|
|
65
|
-
"@opentui/core": ">=0.5.10",
|
|
66
|
-
"@opentui/keymap": ">=0.5.10",
|
|
67
|
-
"@opentui/solid": ">=0.5.10",
|
|
68
|
-
"solid-js": ">=1.0.0"
|
|
69
|
-
},
|
|
70
55
|
"peerDependenciesMeta": {
|
|
71
|
-
"@opencode-ai/
|
|
72
|
-
"optional": true
|
|
73
|
-
},
|
|
74
|
-
"@opentui/core": {
|
|
75
|
-
"optional": true
|
|
76
|
-
},
|
|
77
|
-
"@opentui/keymap": {
|
|
78
|
-
"optional": true
|
|
79
|
-
},
|
|
80
|
-
"@opentui/solid": {
|
|
81
|
-
"optional": true
|
|
82
|
-
},
|
|
83
|
-
"solid-js": {
|
|
56
|
+
"@opencode-ai/sdk": {
|
|
84
57
|
"optional": true
|
|
85
58
|
}
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@opencode/plugin": ">=2.0.0"
|
|
86
62
|
}
|
|
87
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -43,17 +43,21 @@ function buildCompressionSummary(messages: MessageWithParts[], focus: string): s
|
|
|
43
43
|
|
|
44
44
|
for (const msg of messages) {
|
|
45
45
|
for (const part of msg.parts) {
|
|
46
|
-
if (part.type === "tool") {
|
|
46
|
+
if (part.type === "tool-call") {
|
|
47
47
|
const toolPart = part as any
|
|
48
48
|
toolCalls.push(
|
|
49
|
-
`${toolPart.
|
|
49
|
+
`${toolPart.name}: ${JSON.stringify(toolPart.input || {}).slice(0, 100)}`,
|
|
50
50
|
)
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
}
|
|
52
|
+
if (part.type === "tool-result") {
|
|
53
|
+
const toolPart = part as any
|
|
54
|
+
if (toolPart.result?.type === "error") {
|
|
55
|
+
errors.push(String(toolPart.result.value).slice(0, 200) || "Unknown error")
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
if (part.type === "text") {
|
|
56
|
-
const
|
|
59
|
+
const textPart = part as any
|
|
60
|
+
const text = textPart.text || ""
|
|
57
61
|
if (
|
|
58
62
|
text.includes("decided") ||
|
|
59
63
|
text.includes("chose") ||
|
|
@@ -86,22 +90,23 @@ function buildCompressionSummary(messages: MessageWithParts[], focus: string): s
|
|
|
86
90
|
return lines.join("\n")
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
function wrapAsMessageWithParts(msg: any): MessageWithParts {
|
|
94
|
+
return {
|
|
95
|
+
info: msg.info || { id: msg.id || "", role: msg.role, sessionID: "", time: { created: Date.now() } },
|
|
96
|
+
parts: msg.parts || msg.content || [],
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
// ─── Plugin Entry ───────────────────────────────────────────────────────────
|
|
90
101
|
|
|
91
102
|
export default Plugin.define({
|
|
92
103
|
id: "opencodev2-slim",
|
|
93
104
|
async setup(ctx) {
|
|
94
|
-
// Load and create default config if needed
|
|
95
105
|
createDefaultConfig()
|
|
96
106
|
const globalConfig = loadConfig()
|
|
97
107
|
|
|
98
108
|
// ─── Register Compress Tool ───────────────────────────────────────
|
|
99
109
|
await ctx.tool.transform((editor) => {
|
|
100
|
-
editor.namespace({
|
|
101
|
-
name: "slim",
|
|
102
|
-
description: "Smart context management tools",
|
|
103
|
-
})
|
|
104
|
-
|
|
105
110
|
editor.add({
|
|
106
111
|
name: "compress",
|
|
107
112
|
description: getCompressToolDescription(),
|
|
@@ -150,9 +155,7 @@ export default Plugin.define({
|
|
|
150
155
|
}
|
|
151
156
|
const mode = args.mode || "auto"
|
|
152
157
|
const keepRecent = args.keepRecent || 5
|
|
153
|
-
|
|
154
|
-
// Get session ID from context or fallback
|
|
155
|
-
const sessionId = (context as any).sessionID || ""
|
|
158
|
+
const sessionId = context.sessionID
|
|
156
159
|
const config = getConfig(sessionId)
|
|
157
160
|
const state = getState(sessionId, config)
|
|
158
161
|
|
|
@@ -163,12 +166,10 @@ export default Plugin.define({
|
|
|
163
166
|
return { content: "No messages found in session" }
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
const messageWithParts: MessageWithParts[] = messages.map(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}))
|
|
169
|
+
const messageWithParts: MessageWithParts[] = messages.map(
|
|
170
|
+
(m: any) => wrapAsMessageWithParts(m),
|
|
171
|
+
)
|
|
170
172
|
|
|
171
|
-
// Determine what to compress
|
|
172
173
|
let targetIndices: number[] = []
|
|
173
174
|
let inputTokens = 0
|
|
174
175
|
|
|
@@ -193,7 +194,6 @@ export default Plugin.define({
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
} else {
|
|
196
|
-
// Auto mode
|
|
197
197
|
for (let i = 0; i < messageWithParts.length - keepRecent; i++) {
|
|
198
198
|
const msg = messageWithParts[i]
|
|
199
199
|
const text = getMessageText(msg) + getToolResultContent(msg)
|
|
@@ -210,7 +210,6 @@ export default Plugin.define({
|
|
|
210
210
|
|
|
211
211
|
const targetMessages = targetIndices.map((i) => messageWithParts[i])
|
|
212
212
|
const summary = buildCompressionSummary(targetMessages, args.focus)
|
|
213
|
-
|
|
214
213
|
const outputTokens = await countTokens(summary)
|
|
215
214
|
const ratio = inputTokens > 0 ? 1 - outputTokens / inputTokens : 0
|
|
216
215
|
|
|
@@ -256,7 +255,7 @@ export default Plugin.define({
|
|
|
256
255
|
additionalProperties: false,
|
|
257
256
|
},
|
|
258
257
|
execute: async (_input, context) => {
|
|
259
|
-
const sessionId =
|
|
258
|
+
const sessionId = context.sessionID
|
|
260
259
|
const config = getConfig(sessionId)
|
|
261
260
|
const state = getState(sessionId, config)
|
|
262
261
|
|
|
@@ -267,23 +266,18 @@ export default Plugin.define({
|
|
|
267
266
|
return { content: "No messages found in session" }
|
|
268
267
|
}
|
|
269
268
|
|
|
270
|
-
const messageWithParts: MessageWithParts[] = messages.map(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}))
|
|
274
|
-
|
|
275
|
-
const modelId = (context as any).model?.id || "unknown"
|
|
269
|
+
const messageWithParts: MessageWithParts[] = messages.map(
|
|
270
|
+
(m: any) => wrapAsMessageWithParts(m),
|
|
271
|
+
)
|
|
276
272
|
|
|
277
273
|
const panelData = await buildPanelData(
|
|
278
274
|
sessionId,
|
|
279
275
|
messageWithParts,
|
|
280
276
|
state,
|
|
281
277
|
config,
|
|
282
|
-
modelId,
|
|
283
278
|
)
|
|
284
279
|
|
|
285
280
|
const panel = renderPanel(panelData)
|
|
286
|
-
|
|
287
281
|
return { content: panel }
|
|
288
282
|
} catch (error) {
|
|
289
283
|
return {
|
|
@@ -294,92 +288,85 @@ export default Plugin.define({
|
|
|
294
288
|
})
|
|
295
289
|
})
|
|
296
290
|
|
|
297
|
-
// ─── System Prompt Hook
|
|
291
|
+
// ─── System Prompt Hook (sync) ───────────────────────────────────
|
|
298
292
|
await ctx.session.hook("context", (event) => {
|
|
299
|
-
const sessionId =
|
|
293
|
+
const sessionId = event.sessionID
|
|
300
294
|
const config = getConfig(sessionId)
|
|
301
|
-
if (!config.enabled || !config.compress.enabled)
|
|
302
|
-
return
|
|
303
|
-
}
|
|
295
|
+
if (!config.enabled || !config.compress.enabled) return
|
|
304
296
|
|
|
305
297
|
const state = getState(sessionId, config)
|
|
298
|
+
state.modelContextLimit = 200000 // default; updated by tool calls
|
|
306
299
|
|
|
307
|
-
|
|
308
|
-
if ((event as any).model?.limit?.context) {
|
|
309
|
-
state.modelContextLimit = (event as any).model.limit.context
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Add system prompt
|
|
313
|
-
const systemPrompt = getSystemPrompt()
|
|
314
|
-
event.system.push({ type: "text", text: systemPrompt })
|
|
300
|
+
event.system.push({ type: "text", text: getSystemPrompt() })
|
|
315
301
|
})
|
|
316
302
|
|
|
317
|
-
// ─── Messages Transform Hook
|
|
318
|
-
await ctx.session.hook("context",
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
303
|
+
// ─── Messages Transform Hook (sync) ──────────────────────────────
|
|
304
|
+
await ctx.session.hook("context", (event) => {
|
|
305
|
+
const sessionId = event.sessionID
|
|
306
|
+
const config = getConfig(sessionId)
|
|
307
|
+
if (!config.enabled) return
|
|
323
308
|
|
|
324
|
-
const sessionId = (event as any).sessionID || ""
|
|
325
309
|
const state = getState(sessionId, config)
|
|
326
310
|
|
|
327
|
-
// Apply pruning strategies
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
event.messages.push(
|
|
311
|
+
// Apply pruning strategies
|
|
312
|
+
const pruned = pruneMessages(
|
|
313
|
+
event.messages.map((m: any) => wrapAsMessageWithParts(m)),
|
|
314
|
+
config,
|
|
315
|
+
event.messages.length,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// Replace messages in-place
|
|
319
|
+
event.messages.length = 0
|
|
320
|
+
for (const msg of pruned) {
|
|
321
|
+
event.messages.push(msg as any)
|
|
322
|
+
}
|
|
338
323
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
324
|
+
// Quick token estimate (sync, ~4 chars per token)
|
|
325
|
+
let totalTokens = 0
|
|
326
|
+
for (const msg of event.messages) {
|
|
327
|
+
const content = (msg as any).content
|
|
328
|
+
if (Array.isArray(content)) {
|
|
329
|
+
for (const part of content) {
|
|
330
|
+
if (part.type === "text" && part.text) {
|
|
331
|
+
totalTokens += Math.ceil(part.text.length / 4)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
344
334
|
}
|
|
335
|
+
}
|
|
345
336
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
337
|
+
state.currentTokenCount = totalTokens
|
|
338
|
+
|
|
339
|
+
const maxTokens = resolveTokenLimit(
|
|
340
|
+
config.compress.maxContextLimit,
|
|
341
|
+
state.modelContextLimit,
|
|
342
|
+
)
|
|
343
|
+
const minTokens = resolveTokenLimit(
|
|
344
|
+
config.compress.minContextLimit,
|
|
345
|
+
state.modelContextLimit,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const shouldComp = shouldCompress(
|
|
349
|
+
totalTokens,
|
|
350
|
+
maxTokens,
|
|
351
|
+
minTokens,
|
|
352
|
+
state.lastCompressionTime,
|
|
353
|
+
config.compress.nudgeFrequency,
|
|
354
|
+
event.messages.length,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if (shouldComp.compress && !state.manualMode) {
|
|
358
|
+
const nudgeMessage = getNudgeMessage(
|
|
359
|
+
shouldComp.reason,
|
|
358
360
|
totalTokens,
|
|
359
361
|
maxTokens,
|
|
360
|
-
minTokens,
|
|
361
|
-
state.lastCompressionTime,
|
|
362
|
-
config.compress.nudgeFrequency,
|
|
363
|
-
event.messages.length,
|
|
364
362
|
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
totalTokens,
|
|
370
|
-
maxTokens,
|
|
371
|
-
)
|
|
372
|
-
event.messages.push({
|
|
373
|
-
info: {
|
|
374
|
-
role: "assistant",
|
|
375
|
-
sessionID: sessionId,
|
|
376
|
-
} as any,
|
|
377
|
-
parts: [{ type: "text", text: nudgeMessage }],
|
|
378
|
-
} as any)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
saveSessionState(state, config.persistence.directory)
|
|
363
|
+
event.messages.push({
|
|
364
|
+
role: "assistant",
|
|
365
|
+
content: [{ type: "text", text: nudgeMessage }],
|
|
366
|
+
} as any)
|
|
382
367
|
}
|
|
368
|
+
|
|
369
|
+
saveSessionState(state, config.persistence.directory)
|
|
383
370
|
})
|
|
384
371
|
|
|
385
372
|
// ─── Event Subscription ──────────────────────────────────────────
|
|
@@ -387,8 +374,8 @@ export default Plugin.define({
|
|
|
387
374
|
void (async () => {
|
|
388
375
|
for await (const event of ctx.event.subscribe({ signal: eventController.signal })) {
|
|
389
376
|
if (event.type === "session.created") {
|
|
390
|
-
const
|
|
391
|
-
const sessionId =
|
|
377
|
+
const props = (event as any).properties || {}
|
|
378
|
+
const sessionId = props.sessionID || ""
|
|
392
379
|
const config = getConfig(sessionId)
|
|
393
380
|
sessionConfigs.set(sessionId, config)
|
|
394
381
|
getState(sessionId, config)
|
|
@@ -399,7 +386,6 @@ export default Plugin.define({
|
|
|
399
386
|
// ─── Cleanup ─────────────────────────────────────────────────────
|
|
400
387
|
return () => {
|
|
401
388
|
eventController.abort()
|
|
402
|
-
// Save all states on dispose
|
|
403
389
|
for (const [sessionId, state] of sessionStates.entries()) {
|
|
404
390
|
const config = getConfig(sessionId)
|
|
405
391
|
saveSessionState(state, config.persistence.directory)
|
package/src/lib/compress.ts
CHANGED
|
@@ -10,7 +10,6 @@ async function getTokenizer() {
|
|
|
10
10
|
const mod = await import("@anthropic-ai/tokenizer")
|
|
11
11
|
tokenizer = mod
|
|
12
12
|
} catch {
|
|
13
|
-
// Fallback: estimate ~4 chars per token
|
|
14
13
|
return null
|
|
15
14
|
}
|
|
16
15
|
}
|
|
@@ -40,9 +39,8 @@ export function getMessageText(msg: MessageWithParts): string {
|
|
|
40
39
|
|
|
41
40
|
for (const part of msg.parts) {
|
|
42
41
|
if (part.type === "text") {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
texts.push(textPart.text)
|
|
42
|
+
if (part.text) {
|
|
43
|
+
texts.push(part.text)
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
}
|
|
@@ -54,10 +52,15 @@ export function getToolResultContent(msg: MessageWithParts): string {
|
|
|
54
52
|
const results: string[] = []
|
|
55
53
|
|
|
56
54
|
for (const part of msg.parts) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// v1 SDK format: type === "tool"
|
|
56
|
+
if (part.type === "tool" && part.state?.type === "result" && part.state?.output) {
|
|
57
|
+
results.push(String(part.state.output).slice(0, 500))
|
|
58
|
+
}
|
|
59
|
+
// v2 AI format: type === "tool-result"
|
|
60
|
+
if (part.type === "tool-result" && part.result) {
|
|
61
|
+
const val = part.result.value
|
|
62
|
+
if (val !== undefined && val !== null) {
|
|
63
|
+
results.push(String(val).slice(0, 500))
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -67,9 +70,13 @@ export function getToolResultContent(msg: MessageWithParts): string {
|
|
|
67
70
|
|
|
68
71
|
export function getToolName(msg: MessageWithParts): string | null {
|
|
69
72
|
for (const part of msg.parts) {
|
|
73
|
+
// v1 SDK format
|
|
70
74
|
if (part.type === "tool") {
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
return part.tool || null
|
|
76
|
+
}
|
|
77
|
+
// v2 AI format
|
|
78
|
+
if (part.type === "tool-call") {
|
|
79
|
+
return part.name || null
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
return null
|
|
@@ -96,7 +103,6 @@ export function shouldCompress(
|
|
|
96
103
|
|
|
97
104
|
// Soft limit: recommend compression
|
|
98
105
|
if (currentTokens >= minTokens) {
|
|
99
|
-
// Check if enough time has passed since last compression
|
|
100
106
|
if (minutesSinceLast >= nudgeFrequency) {
|
|
101
107
|
return {
|
|
102
108
|
compress: true,
|
package/src/lib/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message
|
|
1
|
+
import type { Message } from "@opencode-ai/sdk/v2"
|
|
2
2
|
|
|
3
3
|
// ─── Config Types ───────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -90,7 +90,7 @@ export interface ToolCallInfo {
|
|
|
90
90
|
|
|
91
91
|
export interface MessageWithParts {
|
|
92
92
|
info: Message
|
|
93
|
-
parts:
|
|
93
|
+
parts: any[]
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export interface CompressionRange {
|
package/src/lib/tui/data.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
2
|
-
import type { SlimConfig } from "../types"
|
|
3
|
-
import { readFileSync, existsSync } from "fs"
|
|
4
|
-
import { join } from "path"
|
|
5
|
-
import { homedir } from "os"
|
|
6
|
-
import { parse } from "jsonc-parser/lib/esm/main.js"
|
|
7
|
-
|
|
8
|
-
const DEFAULT_CONFIG: SlimConfig = {
|
|
9
|
-
enabled: true,
|
|
10
|
-
debug: false,
|
|
11
|
-
compress: {
|
|
12
|
-
enabled: true,
|
|
13
|
-
permission: "allow",
|
|
14
|
-
maxContextLimit: "80%",
|
|
15
|
-
minContextLimit: "40%",
|
|
16
|
-
nudgeFrequency: 5,
|
|
17
|
-
protectUserMessages: false,
|
|
18
|
-
protectedTools: ["task", "skill", "todowrite", "todoread"],
|
|
19
|
-
},
|
|
20
|
-
strategies: {
|
|
21
|
-
deduplication: {
|
|
22
|
-
enabled: true,
|
|
23
|
-
protectedTools: [],
|
|
24
|
-
},
|
|
25
|
-
purgeErrors: {
|
|
26
|
-
enabled: true,
|
|
27
|
-
turns: 4,
|
|
28
|
-
protectedTools: [],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
adaptive: {
|
|
32
|
-
enabled: true,
|
|
33
|
-
learningRate: 0.1,
|
|
34
|
-
minCompressionRatio: 0.3,
|
|
35
|
-
},
|
|
36
|
-
costAware: {
|
|
37
|
-
enabled: true,
|
|
38
|
-
cacheBoostFactor: 0.5,
|
|
39
|
-
},
|
|
40
|
-
persistence: {
|
|
41
|
-
enabled: true,
|
|
42
|
-
directory: join(homedir(), ".config", "opencode", "slim"),
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function deepMerge(base: SlimConfig, override: Partial<SlimConfig>): SlimConfig {
|
|
47
|
-
return {
|
|
48
|
-
...base,
|
|
49
|
-
...override,
|
|
50
|
-
compress: { ...base.compress, ...override.compress },
|
|
51
|
-
strategies: {
|
|
52
|
-
deduplication: { ...base.strategies.deduplication, ...override.strategies?.deduplication },
|
|
53
|
-
purgeErrors: { ...base.strategies.purgeErrors, ...override.strategies?.purgeErrors },
|
|
54
|
-
},
|
|
55
|
-
adaptive: { ...base.adaptive, ...override.adaptive },
|
|
56
|
-
costAware: { ...base.costAware, ...override.costAware },
|
|
57
|
-
persistence: { ...base.persistence, ...override.persistence },
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function loadConfig(_api: TuiPluginApi): SlimConfig {
|
|
62
|
-
let config = { ...DEFAULT_CONFIG }
|
|
63
|
-
|
|
64
|
-
const globalDir = process.env.XDG_CONFIG_HOME
|
|
65
|
-
? join(process.env.XDG_CONFIG_HOME, "opencode")
|
|
66
|
-
: join(homedir(), ".config", "opencode")
|
|
67
|
-
|
|
68
|
-
const globalPath = join(globalDir, "slim.jsonc")
|
|
69
|
-
const globalPathJson = join(globalDir, "slim.json")
|
|
70
|
-
|
|
71
|
-
const configPath = existsSync(globalPath)
|
|
72
|
-
? globalPath
|
|
73
|
-
: existsSync(globalPathJson)
|
|
74
|
-
? globalPathJson
|
|
75
|
-
: null
|
|
76
|
-
|
|
77
|
-
if (configPath) {
|
|
78
|
-
try {
|
|
79
|
-
const content = readFileSync(configPath, "utf-8")
|
|
80
|
-
const parsed = parse(content)
|
|
81
|
-
if (parsed) {
|
|
82
|
-
config = deepMerge(config, parsed)
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Use defaults
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return config
|
|
90
|
-
}
|
package/src/lib/tui/dialogs.tsx
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import { createSignal, onMount, For, Show } from "solid-js"
|
|
4
|
-
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
5
|
-
import type { SlimConfig } from "../types"
|
|
6
|
-
import type { Message, Part, AssistantMessage } from "@opencode-ai/sdk/v2"
|
|
7
|
-
|
|
8
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
interface PanelData {
|
|
11
|
-
currentTokens: number
|
|
12
|
-
maxTokens: number
|
|
13
|
-
usagePercent: number
|
|
14
|
-
status: "healthy" | "warning" | "critical"
|
|
15
|
-
userMessages: number
|
|
16
|
-
assistantMessages: number
|
|
17
|
-
toolCalls: number
|
|
18
|
-
toolResults: number
|
|
19
|
-
compressionCount: number
|
|
20
|
-
averageRatio: number
|
|
21
|
-
totalTokensSaved: number
|
|
22
|
-
estimatedCost: number
|
|
23
|
-
costSaved: number
|
|
24
|
-
model: string
|
|
25
|
-
recommendations: string[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ─── UI Components ──────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function SlimFrame(props: {
|
|
31
|
-
api: TuiPluginApi
|
|
32
|
-
title: string
|
|
33
|
-
eyebrow: string
|
|
34
|
-
children: any
|
|
35
|
-
onBack?: () => void
|
|
36
|
-
}) {
|
|
37
|
-
const theme = props.api.theme.current
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<box paddingLeft={3} paddingRight={3} paddingBottom={1} gap={1}>
|
|
41
|
-
<box flexDirection="row" justifyContent="space-between">
|
|
42
|
-
<box flexDirection="column">
|
|
43
|
-
<text fg={theme.primary}>
|
|
44
|
-
<b>{props.eyebrow}</b>
|
|
45
|
-
</text>
|
|
46
|
-
<text fg={theme.text}>
|
|
47
|
-
<b>{props.title}</b>
|
|
48
|
-
</text>
|
|
49
|
-
</box>
|
|
50
|
-
<text fg={theme.textMuted} onMouseUp={() => props.api.ui.dialog.clear()}>
|
|
51
|
-
esc
|
|
52
|
-
</text>
|
|
53
|
-
</box>
|
|
54
|
-
<box height={1} border={["bottom"]} borderColor={theme.borderSubtle} />
|
|
55
|
-
{props.children}
|
|
56
|
-
<box flexDirection="row" justifyContent="flex-end" paddingTop={1}>
|
|
57
|
-
<box
|
|
58
|
-
paddingLeft={2}
|
|
59
|
-
paddingRight={2}
|
|
60
|
-
backgroundColor={theme.primary}
|
|
61
|
-
onMouseUp={() => props.api.ui.dialog.clear()}
|
|
62
|
-
>
|
|
63
|
-
<text fg={theme.selectedListItemText}>close</text>
|
|
64
|
-
</box>
|
|
65
|
-
</box>
|
|
66
|
-
</box>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function Card(props: { theme: any; title: string; children: any }) {
|
|
71
|
-
const accent = props.theme.primary
|
|
72
|
-
return (
|
|
73
|
-
<box
|
|
74
|
-
flexDirection="column"
|
|
75
|
-
paddingLeft={2}
|
|
76
|
-
paddingRight={2}
|
|
77
|
-
paddingTop={1}
|
|
78
|
-
paddingBottom={1}
|
|
79
|
-
backgroundColor={props.theme.backgroundElement}
|
|
80
|
-
border={["left"]}
|
|
81
|
-
borderColor={accent}
|
|
82
|
-
gap={1}
|
|
83
|
-
>
|
|
84
|
-
<text fg={accent}>
|
|
85
|
-
<b>{props.title}</b>
|
|
86
|
-
</text>
|
|
87
|
-
{props.children}
|
|
88
|
-
</box>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function Metric(props: { theme: any; label: string; value: string; hint?: string }) {
|
|
93
|
-
return (
|
|
94
|
-
<box flexDirection="row" gap={2}>
|
|
95
|
-
<box width={24}>
|
|
96
|
-
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
97
|
-
</box>
|
|
98
|
-
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
99
|
-
<text fg={props.theme.text}>
|
|
100
|
-
<b>{props.value}</b>
|
|
101
|
-
</text>
|
|
102
|
-
{props.hint ? <text fg={props.theme.textMuted}>{props.hint}</text> : null}
|
|
103
|
-
</box>
|
|
104
|
-
</box>
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function Progress(props: {
|
|
109
|
-
theme: any
|
|
110
|
-
label: string
|
|
111
|
-
value: number
|
|
112
|
-
total: number
|
|
113
|
-
color: "primary" | "accent" | "success" | "warning" | "error"
|
|
114
|
-
detail: string
|
|
115
|
-
}) {
|
|
116
|
-
const width = 32
|
|
117
|
-
const filled =
|
|
118
|
-
props.total > 0 ? Math.max(0, Math.round((props.value / props.total) * width)) : 0
|
|
119
|
-
const empty = Math.max(0, width - filled)
|
|
120
|
-
return (
|
|
121
|
-
<box flexDirection="column" gap={0}>
|
|
122
|
-
<box flexDirection="row" gap={2}>
|
|
123
|
-
<box width={20}>
|
|
124
|
-
<text fg={props.theme.text}>{props.label}</text>
|
|
125
|
-
</box>
|
|
126
|
-
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
127
|
-
<text fg={props.theme.text}>
|
|
128
|
-
<b>{props.total > 0 ? `${Math.round((props.value / props.total) * 100)}%` : "0%"}</b>
|
|
129
|
-
</text>
|
|
130
|
-
<text fg={props.theme.textMuted}>{props.detail}</text>
|
|
131
|
-
</box>
|
|
132
|
-
</box>
|
|
133
|
-
<box flexDirection="row">
|
|
134
|
-
<text fg={props.theme[props.color]}>{"█".repeat(filled)}</text>
|
|
135
|
-
<text fg={props.theme.borderSubtle}>{"░".repeat(empty)}</text>
|
|
136
|
-
</box>
|
|
137
|
-
</box>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ─── Panel Dialog ───────────────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
export function PanelDialog(props: { api: TuiPluginApi; config: SlimConfig }) {
|
|
144
|
-
const theme = props.api.theme.current
|
|
145
|
-
const [data, setData] = createSignal<PanelData | null>(null)
|
|
146
|
-
const [loading, setLoading] = createSignal(true)
|
|
147
|
-
const [activeTab, setActiveTab] = createSignal<"context" | "stats" | "help">("context")
|
|
148
|
-
|
|
149
|
-
onMount(async () => {
|
|
150
|
-
try {
|
|
151
|
-
const panelData = await fetchPanelData(props.api, props.config)
|
|
152
|
-
setData(panelData)
|
|
153
|
-
} catch (error) {
|
|
154
|
-
console.error("Failed to load panel data:", error)
|
|
155
|
-
} finally {
|
|
156
|
-
setLoading(false)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
const formatTokens = (tokens: number): string => {
|
|
161
|
-
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`
|
|
162
|
-
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
|
|
163
|
-
return String(tokens)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<SlimFrame api={props.api} title="Context Panel" eyebrow="SLIM">
|
|
168
|
-
{/* Tab Navigation */}
|
|
169
|
-
<box flexDirection="row" gap={1} marginBottom={1}>
|
|
170
|
-
<TabButton
|
|
171
|
-
theme={theme}
|
|
172
|
-
label="Context"
|
|
173
|
-
active={activeTab() === "context"}
|
|
174
|
-
onClick={() => setActiveTab("context")}
|
|
175
|
-
/>
|
|
176
|
-
<TabButton
|
|
177
|
-
theme={theme}
|
|
178
|
-
label="Stats"
|
|
179
|
-
active={activeTab() === "stats"}
|
|
180
|
-
onClick={() => setActiveTab("stats")}
|
|
181
|
-
/>
|
|
182
|
-
<TabButton
|
|
183
|
-
theme={theme}
|
|
184
|
-
label="Help"
|
|
185
|
-
active={activeTab() === "help"}
|
|
186
|
-
onClick={() => setActiveTab("help")}
|
|
187
|
-
/>
|
|
188
|
-
</box>
|
|
189
|
-
|
|
190
|
-
{/* Content */}
|
|
191
|
-
<Show
|
|
192
|
-
when={!loading()}
|
|
193
|
-
fallback={
|
|
194
|
-
<box padding={2}>
|
|
195
|
-
<text fg={theme.textMuted}>Loading...</text>
|
|
196
|
-
</box>
|
|
197
|
-
}
|
|
198
|
-
>
|
|
199
|
-
<Show
|
|
200
|
-
when={data()}
|
|
201
|
-
fallback={
|
|
202
|
-
<box padding={2}>
|
|
203
|
-
<text fg={theme.error}>Failed to load panel data</text>
|
|
204
|
-
</box>
|
|
205
|
-
}
|
|
206
|
-
>
|
|
207
|
-
{activeTab() === "context" && (
|
|
208
|
-
<ContextTab data={data()!} theme={theme} formatTokens={formatTokens} />
|
|
209
|
-
)}
|
|
210
|
-
{activeTab() === "stats" && (
|
|
211
|
-
<StatsTab data={data()!} theme={theme} formatTokens={formatTokens} />
|
|
212
|
-
)}
|
|
213
|
-
{activeTab() === "help" && <HelpTab theme={theme} />}
|
|
214
|
-
</Show>
|
|
215
|
-
</Show>
|
|
216
|
-
</SlimFrame>
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ─── Tab Button ─────────────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
function TabButton(props: {
|
|
223
|
-
theme: any
|
|
224
|
-
label: string
|
|
225
|
-
active: boolean
|
|
226
|
-
onClick: () => void
|
|
227
|
-
}) {
|
|
228
|
-
return (
|
|
229
|
-
<box
|
|
230
|
-
paddingLeft={1}
|
|
231
|
-
paddingRight={1}
|
|
232
|
-
backgroundColor={props.active ? props.theme.primary : props.theme.backgroundElement}
|
|
233
|
-
onMouseUp={props.onClick}
|
|
234
|
-
>
|
|
235
|
-
<text fg={props.active ? props.theme.background : props.theme.text}>
|
|
236
|
-
{props.label}
|
|
237
|
-
</text>
|
|
238
|
-
</box>
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ─── Context Tab ────────────────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
function ContextTab(props: { data: PanelData; theme: any; formatTokens: (n: number) => string }) {
|
|
245
|
-
const statusIcon = () =>
|
|
246
|
-
props.data.status === "healthy" ? "🟢" : props.data.status === "warning" ? "🟡" : "🔴"
|
|
247
|
-
|
|
248
|
-
const statusColor = () =>
|
|
249
|
-
props.data.status === "healthy"
|
|
250
|
-
? props.theme.success
|
|
251
|
-
: props.data.status === "warning"
|
|
252
|
-
? props.theme.warning
|
|
253
|
-
: props.theme.error
|
|
254
|
-
|
|
255
|
-
return (
|
|
256
|
-
<box flexDirection="column" gap={1}>
|
|
257
|
-
<Card theme={props.theme} title="Status">
|
|
258
|
-
<Metric
|
|
259
|
-
theme={props.theme}
|
|
260
|
-
label="Status"
|
|
261
|
-
value={`${statusIcon()} ${props.data.status.toUpperCase()}`}
|
|
262
|
-
/>
|
|
263
|
-
<Progress
|
|
264
|
-
theme={props.theme}
|
|
265
|
-
label="Context"
|
|
266
|
-
value={props.data.currentTokens}
|
|
267
|
-
total={props.data.maxTokens}
|
|
268
|
-
color={props.data.status === "critical" ? "error" : props.data.status === "warning" ? "warning" : "primary"}
|
|
269
|
-
detail={`${props.formatTokens(props.data.currentTokens)} / ${props.formatTokens(props.data.maxTokens)} tokens`}
|
|
270
|
-
/>
|
|
271
|
-
</Card>
|
|
272
|
-
|
|
273
|
-
<Card theme={props.theme} title="Messages">
|
|
274
|
-
<Metric theme={props.theme} label="User" value={`${props.data.userMessages}`} />
|
|
275
|
-
<Metric theme={props.theme} label="Assistant" value={`${props.data.assistantMessages}`} />
|
|
276
|
-
<Metric theme={props.theme} label="Tool calls" value={`${props.data.toolCalls}`} />
|
|
277
|
-
<Metric theme={props.theme} label="Results" value={`${props.data.toolResults}`} />
|
|
278
|
-
</Card>
|
|
279
|
-
|
|
280
|
-
<Card theme={props.theme} title="Recommendations">
|
|
281
|
-
<box flexDirection="column">
|
|
282
|
-
<For each={props.data.recommendations}>
|
|
283
|
-
{(rec) => <text>• {rec}</text>}
|
|
284
|
-
</For>
|
|
285
|
-
</box>
|
|
286
|
-
</Card>
|
|
287
|
-
</box>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── Stats Tab ──────────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
function StatsTab(props: { data: PanelData; theme: any; formatTokens: (n: number) => string }) {
|
|
294
|
-
return (
|
|
295
|
-
<box flexDirection="column" gap={1}>
|
|
296
|
-
<Card theme={props.theme} title="Compression">
|
|
297
|
-
<Metric theme={props.theme} label="Count" value={`${props.data.compressionCount}`} />
|
|
298
|
-
<Metric
|
|
299
|
-
theme={props.theme}
|
|
300
|
-
label="Avg ratio"
|
|
301
|
-
value={`${(props.data.averageRatio * 100).toFixed(1)}%`}
|
|
302
|
-
/>
|
|
303
|
-
<Metric
|
|
304
|
-
theme={props.theme}
|
|
305
|
-
label="Tokens saved"
|
|
306
|
-
value={props.formatTokens(props.data.totalTokensSaved)}
|
|
307
|
-
/>
|
|
308
|
-
</Card>
|
|
309
|
-
|
|
310
|
-
<Card theme={props.theme} title="Cost">
|
|
311
|
-
<Metric
|
|
312
|
-
theme={props.theme}
|
|
313
|
-
label="Current"
|
|
314
|
-
value={`$${props.data.estimatedCost.toFixed(4)}`}
|
|
315
|
-
/>
|
|
316
|
-
<Metric
|
|
317
|
-
theme={props.theme}
|
|
318
|
-
label="Saved"
|
|
319
|
-
value={`$${props.data.costSaved.toFixed(4)}`}
|
|
320
|
-
/>
|
|
321
|
-
<Metric theme={props.theme} label="Model" value={props.data.model} />
|
|
322
|
-
</Card>
|
|
323
|
-
</box>
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ─── Help Tab ───────────────────────────────────────────────────────────────
|
|
328
|
-
|
|
329
|
-
function HelpTab(props: { theme: any }) {
|
|
330
|
-
return (
|
|
331
|
-
<box flexDirection="column" gap={1}>
|
|
332
|
-
<Card theme={props.theme} title="Commands">
|
|
333
|
-
<text>/panel - Open this panel</text>
|
|
334
|
-
<text>/compress [focus] - Run compression</text>
|
|
335
|
-
</Card>
|
|
336
|
-
|
|
337
|
-
<Card theme={props.theme} title="Tool Usage">
|
|
338
|
-
<text>compress({"{ focus: \"old exploration\" }"})</text>
|
|
339
|
-
<text>compress({"{ focus: \"tasks\", mode: \"range\", start: 0, end: 50 }"})</text>
|
|
340
|
-
<text>compress({"{ focus: \"database\", mode: \"topic\", topic: \"db\" }"})</text>
|
|
341
|
-
</Card>
|
|
342
|
-
|
|
343
|
-
<Card theme={props.theme} title="Configuration">
|
|
344
|
-
<text>~/.config/opencode/slim.jsonc</text>
|
|
345
|
-
</Card>
|
|
346
|
-
</box>
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ─── Data Fetching ──────────────────────────────────────────────────────────
|
|
351
|
-
|
|
352
|
-
async function fetchPanelData(
|
|
353
|
-
api: TuiPluginApi,
|
|
354
|
-
config: SlimConfig,
|
|
355
|
-
): Promise<PanelData> {
|
|
356
|
-
const currentRoute = api.route.current
|
|
357
|
-
|
|
358
|
-
let sessionId: string | undefined
|
|
359
|
-
if (currentRoute.name === "session") {
|
|
360
|
-
sessionId = currentRoute.params?.sessionID as string | undefined
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (!sessionId) {
|
|
364
|
-
return {
|
|
365
|
-
currentTokens: 0,
|
|
366
|
-
maxTokens: resolveTokenLimit(config.compress.maxContextLimit, 200000),
|
|
367
|
-
usagePercent: 0,
|
|
368
|
-
status: "healthy",
|
|
369
|
-
userMessages: 0,
|
|
370
|
-
assistantMessages: 0,
|
|
371
|
-
toolCalls: 0,
|
|
372
|
-
toolResults: 0,
|
|
373
|
-
compressionCount: 0,
|
|
374
|
-
averageRatio: 0,
|
|
375
|
-
totalTokensSaved: 0,
|
|
376
|
-
estimatedCost: 0,
|
|
377
|
-
costSaved: 0,
|
|
378
|
-
model: "unknown",
|
|
379
|
-
recommendations: ["No active session. Start a conversation to see context usage."],
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const messages = api.state.session.messages(sessionId)
|
|
384
|
-
|
|
385
|
-
// Calculate token usage from messages
|
|
386
|
-
let currentTokens = 0
|
|
387
|
-
let userMessages = 0
|
|
388
|
-
let assistantMessages = 0
|
|
389
|
-
let toolCalls = 0
|
|
390
|
-
let toolResults = 0
|
|
391
|
-
let model = "unknown"
|
|
392
|
-
|
|
393
|
-
for (const msg of messages) {
|
|
394
|
-
// Get role from message
|
|
395
|
-
const role = msg.role
|
|
396
|
-
if (role === "user") {
|
|
397
|
-
userMessages++
|
|
398
|
-
} else if (role === "assistant") {
|
|
399
|
-
assistantMessages++
|
|
400
|
-
// Get model from assistant message
|
|
401
|
-
const assistantMsg = msg as AssistantMessage
|
|
402
|
-
if (assistantMsg.providerID && assistantMsg.modelID) {
|
|
403
|
-
model = `${assistantMsg.providerID}/${assistantMsg.modelID}`
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Get parts for this message
|
|
408
|
-
const parts = api.state.part(msg.id)
|
|
409
|
-
|
|
410
|
-
for (const part of parts) {
|
|
411
|
-
if (part.type === "text") {
|
|
412
|
-
const textPart = part as any
|
|
413
|
-
currentTokens += Math.ceil((textPart.text?.length || 0) / 4)
|
|
414
|
-
} else if (part.type === "tool") {
|
|
415
|
-
const toolPart = part as any
|
|
416
|
-
const status = toolPart.state?.type
|
|
417
|
-
if (status === "completed") {
|
|
418
|
-
toolResults++
|
|
419
|
-
} else if (status === "pending" || status === "running") {
|
|
420
|
-
toolCalls++
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const maxTokens = resolveTokenLimit(config.compress.maxContextLimit, 200000)
|
|
427
|
-
const usagePercent = (currentTokens / maxTokens) * 100
|
|
428
|
-
|
|
429
|
-
let statusLevel: "healthy" | "warning" | "critical" = "healthy"
|
|
430
|
-
if (usagePercent > 90) statusLevel = "critical"
|
|
431
|
-
else if (usagePercent > 70) statusLevel = "warning"
|
|
432
|
-
|
|
433
|
-
const recommendations: string[] = []
|
|
434
|
-
if (usagePercent > 80) {
|
|
435
|
-
recommendations.push("Context usage is high. Consider compressing older messages.")
|
|
436
|
-
}
|
|
437
|
-
if (usagePercent > 90) {
|
|
438
|
-
recommendations.push("Context nearly full! Run compress immediately.")
|
|
439
|
-
}
|
|
440
|
-
if (recommendations.length === 0) {
|
|
441
|
-
recommendations.push("Context is healthy. No action needed.")
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
currentTokens,
|
|
446
|
-
maxTokens,
|
|
447
|
-
usagePercent,
|
|
448
|
-
status: statusLevel,
|
|
449
|
-
userMessages,
|
|
450
|
-
assistantMessages,
|
|
451
|
-
toolCalls,
|
|
452
|
-
toolResults,
|
|
453
|
-
compressionCount: 0,
|
|
454
|
-
averageRatio: 0,
|
|
455
|
-
totalTokensSaved: 0,
|
|
456
|
-
estimatedCost: (currentTokens / 1000) * 0.003,
|
|
457
|
-
costSaved: 0,
|
|
458
|
-
model,
|
|
459
|
-
recommendations,
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function resolveTokenLimit(value: number | string, contextLimit: number): number {
|
|
464
|
-
if (typeof value === "number") return value
|
|
465
|
-
const percent = parseFloat(value.replace("%", "")) / 100
|
|
466
|
-
return Math.floor(contextLimit * percent)
|
|
467
|
-
}
|
package/src/lib/tui/modals.tsx
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
4
|
-
import type { SlimConfig } from "../types"
|
|
5
|
-
import { PanelDialog } from "./dialogs"
|
|
6
|
-
|
|
7
|
-
export function openPanelModal(api: TuiPluginApi, config: SlimConfig): void {
|
|
8
|
-
api.ui.dialog.setSize("xlarge")
|
|
9
|
-
api.ui.dialog.replace(() => (
|
|
10
|
-
<PanelDialog api={api} config={config} />
|
|
11
|
-
))
|
|
12
|
-
}
|
package/src/tui.tsx
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
4
|
-
import type { SlimConfig } from "./lib/types"
|
|
5
|
-
import { loadConfig } from "./lib/tui/data"
|
|
6
|
-
import { openPanelModal } from "./lib/tui/modals"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Register slash commands using the v2 keymap API.
|
|
10
|
-
* This plugin is v2-only.
|
|
11
|
-
*/
|
|
12
|
-
function registerSlashCommands(api: any, config: SlimConfig): void {
|
|
13
|
-
// v2 way: keymap.registerLayer
|
|
14
|
-
if (api.keymap && typeof api.keymap.registerLayer === "function") {
|
|
15
|
-
api.keymap.registerLayer({
|
|
16
|
-
mode: "global",
|
|
17
|
-
priority: 10,
|
|
18
|
-
commands: [
|
|
19
|
-
{
|
|
20
|
-
name: "slim.panel",
|
|
21
|
-
title: "Open Slim Panel",
|
|
22
|
-
group: "Slim",
|
|
23
|
-
slash: { name: "panel" },
|
|
24
|
-
enabled: () => true,
|
|
25
|
-
suggested: true,
|
|
26
|
-
run: () => {
|
|
27
|
-
openPanelModal(api, config)
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "slim.compress",
|
|
32
|
-
title: "Compress Context",
|
|
33
|
-
group: "Slim",
|
|
34
|
-
slash: { name: "compress" },
|
|
35
|
-
enabled: () => true,
|
|
36
|
-
suggested: true,
|
|
37
|
-
run: () => {
|
|
38
|
-
api.ui.toast({
|
|
39
|
-
title: "Slim",
|
|
40
|
-
message:
|
|
41
|
-
"Use the compress tool: compress({ focus: 'your focus' })",
|
|
42
|
-
variant: "info",
|
|
43
|
-
})
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const tui: TuiPluginModule["tui"] = async (api) => {
|
|
52
|
-
const config = loadConfig(api as any)
|
|
53
|
-
if (!config.enabled) return
|
|
54
|
-
|
|
55
|
-
registerSlashCommands(api, config)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default {
|
|
59
|
-
id: "opencodev2-slim",
|
|
60
|
-
tui,
|
|
61
|
-
} satisfies TuiPluginModule
|