@serkanalgur/opencodev2-slim 2.0.8 → 2.0.11

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/README.md +20 -0
  2. package/package.json +5 -2
  3. package/src/tui.tsx +151 -37
package/README.md CHANGED
@@ -141,6 +141,26 @@ Note: Compression is performed by the AI assistant using the `compress` tool. Th
141
141
 
142
142
  ## Changelog
143
143
 
144
+ ### 2.0.11
145
+
146
+ - Fix CI publish: add `solid-js`, `@opentui/core`, `@opentui/solid` to `devDependencies`.
147
+ The workflow runs `npm ci --legacy-peer-deps`, which skips peer deps, so loading
148
+ `@opencode/plugin/tui` failed with `Cannot find package 'solid-js'`.
149
+
150
+ ### 2.0.10
151
+
152
+ - Fix `/panel` output showing `User tokens: 0`: user/system messages carry their text
153
+ on a top-level `text` field (not inside `content`), which `deriveStats` now captures.
154
+ - Add regression tests for CLI panel stats.
155
+
156
+ ### 2.0.9
157
+
158
+ - `feat(panel-as-message)`: `/panel` and `slim-panel` now print the context stats as plain
159
+ text into the message stream via `client.session.synthetic`, instead of taking over
160
+ OpenCode's own panel UI (`session.panel` slot + `ui.panel.open` removed).
161
+ - The TUI panel now derives its own stats (token estimate, role breakdown, tool/compaction
162
+ counts) directly from the session transcript rather than deferring to the server tool.
163
+
144
164
  ### 2.0.8
145
165
 
146
166
  - Fix `keymap.provider is missing` in the CLI plugin: register the keymap layer inside
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencodev2-slim",
3
- "version": "2.0.8",
3
+ "version": "2.0.11",
4
4
  "description": "Smart context management plugin for OpenCode v2 - semantic compression, cost-aware pruning, adaptive thresholds",
5
5
  "keywords": [
6
6
  "opencode",
@@ -49,10 +49,13 @@
49
49
  "jsonc-parser": "^3.3.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@opencode/plugin": "^2.0.0",
53
52
  "@opencode-ai/sdk": "^1.18.29",
53
+ "@opencode/plugin": "^2.0.0",
54
+ "@opentui/core": "^0.5.11",
55
+ "@opentui/solid": "^0.5.11",
54
56
  "@types/node": "^22.0.0",
55
57
  "prettier": "^3.4.0",
58
+ "solid-js": "^1.9.12",
56
59
  "tsx": "^4.19.0",
57
60
  "typescript": "^5.7.0"
58
61
  },
package/src/tui.tsx CHANGED
@@ -1,41 +1,133 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { Show } from "solid-js"
3
2
  import { Plugin } from "@opencode/plugin/tui"
4
- import type { PanelInput } from "@opencode/plugin/tui/context"
5
3
 
