@serkanalgur/opencodev2-slim 2.1.0 → 2.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +2 -2
  3. package/src/tui.tsx +266 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencodev2-slim",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Smart context management plugin for OpenCode v2 - semantic compression, cost-aware pruning, adaptive thresholds",
5
5
  "keywords": [
6
6
  "opencode",
package/src/index.ts CHANGED
@@ -279,7 +279,7 @@ export default Plugin.define({
279
279
  required: ["focus"],
280
280
  additionalProperties: false,
281
281
  },
282
- options: { codemode: true },
282
+ options: { codemode: false },
283
283
  execute: async (input, context) => {
284
284
  const args = input as {
285
285
  focus: string
@@ -425,7 +425,7 @@ export default Plugin.define({
425
425
  properties: {},
426
426
  additionalProperties: false,
427
427
  },
428
- options: { codemode: true },
428
+ options: { codemode: false },
429
429
  execute: async (_input, context) => {
430
430
  const sessionId = context.sessionID
431
431
  const config = getConfig(sessionId)
package/src/tui.tsx CHANGED
@@ -157,17 +157,38 @@ async function measureSession(context: any, sessionID: string): Promise<Measured
157
157
  (typeof tokens.reasoning === "number" ? tokens.reasoning : 0) +
158
158
  (typeof tokens.cache?.read === "number" ? tokens.cache.read : 0) +
159
159
  (typeof tokens.cache?.write === "number" ? tokens.cache.write : 0)
160
- const contextLimit: number =
161
- typeof info.model?.limit?.context === "number" && info.model.limit.context > 0
162
- ? info.model.limit.context
163
- : 200000
160
+
161
+ // Resolve context limit: try model.list() first, then session info, then default.
162
+ let contextLimit = 200000
163
+ const modelID: string = info.model?.id || ""
164
+ const providerID: string = info.model?.providerID || ""
165
+
166
+ try {
167
+ const modelList: any = await context.client.model.list()
168
+ const models: any[] = modelList?.data ?? modelList ?? []
169
+ // Find by exact match (providerID/modelID), then by modelID alone
170
+ const found = models.find(
171
+ (m: any) => m.providerID === providerID && m.modelID === modelID,
172
+ ) || models.find((m: any) => m.modelID === modelID)
173
+ if (found?.limit?.context && found.limit.context > 0) {
174
+ contextLimit = found.limit.context
175
+ }
176
+ } catch {
177
+ // Fall through to session info or default
178
+ }
179
+
180
+ // Fallback: session info might have model.limit.context
181
+ if (contextLimit === 200000 && info.model?.limit?.context && info.model.limit.context > 0) {
182
+ contextLimit = info.model.limit.context
183
+ }
184
+
164
185
  const usagePercent =
165
186
  contextLimit > 0 ? Math.min(100, Math.round((tokenCount / contextLimit) * 100)) : 0
166
187
  return {
167
188
  tokens: tokenCount,
168
189
  cost: typeof info.cost === "number" ? info.cost : 0,
169
190
  contextLimit,
170
- model: info.model?.id || "unknown",
191
+ model: modelID || "unknown",
171
192
  usagePercent,
172
193
  }
173
194
  } catch {
@@ -178,7 +199,7 @@ async function measureSession(context: any, sessionID: string): Promise<Measured
178
199
  export default Plugin.define({
179
200
  id: "opencodev2-slim.cli",
180
201
  setup(context) {
181
- // Register the command inside the "app" slot render, where the keymap
202
+ // Register commands inside the "app" slot render, where the keymap
182
203
  // provider is available (consistent with OpenCode V2 CLI plugins).
183
204
  context.ui.slot({
184
205
  append: "app",
@@ -187,6 +208,7 @@ export default Plugin.define({
187
208
  mode: "global",
188
209
  priority: 10,
189
210
  commands: [
211
+ // ─── /panel ────────────────────────────────────
190
212
  {
191
213
  id: "opencodev2-slim.panel",
192
214
  title: "Show Slim Context Panel",
@@ -212,16 +234,12 @@ export default Plugin.define({
212
234
  }
213
235
 
214
236
  try {
215
- // Make sure the cached transcript is loaded before reading it.
216
237
  await context.data.session.message.sync(sessionID)
217
238
  const messages =
218
239
  context.data.session.message.list(sessionID) || []
219
- // Prefer live server-measured context numbers when available.
220
240
  const real = await measureSession(context, sessionID)
221
241
  const stats = deriveStats(messages)
222
242
  const text = renderPanelText(sessionID, stats, real)
223
- // Inject the panel as plain text into the session stream,
224
- // so it doesn't take over OpenCode's own panel UI.
225
243
  await context.client.session.synthetic({
226
244
  sessionID,
227
245
  text,
@@ -236,6 +254,241 @@ export default Plugin.define({
236
254
  }
237
255
  },
238
256
  },
257
+
258
+ // ─── /compress ─────────────────────────────────
259
+ {
260
+ id: "opencodev2-slim.compress",
261
+ title: "Compress Context",
262
+ group: "Slim",
263
+ palette: true,
264
+ slash: {
265
+ name: "compress",
266
+ aliases: ["slim-compress"],
267
+ args: [
268
+ {
269
+ name: "focus",
270
+ description: "What to compress (e.g., 'old exploration')",
271
+ required: false,
272
+ },
273
+ {
274
+ name: "mode",
275
+ description: "Compression mode: auto, range, or topic",
276
+ required: false,
277
+ },
278
+ {
279
+ name: "keepRecent",
280
+ description: "Number of recent messages to keep (default: 5)",
281
+ required: false,
282
+ },
283
+ ],
284
+ },
285
+ enabled: true,
286
+ suggested: true,
287
+ run: async (input: unknown, event: unknown) => {
288
+ const sessionID =
289
+ resolveCurrentSession(context) ||
290
+ (event && typeof event === "object" && "sessionID" in event
291
+ ? (event as any).sessionID
292
+ : null)
293
+
294
+ if (!sessionID) {
295
+ context.ui.toast.show({
296
+ title: "Slim Compress",
297
+ message: "No active session found.",
298
+ variant: "warning",
299
+ })
300
+ return
301
+ }
302
+
303
+ try {
304
+ const args = (input as any) || {}
305
+ const focus = args.focus || "user-requested compression"
306
+ const mode = args.mode || "auto"
307
+ const keepRecent = args.keepRecent ?? 5
308
+
309
+ // Measure current state first
310
+ const real = await measureSession(context, sessionID)
311
+
312
+ const statusLine = real
313
+ ? `${real.tokens.toLocaleString()} tokens (${real.usagePercent}% of ${real.contextLimit.toLocaleString()})`
314
+ : "unknown"
315
+
316
+ const text = [
317
+ `**Slim Compress**`,
318
+ ``,
319
+ `**Current state:** ${statusLine}`,
320
+ ``,
321
+ `Ready to compress with:`,
322
+ `- **Focus:** ${focus}`,
323
+ `- **Mode:** ${mode}`,
324
+ `- **Keep recent:** ${keepRecent} messages`,
325
+ ``,
326
+ `> The assistant will now call the compress tool.`,
327
+ `> Or type: \`compress({ focus: "${focus}", mode: "${mode}", keepRecent: ${keepRecent} })\``,
328
+ ].join("\n")
329
+
330
+ await context.client.session.synthetic({
331
+ sessionID,
332
+ text,
333
+ description: "slim-compress",
334
+ })
335
+
336
+ context.ui.toast.show({
337
+ title: "Slim Compress",
338
+ message: `Compression ready: "${focus}". Assistant will process it.`,
339
+ variant: "success",
340
+ duration: 3000,
341
+ })
342
+ } catch (e) {
343
+ context.ui.toast.show({
344
+ title: "Slim Compress",
345
+ message: `Error: ${e instanceof Error ? e.message : e}`,
346
+ variant: "error",
347
+ })
348
+ }
349
+ },
350
+ },
351
+
352
+ // ─── /status ──────────────────────────────────
353
+ {
354
+ id: "opencodev2-slim.status",
355
+ title: "Show Compact Status",
356
+ group: "Slim",
357
+ palette: true,
358
+ slash: { name: "status", aliases: ["slim-status"] },
359
+ enabled: true,
360
+ suggested: false,
361
+ run: async (input: unknown, event: unknown) => {
362
+ const sessionID =
363
+ resolveCurrentSession(context) ||
364
+ (event && typeof event === "object" && "sessionID" in event
365
+ ? (event as any).sessionID
366
+ : null)
367
+
368
+ if (!sessionID) {
369
+ context.ui.toast.show({
370
+ title: "Slim Status",
371
+ message: "No active session found.",
372
+ variant: "warning",
373
+ })
374
+ return
375
+ }
376
+
377
+ try {
378
+ const real = await measureSession(context, sessionID)
379
+ if (!real) {
380
+ context.ui.toast.show({
381
+ title: "Slim Status",
382
+ message: "Could not measure session.",
383
+ variant: "warning",
384
+ })
385
+ return
386
+ }
387
+
388
+ const status =
389
+ real.usagePercent >= 90
390
+ ? "🔴 CRITICAL"
391
+ : real.usagePercent >= 70
392
+ ? "🟡 WARNING"
393
+ : "🟢 HEALTHY"
394
+
395
+ const text = [
396
+ `**Context Status:** ${status}`,
397
+ `**Usage:** ${real.tokens.toLocaleString()} / ${real.contextLimit.toLocaleString()} tokens (${real.usagePercent}%)`,
398
+ `**Model:** ${real.model}`,
399
+ real.cost > 0 ? `**Cost:** $${real.cost.toFixed(4)}` : "",
400
+ ]
401
+ .filter(Boolean)
402
+ .join("\n")
403
+
404
+ await context.client.session.synthetic({
405
+ sessionID,
406
+ text,
407
+ description: "slim-status",
408
+ })
409
+ } catch (e) {
410
+ context.ui.toast.show({
411
+ title: "Slim Status",
412
+ message: `Error: ${e instanceof Error ? e.message : e}`,
413
+ variant: "error",
414
+ })
415
+ }
416
+ },
417
+ },
418
+
419
+ // ─── /slim-debug ──────────────────────────────
420
+ {
421
+ id: "opencodev2-slim.debug",
422
+ title: "Toggle Slim Debug Mode",
423
+ group: "Slim",
424
+ palette: true,
425
+ slash: { name: "slim-debug", aliases: ["debug-slim"] },
426
+ enabled: true,
427
+ suggested: false,
428
+ run: async (input: unknown, event: unknown) => {
429
+ const sessionID =
430
+ resolveCurrentSession(context) ||
431
+ (event && typeof event === "object" && "sessionID" in event
432
+ ? (event as any).sessionID
433
+ : null)
434
+
435
+ if (!sessionID) {
436
+ context.ui.toast.show({
437
+ title: "Slim Debug",
438
+ message: "No active session found.",
439
+ variant: "warning",
440
+ })
441
+ return
442
+ }
443
+
444
+ try {
445
+ // Read current config
446
+ const configPath = `${process.env.HOME || "~"}/.config/opencode/slim.jsonc`
447
+ const fs = await import("fs")
448
+ let debug = false
449
+ if (fs.existsSync(configPath)) {
450
+ const content = fs.readFileSync(configPath, "utf-8")
451
+ const match = content.match(/"debug"\s*:\s*(true|false)/)
452
+ if (match) debug = match[1] === "true"
453
+ }
454
+
455
+ // Toggle
456
+ debug = !debug
457
+
458
+ // Write back
459
+ const { parse } = await import("jsonc-parser")
460
+ let config: any = {}
461
+ if (fs.existsSync(configPath)) {
462
+ config = parse(fs.readFileSync(configPath, "utf-8")) || {}
463
+ }
464
+ config.debug = debug
465
+
466
+ const dir = `${process.env.HOME || "~"}/.config/opencode`
467
+ if (!fs.existsSync(dir)) {
468
+ fs.mkdirSync(dir, { recursive: true })
469
+ }
470
+ fs.writeFileSync(
471
+ configPath,
472
+ JSON.stringify(config, null, 2),
473
+ "utf-8",
474
+ )
475
+
476
+ const text = `**Slim Debug Mode:** ${debug ? "ON 🔴" : "OFF ⚪"}\n\nDebug logs will ${debug ? "now" : "no longer"} appear in the console.`
477
+
478
+ await context.client.session.synthetic({
479
+ sessionID,
480
+ text,
481
+ description: "slim-debug",
482
+ })
483
+ } catch (e) {
484
+ context.ui.toast.show({
485
+ title: "Slim Debug",
486
+ message: `Error: ${e instanceof Error ? e.message : e}`,
487
+ variant: "error",
488
+ })
489
+ }
490
+ },
491
+ },
239
492
  ],
240
493
  }))
241
494
  return null
@@ -243,10 +496,10 @@ export default Plugin.define({
243
496
  })
244
497
 
245
498
  context.ui.toast.show({
246
- title: "Slim Plugin",
247
- message: "Use /panel to print the context panel as a message.",
499
+ title: "Slim Plugin v2.1.0",
500
+ message: "Commands: /panel, /compress, /status, /slim-debug",
248
501
  variant: "success",
249
- duration: 3000,
502
+ duration: 4000,
250
503
  })
251
504
 
252
505
  return () => {}