@serkanalgur/opencodev2-slim 2.0.7 → 2.0.9

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 +13 -0
  2. package/package.json +1 -1
  3. package/src/tui.tsx +171 -55
package/README.md CHANGED
@@ -141,6 +141,19 @@ Note: Compression is performed by the AI assistant using the `compress` tool. Th
141
141
 
142
142
  ## Changelog
143
143
 
144
+ ### 2.0.9
145
+
146
+ - `feat(panel-as-message)`: `/panel` and `slim-panel` now print the context stats as plain
147
+ text into the message stream via `client.session.synthetic`, instead of taking over
148
+ OpenCode's own panel UI (`session.panel` slot + `ui.panel.open` removed).
149
+ - The TUI panel now derives its own stats (token estimate, role breakdown, tool/compaction
150
+ counts) directly from the session transcript rather than deferring to the server tool.
151
+
152
+ ### 2.0.8
153
+
154
+ - Fix `keymap.provider is missing` in the CLI plugin: register the keymap layer inside
155
+ an `app` slot render (where the keymap provider is available) instead of in `setup()`.
156
+
144
157
  ### 2.0.7
145
158
 
146
159
  - Fix `Cannot find package 'react'` when the plugin is loaded from the global npm cache:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencodev2-slim",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
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/tui.tsx CHANGED
@@ -1,76 +1,192 @@
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
+ 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"
44
+
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
+ if (Array.isArray(m.content)) {
59
+ for (const part of m.content) {
60
+ if (part?.type === "text" && typeof part.text === "string") {
61
+ text += part.text
62
+ } else if (part?.type === "tool") {
63
+ stats.toolCalls++
64
+ if (typeof part.text === "string") text += part.text
65
+ }
66
+ }
67
+ }
7
68
 
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>
69
+ const tokens = estimateTokens(text)
70
+ switch (role) {
71
+ case "user":
72
+ stats.userMessages++
73
+ stats.tokensByRole.user += tokens
74
+ break
75
+ case "assistant":
76
+ stats.assistantMessages++
77
+ stats.tokensByRole.assistant += tokens
78
+ break
79
+ case "system":
80
+ stats.systemMessages++
81
+ stats.tokensByRole.system += tokens
82
+ break
83
+ }
84
+ stats.totalTokens += tokens
85
+ }
86
+ stats.totalMessages = messages.length
87
+ return stats
88
+ }
89
+
90
+ // Builds a human-readable panel as plain text (injected into the message stream).
91
+ function renderPanelText(sessionID: string, stats: PanelStats): string {
92
+ const lines: string[] = []
93
+ lines.push("┌─────────────────────────────────────────────────────────────┐")
94
+ lines.push("│ SLIM CONTEXT PANEL │")
95
+ lines.push("├─────────────────────────────────────────────────────────────┤")
96
+ lines.push(`│ Session: ${sessionID.slice(0, 40)}`)
97
+ lines.push(`│ Messages: ${stats.totalMessages}`)
98
+ lines.push(
99
+ `│ User: ${stats.userMessages} Assistant: ${stats.assistantMessages} System: ${stats.systemMessages}`,
22
100
  )
101
+ lines.push(`│ Tool calls: ${stats.toolCalls} Compactions: ${stats.compactionCount}`)
102
+ lines.push(`│ Tokens (est): User ${stats.tokensByRole.user} | Assistant ${stats.tokensByRole.assistant} | System ${stats.tokensByRole.system}`)
103
+ lines.push(`│ Total token estimate: ${stats.totalTokens}`)
104
+ lines.push("└─────────────────────────────────────────────────────────────┘")
105
+ return lines.join("\n")
106
+ }
107
+
108
+ // Resolves the "current" session: the focused session if any, else the most recent.
109
+ function resolveCurrentSession(context: any): string | null {
110
+ const sessions = context.data.session.list() || []
111
+ if (sessions.length === 0) return null
112
+ // Prefer the focused session if exposed; otherwise fall back to the first.
113
+ const focused = context.router?.current?.()
114
+ if (focused && typeof focused === "object" && "sessionID" in focused) {
115
+ return focused.sessionID as string
116
+ }
117
+ return sessions[0].id
23
118
  }
24
119
 
25
120
  export default Plugin.define({
26
121
  id: "opencodev2-slim.cli",
27
122
  setup(context) {
123
+ // Register the command inside the "app" slot render, where the keymap
124
+ // provider is available (consistent with OpenCode V2 CLI plugins).
28
125
  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
- })
126
+ append: "app",
127
+ render: () => {
128
+ context.keymap.layer(() => ({
129
+ mode: "global",
130
+ priority: 10,
131
+ commands: [
132
+ {
133
+ id: "opencodev2-slim.panel",
134
+ title: "Show Slim Context Panel",
135
+ group: "Slim",
136
+ palette: true,
137
+ slash: { name: "panel", aliases: ["slim-panel"] },
138
+ enabled: true,
139
+ suggested: true,
140
+ run: async (input: unknown, event: unknown) => {
141
+ const sessionID =
142
+ resolveCurrentSession(context) ||
143
+ (event && typeof event === "object" && "sessionID" in event
144
+ ? (event as any).sessionID
145
+ : null)
36
146
 
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
- }))
147
+ if (!sessionID) {
148
+ context.ui.toast.show({
149
+ title: "Slim Panel",
150
+ message: "No active session found. Open a session first.",
151
+ variant: "warning",
152
+ })
153
+ return
154
+ }
155
+
156
+ try {
157
+ const messages =
158
+ context.data.session.message.list(sessionID) || []
159
+ const stats = deriveStats(messages)
160
+ const text = renderPanelText(sessionID, stats)
161
+ // Inject the panel as plain text into the session stream,
162
+ // so it doesn't take over OpenCode's own panel UI.
163
+ await context.client.session.synthetic({
164
+ sessionID,
165
+ text,
166
+ description: "slim-panel",
167
+ })
168
+ } catch (e) {
169
+ context.ui.toast.show({
170
+ title: "Slim Panel",
171
+ message: `Error: ${e instanceof Error ? e.message : e}`,
172
+ variant: "error",
173
+ })
174
+ }
175
+ },
176
+ },
177
+ ],
178
+ }))
179
+ return null
180
+ },
181
+ })
64
182
 
65
183
  context.ui.toast.show({
66
184
  title: "Slim Plugin",
67
- message: "CLI loaded. Use /panel to show the context panel.",
185
+ message: "Use /panel to print the context panel as a message.",
68
186
  variant: "success",
69
187
  duration: 3000,
70
188
  })
71
189
 
72
- return () => {
73
- // Cleanup
74
- }
190
+ return () => {}
75
191
  },
76
192
  })