6
- const PANEL_NAME = "opencodev2-slim.panel"
4
+ // Rough token estimate: ~4 chars per token.
5
+ function estimateTokens(text: string): number {
6
+ return Math.ceil(text.length / 4)
7
+ }
8
+
9
+ interface PanelStats {
10
+ totalMessages: number
11
+ userMessages: number
12
+ assistantMessages: number
13
+ toolCalls: number
14
+ systemMessages: number
15
+ compactionCount: number
16
+ totalTokens: number
17
+ tokensByRole: { user: number; assistant: number; system: number }
18
+ }
19
+
20
+ function emptyStats(): PanelStats {
21
+ return {
22
+ totalMessages: 0,
23
+ userMessages: 0,
24
+ assistantMessages: 0,
25
+ toolCalls: 0,
26
+ systemMessages: 0,
27
+ compactionCount: 0,
28
+ totalTokens: 0,
29
+ tokensByRole: { user: 0, assistant: 0, system: 0 },
30
+ }
31
+ }
32
+
33
+ // Derives context-usage stats from the session transcript.
34
+ export function deriveStats(messages: readonly unknown[]): PanelStats {
35
+ const stats = emptyStats()
36
+ for (const raw of messages) {
37
+ const m = raw as {
38
+ type?: string
39
+ content?: Array<{ type?: string; text?: string }>
40
+ summary?: string
41
+ }
42
+ let text = ""
43
+ let role: "user" | "assistant" | "system" = "assistant"
7
44
 
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>
45
+ const t = m?.type
46
+ if (t === "user" || t === "synthetic" || t === "shell") {
47
+ role = "user"
48
+ } else if (t === "assistant") {
49
+ role = "assistant"
50
+ } else if (t === "system" || t === "skill") {
51
+ role = "system"
52
+ } else if (t === "compaction") {
53
+ role = "system"
54
+ stats.compactionCount++
55
+ text = m.summary || ""
56
+ }
57
+
58
+ // User/system messages carry their text on a top-level `text` field
59
+ // (not inside a `content` array). Capture it too so their tokens count.
60
+ if (role !== "assistant" && typeof (m as any).text === "string") {
61
+ text += (m as any).text
62
+ }
63
+
64
+ if (Array.isArray(m.content)) {
65
+ for (const part of m.content) {
66
+ if (part?.type === "text" && typeof part.text === "string") {
67
+ text += part.text
68
+ } else if (part?.type === "tool") {
69
+ stats.toolCalls++
70
+ if (typeof part.text === "string") text += part.text
71
+ }
72
+ }
73
+ }
74
+
75
+ const tokens = estimateTokens(text)
76
+ switch (role) {
77
+ case "user":
78
+ stats.userMessages++
79
+ stats.tokensByRole.user += tokens
80
+ break
81
+ case "assistant":
82
+ stats.assistantMessages++
83
+ stats.tokensByRole.assistant += tokens
84
+ break
85
+ case "system":
86
+ stats.systemMessages++
87
+ stats.tokensByRole.system += tokens
88
+ break
89
+ }
90
+ stats.totalTokens += tokens
91
+ }
92
+ stats.totalMessages = messages.length
93
+ return stats
94
+ }
95
+
96
+ // Builds a human-readable panel as plain text (injected into the message stream).
97
+ function renderPanelText(sessionID: string, stats: PanelStats): string {
98
+ const lines: string[] = []
99
+ lines.push("┌─────────────────────────────────────────────────────────────┐")
100
+ lines.push("│ SLIM CONTEXT PANEL │")
101
+ lines.push("├─────────────────────────────────────────────────────────────┤")
102
+ lines.push(`│ Session: ${sessionID.slice(0, 40)}`)
103
+ lines.push(`│ Messages: ${stats.totalMessages}`)
104
+ lines.push(
105
+ `│ User: ${stats.userMessages} Assistant: ${stats.assistantMessages} System: ${stats.systemMessages}`,
22
106
  )
107
+ lines.push(`│ Tool calls: ${stats.toolCalls} Compactions: ${stats.compactionCount}`)
108
+ lines.push(`│ Tokens (est): User ${stats.tokensByRole.user} | Assistant ${stats.tokensByRole.assistant} | System ${stats.tokensByRole.system}`)
109
+ lines.push(`│ Total token estimate: ${stats.totalTokens}`)
110
+ lines.push("└─────────────────────────────────────────────────────────────┘")
111
+ return lines.join("\n")
112
+ }
113
+
114
+ // Resolves the "current" session: the focused session if any, else the most recent.
115
+ function resolveCurrentSession(context: any): string | null {
116
+ const sessions = context.data.session.list() || []
117
+ if (sessions.length === 0) return null
118
+ // Prefer the focused session if exposed; otherwise fall back to the first.
119
+ const focused = context.router?.current?.()
120
+ if (focused && typeof focused === "object" && "sessionID" in focused) {
121
+ return focused.sessionID as string
122
+ }
123
+ return sessions[0].id
23
124
  }
24
125
 
25
126
  export default Plugin.define({
26
127
  id: "opencodev2-slim.cli",
27
128
  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
- // Register the command/shortcut inside the "app" slot render, where the
38
- // keymap provider is available (consistent with OpenCode V2 CLI plugins).
129
+ // Register the command inside the "app" slot render, where the keymap
130
+ // provider is available (consistent with OpenCode V2 CLI plugins).
39
131
  context.ui.slot({
40
132
  append: "app",
41
133
  render: () => {
@@ -51,16 +143,40 @@ export default Plugin.define({
51
143
  slash: { name: "panel", aliases: ["slim-panel"] },
52
144
  enabled: true,
53
145
  suggested: true,
54
- run: async () => {
55
- const opened = context.ui.panel.open(PANEL_NAME, {
56
- presentation: "panel",
57
- })
58
- if (!opened) {
146
+ run: async (input: unknown, event: unknown) => {
147
+ const sessionID =
148
+ resolveCurrentSession(context) ||
149
+ (event && typeof event === "object" && "sessionID" in event
150
+ ? (event as any).sessionID
151
+ : null)
152
+
153
+ if (!sessionID) {
59
154
  context.ui.toast.show({
60
155
  title: "Slim Panel",
61
156
  message: "No active session found. Open a session first.",
62
157
  variant: "warning",
63
158
  })
159
+ return
160
+ }
161
+
162
+ try {
163
+ const messages =
164
+ context.data.session.message.list(sessionID) || []
165
+ const stats = deriveStats(messages)
166
+ const text = renderPanelText(sessionID, stats)
167
+ // Inject the panel as plain text into the session stream,
168
+ // so it doesn't take over OpenCode's own panel UI.
169
+ await context.client.session.synthetic({
170
+ sessionID,
171
+ text,
172
+ description: "slim-panel",
173
+ })
174
+ } catch (e) {
175
+ context.ui.toast.show({
176
+ title: "Slim Panel",
177
+ message: `Error: ${e instanceof Error ? e.message : e}`,
178
+ variant: "error",
179
+ })
64
180
  }
65
181
  },
66
182
  },
@@ -72,13 +188,11 @@ export default Plugin.define({
72
188
 
73
189
  context.ui.toast.show({
74
190
  title: "Slim Plugin",
75
- message: "CLI loaded. Use /panel to show the context panel.",
191
+ message: "Use /panel to print the context panel as a message.",
76
192
  variant: "success",
77
193
  duration: 3000,
78
194
  })
79
195
 
80
- return () => {
81
- // Cleanup
82
- }
196
+ return () => {}
83
197
  },
84
198
  })