@serkanalgur/opencodev2-slim 1.0.0
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/LICENSE +21 -0
- package/README.md +146 -0
- package/package.json +80 -0
- package/src/index.ts +407 -0
- package/src/lib/compress.ts +117 -0
- package/src/lib/config.ts +126 -0
- package/src/lib/prompts.ts +48 -0
- package/src/lib/state.ts +122 -0
- package/src/lib/strategies.ts +49 -0
- package/src/lib/tui/data.ts +90 -0
- package/src/lib/tui/dialogs.tsx +467 -0
- package/src/lib/tui/modals.tsx +12 -0
- package/src/lib/tui.ts +336 -0
- package/src/lib/types.ts +154 -0
- package/src/tui.tsx +61 -0
package/src/lib/tui.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { MessageWithParts, SessionState, SlimConfig, CompressionRecord } from "./types"
|
|
2
|
+
import { getMessageText, getToolResultContent, getToolName, countTokens } from "./compress"
|
|
3
|
+
import { COST_PROFILES } from "./types"
|
|
4
|
+
import { resolveTokenLimit } from "./config"
|
|
5
|
+
|
|
6
|
+
// ─── Panel Data Types ──────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface PanelData {
|
|
9
|
+
sessionId: string
|
|
10
|
+
timestamp: number
|
|
11
|
+
|
|
12
|
+
// Context usage
|
|
13
|
+
currentTokens: number
|
|
14
|
+
maxTokens: number
|
|
15
|
+
usagePercent: number
|
|
16
|
+
status: "healthy" | "warning" | "critical"
|
|
17
|
+
|
|
18
|
+
// Message breakdown
|
|
19
|
+
messageCount: number
|
|
20
|
+
userMessages: number
|
|
21
|
+
assistantMessages: number
|
|
22
|
+
toolCalls: number
|
|
23
|
+
toolResults: number
|
|
24
|
+
|
|
25
|
+
// Token breakdown
|
|
26
|
+
tokensByRole: {
|
|
27
|
+
user: number
|
|
28
|
+
assistant: number
|
|
29
|
+
tools: number
|
|
30
|
+
system: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Compression stats
|
|
34
|
+
compressionCount: number
|
|
35
|
+
averageRatio: number
|
|
36
|
+
totalTokensSaved: number
|
|
37
|
+
lastCompression: CompressionRecord | null
|
|
38
|
+
|
|
39
|
+
// Cost estimate
|
|
40
|
+
estimatedCost: number
|
|
41
|
+
costSaved: number
|
|
42
|
+
model: string
|
|
43
|
+
|
|
44
|
+
// Topic distribution
|
|
45
|
+
topics: { topic: string; count: number; tokens: number }[]
|
|
46
|
+
|
|
47
|
+
// Recommendations
|
|
48
|
+
recommendations: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Panel Builder ─────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export async function buildPanelData(
|
|
54
|
+
sessionId: string,
|
|
55
|
+
messages: MessageWithParts[],
|
|
56
|
+
state: SessionState,
|
|
57
|
+
config: SlimConfig,
|
|
58
|
+
modelId?: string,
|
|
59
|
+
): Promise<PanelData> {
|
|
60
|
+
const modelContextLimit = state.modelContextLimit || 200000
|
|
61
|
+
const maxTokens = resolveTokenLimit(config.compress.maxContextLimit, modelContextLimit)
|
|
62
|
+
|
|
63
|
+
// Count tokens
|
|
64
|
+
let currentTokens = 0
|
|
65
|
+
const tokensByRole = { user: 0, assistant: 0, tools: 0, system: 0 }
|
|
66
|
+
let userMessages = 0
|
|
67
|
+
let assistantMessages = 0
|
|
68
|
+
let toolCalls = 0
|
|
69
|
+
let toolResults = 0
|
|
70
|
+
|
|
71
|
+
for (const msg of messages) {
|
|
72
|
+
const role = msg.info.role
|
|
73
|
+
const text = getMessageText(msg)
|
|
74
|
+
const toolContent = getToolResultContent(msg)
|
|
75
|
+
const msgTokens = await countTokens(text + toolContent)
|
|
76
|
+
currentTokens += msgTokens
|
|
77
|
+
|
|
78
|
+
if (role === "user") {
|
|
79
|
+
tokensByRole.user += msgTokens
|
|
80
|
+
userMessages++
|
|
81
|
+
} else if (role === "assistant") {
|
|
82
|
+
tokensByRole.assistant += msgTokens
|
|
83
|
+
assistantMessages++
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Count tool parts
|
|
87
|
+
for (const part of msg.parts) {
|
|
88
|
+
if (part.type === "tool") {
|
|
89
|
+
const toolPart = part as any
|
|
90
|
+
if (toolPart.state?.status === "completed") {
|
|
91
|
+
toolResults++
|
|
92
|
+
} else {
|
|
93
|
+
toolCalls++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tokensByRole.tools = tokensByRole.user + tokensByRole.assistant - tokensByRole.user - tokensByRole.assistant
|
|
100
|
+
tokensByRole.system = Math.max(0, currentTokens - tokensByRole.user - tokensByRole.assistant - tokensByRole.tools)
|
|
101
|
+
|
|
102
|
+
// Calculate status
|
|
103
|
+
const usagePercent = (currentTokens / maxTokens) * 100
|
|
104
|
+
let status: "healthy" | "warning" | "critical" = "healthy"
|
|
105
|
+
if (usagePercent > 90) status = "critical"
|
|
106
|
+
else if (usagePercent > 70) status = "warning"
|
|
107
|
+
|
|
108
|
+
// Compression stats
|
|
109
|
+
const compressionCount = state.compressionCount
|
|
110
|
+
const averageRatio = state.averageCompressionRatio
|
|
111
|
+
let totalTokensSaved = 0
|
|
112
|
+
for (const record of state.compressionHistory) {
|
|
113
|
+
if (record.success) {
|
|
114
|
+
totalTokensSaved += record.inputTokens - record.outputTokens
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const lastCompression = state.compressionHistory.length > 0
|
|
118
|
+
? state.compressionHistory[state.compressionHistory.length - 1]
|
|
119
|
+
: null
|
|
120
|
+
|
|
121
|
+
// Cost estimate
|
|
122
|
+
const profile = COST_PROFILES[modelId || "default"] || COST_PROFILES.default
|
|
123
|
+
const estimatedCost = (currentTokens / 1000) * profile.inputPricePer1k
|
|
124
|
+
const costSaved = (totalTokensSaved / 1000) * profile.inputPricePer1k
|
|
125
|
+
|
|
126
|
+
// Topic distribution
|
|
127
|
+
const topicMap = new Map<string, { count: number; tokens: number }>()
|
|
128
|
+
for (const msg of messages) {
|
|
129
|
+
const text = getMessageText(msg)
|
|
130
|
+
const topics = extractTopics(text)
|
|
131
|
+
for (const topic of topics) {
|
|
132
|
+
const existing = topicMap.get(topic) || { count: 0, tokens: 0 }
|
|
133
|
+
existing.count++
|
|
134
|
+
existing.tokens += await countTokens(text)
|
|
135
|
+
topicMap.set(topic, existing)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const topics = Array.from(topicMap.entries())
|
|
139
|
+
.map(([topic, data]) => ({ topic, ...data }))
|
|
140
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
141
|
+
.slice(0, 10)
|
|
142
|
+
|
|
143
|
+
// Recommendations
|
|
144
|
+
const recommendations = generateRecommendations(
|
|
145
|
+
usagePercent,
|
|
146
|
+
compressionCount,
|
|
147
|
+
averageRatio,
|
|
148
|
+
messages.length,
|
|
149
|
+
config,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
sessionId,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
currentTokens,
|
|
156
|
+
maxTokens,
|
|
157
|
+
usagePercent,
|
|
158
|
+
status,
|
|
159
|
+
messageCount: messages.length,
|
|
160
|
+
userMessages,
|
|
161
|
+
assistantMessages,
|
|
162
|
+
toolCalls,
|
|
163
|
+
toolResults,
|
|
164
|
+
tokensByRole,
|
|
165
|
+
compressionCount,
|
|
166
|
+
averageRatio,
|
|
167
|
+
totalTokensSaved,
|
|
168
|
+
lastCompression,
|
|
169
|
+
estimatedCost,
|
|
170
|
+
costSaved,
|
|
171
|
+
model: modelId || "unknown",
|
|
172
|
+
topics,
|
|
173
|
+
recommendations,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Topic Extraction ──────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
const TOPIC_KEYWORDS: Record<string, string[]> = {
|
|
180
|
+
"authentication": ["auth", "login", "password", "token", "session", "jwt"],
|
|
181
|
+
"database": ["database", "db", "sql", "query", "migration", "schema"],
|
|
182
|
+
"api": ["api", "endpoint", "route", "request", "response", "http"],
|
|
183
|
+
"testing": ["test", "spec", "assert", "expect", "describe", "jest"],
|
|
184
|
+
"configuration": ["config", "settings", "env", "environment", "variable"],
|
|
185
|
+
"deployment": ["deploy", "docker", "kubernetes", "ci", "cd", "pipeline"],
|
|
186
|
+
"ui": ["ui", "component", "render", "display", "style", "css"],
|
|
187
|
+
"error": ["error", "exception", "catch", "throw", "debug", "fix"],
|
|
188
|
+
"performance": ["performance", "optimize", "cache", "speed", "slow"],
|
|
189
|
+
"security": ["security", "encrypt", "decrypt", "hash", "sanitize"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractTopics(text: string): string[] {
|
|
193
|
+
const lower = text.toLowerCase()
|
|
194
|
+
const topics: string[] = []
|
|
195
|
+
|
|
196
|
+
for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
|
|
197
|
+
if (keywords.some((kw) => lower.includes(kw))) {
|
|
198
|
+
topics.push(topic)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return topics.length > 0 ? topics : ["general"]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Recommendations ───────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function generateRecommendations(
|
|
208
|
+
usagePercent: number,
|
|
209
|
+
compressionCount: number,
|
|
210
|
+
averageRatio: number,
|
|
211
|
+
messageCount: number,
|
|
212
|
+
config: SlimConfig,
|
|
213
|
+
): string[] {
|
|
214
|
+
const recs: string[] = []
|
|
215
|
+
|
|
216
|
+
if (usagePercent > 80) {
|
|
217
|
+
recs.push("Context usage is high. Consider compressing older messages.")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (usagePercent > 90) {
|
|
221
|
+
recs.push("Context nearly full! Run compress immediately to avoid truncation.")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (compressionCount === 0 && messageCount > 20) {
|
|
225
|
+
recs.push("No compressions yet with many messages. Consider running compress.")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (averageRatio < 0.3 && compressionCount > 0) {
|
|
229
|
+
recs.push("Compression ratio is low. Summaries may be too verbose.")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (messageCount > 50 && usagePercent < 50) {
|
|
233
|
+
recs.push("Many messages but low usage. Deduplication may help further.")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (recs.length === 0) {
|
|
237
|
+
recs.push("Context is healthy. No action needed.")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return recs
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Panel Renderer ────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
export function renderPanel(data: PanelData): string {
|
|
246
|
+
const lines: string[] = []
|
|
247
|
+
|
|
248
|
+
// Header
|
|
249
|
+
lines.push("┌─────────────────────────────────────────────────────────────┐")
|
|
250
|
+
lines.push("│ SLIM CONTEXT PANEL │")
|
|
251
|
+
lines.push("├─────────────────────────────────────────────────────────────┤")
|
|
252
|
+
|
|
253
|
+
// Status indicator
|
|
254
|
+
const statusIcon = data.status === "healthy" ? "🟢" : data.status === "warning" ? "🟡" : "🔴"
|
|
255
|
+
lines.push(`│ Status: ${statusIcon} ${data.status.toUpperCase().padEnd(10)} │`)
|
|
256
|
+
lines.push("")
|
|
257
|
+
|
|
258
|
+
// Context usage bar
|
|
259
|
+
const barLength = 30
|
|
260
|
+
const filledLength = Math.round((data.usagePercent / 100) * barLength)
|
|
261
|
+
const emptyLength = barLength - filledLength
|
|
262
|
+
const bar = "█".repeat(filledLength) + "░".repeat(emptyLength)
|
|
263
|
+
lines.push(`│ Context: [${bar}] ${data.usagePercent.toFixed(1)}%`)
|
|
264
|
+
lines.push(`│ ${formatTokens(data.currentTokens)} / ${formatTokens(data.maxTokens)} tokens`)
|
|
265
|
+
lines.push("")
|
|
266
|
+
|
|
267
|
+
// Message breakdown
|
|
268
|
+
lines.push("│ Messages:")
|
|
269
|
+
lines.push(`│ User: ${data.userMessages} Assistant: ${data.assistantMessages}`)
|
|
270
|
+
lines.push(`│ Tool calls: ${data.toolCalls} Results: ${data.toolResults}`)
|
|
271
|
+
lines.push("")
|
|
272
|
+
|
|
273
|
+
// Token breakdown
|
|
274
|
+
lines.push("│ Token Distribution:")
|
|
275
|
+
lines.push(`│ User: ${formatTokens(data.tokensByRole.user)}`)
|
|
276
|
+
lines.push(`│ Assistant: ${formatTokens(data.tokensByRole.assistant)}`)
|
|
277
|
+
lines.push("")
|
|
278
|
+
|
|
279
|
+
// Compression stats
|
|
280
|
+
lines.push("│ Compression Stats:")
|
|
281
|
+
lines.push(`│ Count: ${data.compressionCount}`)
|
|
282
|
+
lines.push(`│ Avg ratio: ${(data.averageRatio * 100).toFixed(1)}%`)
|
|
283
|
+
lines.push(`│ Tokens saved: ${formatTokens(data.totalTokensSaved)}`)
|
|
284
|
+
if (data.lastCompression) {
|
|
285
|
+
const ago = Date.now() - data.lastCompression.timestamp
|
|
286
|
+
lines.push(`│ Last: ${formatTimeAgo(ago)} ago`)
|
|
287
|
+
}
|
|
288
|
+
lines.push("")
|
|
289
|
+
|
|
290
|
+
// Cost
|
|
291
|
+
lines.push("│ Cost Estimate:")
|
|
292
|
+
lines.push(`│ Current: $${data.estimatedCost.toFixed(4)}`)
|
|
293
|
+
lines.push(`│ Saved: $${data.costSaved.toFixed(4)}`)
|
|
294
|
+
lines.push(`│ Model: ${data.model}`)
|
|
295
|
+
lines.push("")
|
|
296
|
+
|
|
297
|
+
// Topics
|
|
298
|
+
if (data.topics.length > 0) {
|
|
299
|
+
lines.push("│ Top Topics:")
|
|
300
|
+
for (const topic of data.topics.slice(0, 5)) {
|
|
301
|
+
lines.push(`│ ${topic.topic}: ${topic.count} msgs (${formatTokens(topic.tokens)})`)
|
|
302
|
+
}
|
|
303
|
+
lines.push("")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Recommendations
|
|
307
|
+
lines.push("│ Recommendations:")
|
|
308
|
+
for (const rec of data.recommendations) {
|
|
309
|
+
lines.push(`│ • ${rec}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
lines.push("└─────────────────────────────────────────────────────────────┘")
|
|
313
|
+
|
|
314
|
+
return lines.join("\n")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function formatTokens(tokens: number): string {
|
|
320
|
+
if (tokens >= 1000000) {
|
|
321
|
+
return `${(tokens / 1000000).toFixed(1)}M`
|
|
322
|
+
}
|
|
323
|
+
if (tokens >= 1000) {
|
|
324
|
+
return `${(tokens / 1000).toFixed(1)}K`
|
|
325
|
+
}
|
|
326
|
+
return String(tokens)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatTimeAgo(ms: number): string {
|
|
330
|
+
const seconds = Math.floor(ms / 1000)
|
|
331
|
+
if (seconds < 60) return `${seconds}s`
|
|
332
|
+
const minutes = Math.floor(seconds / 60)
|
|
333
|
+
if (minutes < 60) return `${minutes}m`
|
|
334
|
+
const hours = Math.floor(minutes / 60)
|
|
335
|
+
return `${hours}h`
|
|
336
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
|
2
|
+
|
|
3
|
+
// ─── Config Types ───────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SlimConfig {
|
|
6
|
+
enabled: boolean
|
|
7
|
+
debug: boolean
|
|
8
|
+
|
|
9
|
+
// Compression settings
|
|
10
|
+
compress: {
|
|
11
|
+
enabled: boolean
|
|
12
|
+
permission: "allow" | "ask" | "deny"
|
|
13
|
+
maxContextLimit: number | string // number or "80%"
|
|
14
|
+
minContextLimit: number | string // number or "40%"
|
|
15
|
+
nudgeFrequency: number
|
|
16
|
+
protectUserMessages: boolean
|
|
17
|
+
protectedTools: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Strategy settings
|
|
21
|
+
strategies: {
|
|
22
|
+
deduplication: {
|
|
23
|
+
enabled: boolean
|
|
24
|
+
protectedTools: string[]
|
|
25
|
+
}
|
|
26
|
+
purgeErrors: {
|
|
27
|
+
enabled: boolean
|
|
28
|
+
turns: number
|
|
29
|
+
protectedTools: string[]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Adaptive thresholds
|
|
34
|
+
adaptive: {
|
|
35
|
+
enabled: boolean
|
|
36
|
+
learningRate: number
|
|
37
|
+
minCompressionRatio: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Cost awareness
|
|
41
|
+
costAware: {
|
|
42
|
+
enabled: boolean
|
|
43
|
+
cacheBoostFactor: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Persistence
|
|
47
|
+
persistence: {
|
|
48
|
+
enabled: boolean
|
|
49
|
+
directory: string
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── State Types ────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export interface SessionState {
|
|
56
|
+
sessionId: string
|
|
57
|
+
modelContextLimit: number
|
|
58
|
+
currentTokenCount: number
|
|
59
|
+
compressionCount: number
|
|
60
|
+
lastCompressionTime: number
|
|
61
|
+
manualMode: boolean
|
|
62
|
+
compressPermission: "allow" | "ask" | "deny" | null
|
|
63
|
+
|
|
64
|
+
// Adaptive learning
|
|
65
|
+
compressionHistory: CompressionRecord[]
|
|
66
|
+
averageCompressionRatio: number
|
|
67
|
+
|
|
68
|
+
// Tool call tracking
|
|
69
|
+
toolCalls: Map<string, ToolCallInfo>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CompressionRecord {
|
|
73
|
+
timestamp: number
|
|
74
|
+
inputTokens: number
|
|
75
|
+
outputTokens: number
|
|
76
|
+
ratio: number
|
|
77
|
+
messageCount: number
|
|
78
|
+
success: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ToolCallInfo {
|
|
82
|
+
tool: string
|
|
83
|
+
args: unknown
|
|
84
|
+
timestamp: number
|
|
85
|
+
turn: number
|
|
86
|
+
error?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Message Types ──────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface MessageWithParts {
|
|
92
|
+
info: Message
|
|
93
|
+
parts: Part[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CompressionRange {
|
|
97
|
+
start: number
|
|
98
|
+
end: number
|
|
99
|
+
messages: MessageWithParts[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CompressionResult {
|
|
103
|
+
success: boolean
|
|
104
|
+
summary: string
|
|
105
|
+
inputTokens: number
|
|
106
|
+
outputTokens: number
|
|
107
|
+
ratio: number
|
|
108
|
+
compressedIds: string[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Cost Types ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export interface CostProfile {
|
|
114
|
+
inputPricePer1k: number
|
|
115
|
+
outputPricePer1k: number
|
|
116
|
+
cacheReadPricePer1k: number
|
|
117
|
+
cacheWritePricePer1k: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const COST_PROFILES: Record<string, CostProfile> = {
|
|
121
|
+
// Anthropic
|
|
122
|
+
"anthropic/claude-sonnet-4-20250514": {
|
|
123
|
+
inputPricePer1k: 0.003,
|
|
124
|
+
outputPricePer1k: 0.015,
|
|
125
|
+
cacheReadPricePer1k: 0.0003,
|
|
126
|
+
cacheWritePricePer1k: 0.00375,
|
|
127
|
+
},
|
|
128
|
+
"anthropic/claude-3-5-sonnet-20241022": {
|
|
129
|
+
inputPricePer1k: 0.003,
|
|
130
|
+
outputPricePer1k: 0.015,
|
|
131
|
+
cacheReadPricePer1k: 0.0003,
|
|
132
|
+
cacheWritePricePer1k: 0.00375,
|
|
133
|
+
},
|
|
134
|
+
// OpenAI
|
|
135
|
+
"openai/gpt-4o": {
|
|
136
|
+
inputPricePer1k: 0.0025,
|
|
137
|
+
outputPricePer1k: 0.01,
|
|
138
|
+
cacheReadPricePer1k: 0.00125,
|
|
139
|
+
cacheWritePricePer1k: 0.0025,
|
|
140
|
+
},
|
|
141
|
+
"openai/gpt-4o-mini": {
|
|
142
|
+
inputPricePer1k: 0.00015,
|
|
143
|
+
outputPricePer1k: 0.0006,
|
|
144
|
+
cacheReadPricePer1k: 0.000075,
|
|
145
|
+
cacheWritePricePer1k: 0.00015,
|
|
146
|
+
},
|
|
147
|
+
// Default
|
|
148
|
+
default: {
|
|
149
|
+
inputPricePer1k: 0.003,
|
|
150
|
+
outputPricePer1k: 0.015,
|
|
151
|
+
cacheReadPricePer1k: 0.0003,
|
|
152
|
+
cacheWritePricePer1k: 0.00375,
|
|
153
|
+
},
|
|
154
|
+
}
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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
|