@serkanalgur/opencodev2-slim 2.0.8 → 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.
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/tui.tsx +145 -37
package/README.md
CHANGED
|
@@ -141,6 +141,14 @@ 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
|
+
|
|
144
152
|
### 2.0.8
|
|
145
153
|
|
|
146
154
|
- Fix `keymap.provider is missing` in the CLI plugin: register the keymap layer inside
|
package/package.json
CHANGED
package/src/tui.tsx
CHANGED
|
@@ -1,41 +1,127 @@
|
|
|
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
|
-
|
|
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
|
+
}
|
|
68
|
+
|
|
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
|
+
}
|
|
7
89
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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>
|
|
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) {
|
|
28
|
-
|
|
29
|
-
|
|
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).
|
|
123
|
+
// Register the command inside the "app" slot render, where the keymap
|
|
124
|
+
// provider is available (consistent with OpenCode V2 CLI plugins).
|
|
39
125
|
context.ui.slot({
|
|
40
126
|
append: "app",
|
|
41
127
|
render: () => {
|
|
@@ -51,16 +137,40 @@ export default Plugin.define({
|
|
|
51
137
|
slash: { name: "panel", aliases: ["slim-panel"] },
|
|
52
138
|
enabled: true,
|
|
53
139
|
suggested: true,
|
|
54
|
-
run: async () => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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)
|
|
146
|
+
|
|
147
|
+
if (!sessionID) {
|
|
59
148
|
context.ui.toast.show({
|
|
60
149
|
title: "Slim Panel",
|
|
61
150
|
message: "No active session found. Open a session first.",
|
|
62
151
|
variant: "warning",
|
|
63
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
|
+
})
|
|
64
174
|
}
|
|
65
175
|
},
|
|
66
176
|
},
|
|
@@ -72,13 +182,11 @@ export default Plugin.define({
|
|
|
72
182
|
|
|
73
183
|
context.ui.toast.show({
|
|
74
184
|
title: "Slim Plugin",
|
|
75
|
-
message: "
|
|
185
|
+
message: "Use /panel to print the context panel as a message.",
|
|
76
186
|
variant: "success",
|
|
77
187
|
duration: 3000,
|
|
78
188
|
})
|
|
79
189
|
|
|
80
|
-
return () => {
|
|
81
|
-
// Cleanup
|
|
82
|
-
}
|
|
190
|
+
return () => {}
|
|
83
191
|
},
|
|
84
192
|
})
|