@serkanalgur/opencode-slim 1.1.0 → 1.3.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/README.md +59 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +338 -11
- package/dist/index.js.map +1 -1
- package/dist/lib/prompts.d.ts +1 -0
- package/dist/lib/prompts.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +14 -0
- package/dist/lib/tui/commands.d.ts.map +1 -0
- package/dist/lib/tui/data.d.ts +4 -0
- package/dist/lib/tui/data.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +8 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/modals.d.ts +5 -0
- package/dist/lib/tui/modals.d.ts.map +1 -0
- package/dist/lib/tui.d.ts +36 -0
- package/dist/lib/tui.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/tui/commands.ts +30 -0
- package/lib/tui/data.ts +90 -0
- package/lib/tui/dialogs.tsx +467 -0
- package/lib/tui/modals.tsx +12 -0
- package/package.json +34 -3
- package/tui.tsx +26 -0
package/README.md
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# opencode-slim
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@serkanalgur/opencode-slim)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
3
6
|
Smart context management plugin for OpenCode. Optimizes token usage through semantic compression, cost-aware pruning, and adaptive thresholds.
|
|
4
7
|
|
|
5
8
|
## Features
|
|
6
9
|
|
|
10
|
+
- **TUI Panel** - Rich context usage visualization with status indicators
|
|
11
|
+
- **Enhanced Compress** - Auto/range/topic modes for flexible compression
|
|
7
12
|
- **Semantic Compression** - Groups related tool calls and compresses them intelligently
|
|
8
13
|
- **Cost-Aware Pruning** - Considers token pricing when deciding what to compress
|
|
9
14
|
- **Adaptive Thresholds** - Learns from compression history to optimize timing
|
|
10
15
|
- **Session Persistence** - Saves state across restarts
|
|
11
16
|
- **Deduplication** - Removes repeated tool calls automatically
|
|
12
17
|
- **Error Purging** - Cleans up failed tool call outputs after configurable turns
|
|
18
|
+
- **Topic Extraction** - Identifies and tracks conversation topics
|
|
19
|
+
- **Smart Recommendations** - Provides actionable suggestions for context optimization
|
|
13
20
|
|
|
14
21
|
## Installation
|
|
15
22
|
|
|
@@ -17,6 +24,39 @@ Smart context management plugin for OpenCode. Optimizes token usage through sema
|
|
|
17
24
|
opencode plugin @serkanalgur/opencode-slim@latest --global
|
|
18
25
|
```
|
|
19
26
|
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Slash Commands
|
|
30
|
+
|
|
31
|
+
Slim provides slash commands for easy access:
|
|
32
|
+
|
|
33
|
+
- `/panel` — Opens the rich TUI panel with context usage, stats, and help
|
|
34
|
+
- `/compress [focus]` — Manually trigger compression with focus description
|
|
35
|
+
|
|
36
|
+
### TUI Panel
|
|
37
|
+
|
|
38
|
+
The panel provides a real-time overview of your context usage, including:
|
|
39
|
+
- Token usage vs model limit with visual progress bar
|
|
40
|
+
- Message breakdown by role (user/assistant/tools)
|
|
41
|
+
- Compression history and savings
|
|
42
|
+
- Cost estimation based on your model
|
|
43
|
+
- Smart recommendations for optimization
|
|
44
|
+
|
|
45
|
+
### Compress Tool
|
|
46
|
+
|
|
47
|
+
The enhanced compress tool supports multiple modes:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Auto mode (default) - intelligently selects what to compress
|
|
51
|
+
compress({ focus: "old exploration" })
|
|
52
|
+
|
|
53
|
+
// Range mode - compress specific message range
|
|
54
|
+
compress({ focus: "completed tasks", mode: "range", start: 0, end: 50 })
|
|
55
|
+
|
|
56
|
+
// Topic mode - compress messages matching a topic
|
|
57
|
+
compress({ focus: "database work", mode: "topic", topic: "database" })
|
|
58
|
+
```
|
|
59
|
+
|
|
20
60
|
## Configuration
|
|
21
61
|
|
|
22
62
|
Create `~/.config/opencode/slim.jsonc`:
|
|
@@ -62,6 +102,16 @@ Create `~/.config/opencode/slim.jsonc`:
|
|
|
62
102
|
|
|
63
103
|
## How It Works
|
|
64
104
|
|
|
105
|
+
### TUI Panel
|
|
106
|
+
|
|
107
|
+
The panel provides a real-time overview of your context usage, including:
|
|
108
|
+
- Token usage vs model limit with visual progress bar
|
|
109
|
+
- Message breakdown by role (user/assistant/tools)
|
|
110
|
+
- Compression history and savings
|
|
111
|
+
- Cost estimation based on your model
|
|
112
|
+
- Topic distribution across the conversation
|
|
113
|
+
- Smart recommendations for optimization
|
|
114
|
+
|
|
65
115
|
### Semantic Compression
|
|
66
116
|
|
|
67
117
|
Unlike simple text truncation, slim analyzes the semantic content of messages and groups related tool calls together. This preserves context while removing redundancy.
|
|
@@ -80,19 +130,24 @@ State is saved to disk, so compression history and learning persist across resta
|
|
|
80
130
|
|
|
81
131
|
## Commands
|
|
82
132
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
133
|
+
| Command | Description |
|
|
134
|
+
|---------|-------------|
|
|
135
|
+
| `/panel` | Open the Slim TUI panel with context usage, stats, and help |
|
|
136
|
+
| `/compress [focus]` | Manually trigger compression with focus description |
|
|
86
137
|
|
|
87
138
|
## Comparison with DCP
|
|
88
139
|
|
|
89
140
|
| Feature | slim | DCP |
|
|
90
141
|
|---------|------|-----|
|
|
142
|
+
| TUI Panel | ✅ | ✅ |
|
|
143
|
+
| Slash Commands | ✅ `/panel` `/compress` | ✅ `/dcp` `/dcp-compress` |
|
|
144
|
+
| Manual Compress | ✅ | ✅ |
|
|
91
145
|
| Semantic grouping | ✅ | ❌ |
|
|
92
146
|
| Cost awareness | ✅ | ❌ |
|
|
93
147
|
| Adaptive thresholds | ✅ | ❌ |
|
|
94
148
|
| Session persistence | ✅ | ❌ |
|
|
95
|
-
|
|
|
149
|
+
| Topic extraction | ✅ | ❌ |
|
|
150
|
+
| File count | ~10 | ~150 |
|
|
96
151
|
| License | MIT | AGPL-3.0 |
|
|
97
152
|
| Config complexity | Low | High |
|
|
98
153
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAqYjD,wBAA8C"}
|
package/dist/index.js
CHANGED
|
@@ -13663,6 +13663,251 @@ function getNudgeMessage(reason, tokenCount, maxTokens) {
|
|
|
13663
13663
|
}
|
|
13664
13664
|
}
|
|
13665
13665
|
|
|
13666
|
+
// lib/types.ts
|
|
13667
|
+
var COST_PROFILES = {
|
|
13668
|
+
// Anthropic
|
|
13669
|
+
"anthropic/claude-sonnet-4-20250514": {
|
|
13670
|
+
inputPricePer1k: 3e-3,
|
|
13671
|
+
outputPricePer1k: 0.015,
|
|
13672
|
+
cacheReadPricePer1k: 3e-4,
|
|
13673
|
+
cacheWritePricePer1k: 375e-5
|
|
13674
|
+
},
|
|
13675
|
+
"anthropic/claude-3-5-sonnet-20241022": {
|
|
13676
|
+
inputPricePer1k: 3e-3,
|
|
13677
|
+
outputPricePer1k: 0.015,
|
|
13678
|
+
cacheReadPricePer1k: 3e-4,
|
|
13679
|
+
cacheWritePricePer1k: 375e-5
|
|
13680
|
+
},
|
|
13681
|
+
// OpenAI
|
|
13682
|
+
"openai/gpt-4o": {
|
|
13683
|
+
inputPricePer1k: 25e-4,
|
|
13684
|
+
outputPricePer1k: 0.01,
|
|
13685
|
+
cacheReadPricePer1k: 125e-5,
|
|
13686
|
+
cacheWritePricePer1k: 25e-4
|
|
13687
|
+
},
|
|
13688
|
+
"openai/gpt-4o-mini": {
|
|
13689
|
+
inputPricePer1k: 15e-5,
|
|
13690
|
+
outputPricePer1k: 6e-4,
|
|
13691
|
+
cacheReadPricePer1k: 75e-6,
|
|
13692
|
+
cacheWritePricePer1k: 15e-5
|
|
13693
|
+
},
|
|
13694
|
+
// Default
|
|
13695
|
+
"default": {
|
|
13696
|
+
inputPricePer1k: 3e-3,
|
|
13697
|
+
outputPricePer1k: 0.015,
|
|
13698
|
+
cacheReadPricePer1k: 3e-4,
|
|
13699
|
+
cacheWritePricePer1k: 375e-5
|
|
13700
|
+
}
|
|
13701
|
+
};
|
|
13702
|
+
|
|
13703
|
+
// lib/tui.ts
|
|
13704
|
+
async function buildPanelData(sessionId, messages, state, config2, modelId) {
|
|
13705
|
+
const modelContextLimit = state.modelContextLimit || 2e5;
|
|
13706
|
+
const maxTokens = resolveTokenLimit(config2.compress.maxContextLimit, modelContextLimit);
|
|
13707
|
+
let currentTokens = 0;
|
|
13708
|
+
const tokensByRole = { user: 0, assistant: 0, tools: 0, system: 0 };
|
|
13709
|
+
let userMessages = 0;
|
|
13710
|
+
let assistantMessages = 0;
|
|
13711
|
+
let toolCalls = 0;
|
|
13712
|
+
let toolResults = 0;
|
|
13713
|
+
for (const msg of messages) {
|
|
13714
|
+
const role = msg.info.role;
|
|
13715
|
+
const text = getMessageText(msg);
|
|
13716
|
+
const toolContent = getToolResultContent(msg);
|
|
13717
|
+
const msgTokens = await countTokens(text + toolContent);
|
|
13718
|
+
currentTokens += msgTokens;
|
|
13719
|
+
if (role === "user") {
|
|
13720
|
+
tokensByRole.user += msgTokens;
|
|
13721
|
+
userMessages++;
|
|
13722
|
+
} else if (role === "assistant") {
|
|
13723
|
+
tokensByRole.assistant += msgTokens;
|
|
13724
|
+
assistantMessages++;
|
|
13725
|
+
}
|
|
13726
|
+
for (const part of msg.parts) {
|
|
13727
|
+
if (part.type === "tool") {
|
|
13728
|
+
const toolPart = part;
|
|
13729
|
+
if (toolPart.state?.status === "completed") {
|
|
13730
|
+
toolResults++;
|
|
13731
|
+
} else {
|
|
13732
|
+
toolCalls++;
|
|
13733
|
+
}
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13736
|
+
}
|
|
13737
|
+
tokensByRole.tools = tokensByRole.user + tokensByRole.assistant - tokensByRole.user - tokensByRole.assistant;
|
|
13738
|
+
tokensByRole.system = Math.max(0, currentTokens - tokensByRole.user - tokensByRole.assistant - tokensByRole.tools);
|
|
13739
|
+
const usagePercent = currentTokens / maxTokens * 100;
|
|
13740
|
+
let status = "healthy";
|
|
13741
|
+
if (usagePercent > 90) status = "critical";
|
|
13742
|
+
else if (usagePercent > 70) status = "warning";
|
|
13743
|
+
const compressionCount = state.compressionCount;
|
|
13744
|
+
const averageRatio = state.averageCompressionRatio;
|
|
13745
|
+
let totalTokensSaved = 0;
|
|
13746
|
+
for (const record2 of state.compressionHistory) {
|
|
13747
|
+
if (record2.success) {
|
|
13748
|
+
totalTokensSaved += record2.inputTokens - record2.outputTokens;
|
|
13749
|
+
}
|
|
13750
|
+
}
|
|
13751
|
+
const lastCompression = state.compressionHistory.length > 0 ? state.compressionHistory[state.compressionHistory.length - 1] : null;
|
|
13752
|
+
const profile = COST_PROFILES[modelId || "default"] || COST_PROFILES.default;
|
|
13753
|
+
const estimatedCost = currentTokens / 1e3 * profile.inputPricePer1k;
|
|
13754
|
+
const costSaved = totalTokensSaved / 1e3 * profile.inputPricePer1k;
|
|
13755
|
+
const topicMap = /* @__PURE__ */ new Map();
|
|
13756
|
+
for (const msg of messages) {
|
|
13757
|
+
const text = getMessageText(msg);
|
|
13758
|
+
const topics2 = extractTopics(text);
|
|
13759
|
+
for (const topic of topics2) {
|
|
13760
|
+
const existing = topicMap.get(topic) || { count: 0, tokens: 0 };
|
|
13761
|
+
existing.count++;
|
|
13762
|
+
existing.tokens += await countTokens(text);
|
|
13763
|
+
topicMap.set(topic, existing);
|
|
13764
|
+
}
|
|
13765
|
+
}
|
|
13766
|
+
const topics = Array.from(topicMap.entries()).map(([topic, data]) => ({ topic, ...data })).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
|
|
13767
|
+
const recommendations = generateRecommendations(
|
|
13768
|
+
usagePercent,
|
|
13769
|
+
compressionCount,
|
|
13770
|
+
averageRatio,
|
|
13771
|
+
messages.length,
|
|
13772
|
+
config2
|
|
13773
|
+
);
|
|
13774
|
+
return {
|
|
13775
|
+
sessionId,
|
|
13776
|
+
timestamp: Date.now(),
|
|
13777
|
+
currentTokens,
|
|
13778
|
+
maxTokens,
|
|
13779
|
+
usagePercent,
|
|
13780
|
+
status,
|
|
13781
|
+
messageCount: messages.length,
|
|
13782
|
+
userMessages,
|
|
13783
|
+
assistantMessages,
|
|
13784
|
+
toolCalls,
|
|
13785
|
+
toolResults,
|
|
13786
|
+
tokensByRole,
|
|
13787
|
+
compressionCount,
|
|
13788
|
+
averageRatio,
|
|
13789
|
+
totalTokensSaved,
|
|
13790
|
+
lastCompression,
|
|
13791
|
+
estimatedCost,
|
|
13792
|
+
costSaved,
|
|
13793
|
+
model: modelId || "unknown",
|
|
13794
|
+
topics,
|
|
13795
|
+
recommendations
|
|
13796
|
+
};
|
|
13797
|
+
}
|
|
13798
|
+
var TOPIC_KEYWORDS = {
|
|
13799
|
+
"authentication": ["auth", "login", "password", "token", "session", "jwt"],
|
|
13800
|
+
"database": ["database", "db", "sql", "query", "migration", "schema"],
|
|
13801
|
+
"api": ["api", "endpoint", "route", "request", "response", "http"],
|
|
13802
|
+
"testing": ["test", "spec", "assert", "expect", "describe", "jest"],
|
|
13803
|
+
"configuration": ["config", "settings", "env", "environment", "variable"],
|
|
13804
|
+
"deployment": ["deploy", "docker", "kubernetes", "ci", "cd", "pipeline"],
|
|
13805
|
+
"ui": ["ui", "component", "render", "display", "style", "css"],
|
|
13806
|
+
"error": ["error", "exception", "catch", "throw", "debug", "fix"],
|
|
13807
|
+
"performance": ["performance", "optimize", "cache", "speed", "slow"],
|
|
13808
|
+
"security": ["security", "encrypt", "decrypt", "hash", "sanitize"]
|
|
13809
|
+
};
|
|
13810
|
+
function extractTopics(text) {
|
|
13811
|
+
const lower = text.toLowerCase();
|
|
13812
|
+
const topics = [];
|
|
13813
|
+
for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
|
|
13814
|
+
if (keywords.some((kw) => lower.includes(kw))) {
|
|
13815
|
+
topics.push(topic);
|
|
13816
|
+
}
|
|
13817
|
+
}
|
|
13818
|
+
return topics.length > 0 ? topics : ["general"];
|
|
13819
|
+
}
|
|
13820
|
+
function generateRecommendations(usagePercent, compressionCount, averageRatio, messageCount, config2) {
|
|
13821
|
+
const recs = [];
|
|
13822
|
+
if (usagePercent > 80) {
|
|
13823
|
+
recs.push("Context usage is high. Consider compressing older messages.");
|
|
13824
|
+
}
|
|
13825
|
+
if (usagePercent > 90) {
|
|
13826
|
+
recs.push("Context nearly full! Run compress immediately to avoid truncation.");
|
|
13827
|
+
}
|
|
13828
|
+
if (compressionCount === 0 && messageCount > 20) {
|
|
13829
|
+
recs.push("No compressions yet with many messages. Consider running compress.");
|
|
13830
|
+
}
|
|
13831
|
+
if (averageRatio < 0.3 && compressionCount > 0) {
|
|
13832
|
+
recs.push("Compression ratio is low. Summaries may be too verbose.");
|
|
13833
|
+
}
|
|
13834
|
+
if (messageCount > 50 && usagePercent < 50) {
|
|
13835
|
+
recs.push("Many messages but low usage. Deduplication may help further.");
|
|
13836
|
+
}
|
|
13837
|
+
if (recs.length === 0) {
|
|
13838
|
+
recs.push("Context is healthy. No action needed.");
|
|
13839
|
+
}
|
|
13840
|
+
return recs;
|
|
13841
|
+
}
|
|
13842
|
+
function renderPanel(data) {
|
|
13843
|
+
const lines = [];
|
|
13844
|
+
lines.push("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
13845
|
+
lines.push("\u2502 SLIM CONTEXT PANEL \u2502");
|
|
13846
|
+
lines.push("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
13847
|
+
const statusIcon = data.status === "healthy" ? "\u{1F7E2}" : data.status === "warning" ? "\u{1F7E1}" : "\u{1F534}";
|
|
13848
|
+
lines.push(`\u2502 Status: ${statusIcon} ${data.status.toUpperCase().padEnd(10)} \u2502`);
|
|
13849
|
+
lines.push("");
|
|
13850
|
+
const barLength = 30;
|
|
13851
|
+
const filledLength = Math.round(data.usagePercent / 100 * barLength);
|
|
13852
|
+
const emptyLength = barLength - filledLength;
|
|
13853
|
+
const bar = "\u2588".repeat(filledLength) + "\u2591".repeat(emptyLength);
|
|
13854
|
+
lines.push(`\u2502 Context: [${bar}] ${data.usagePercent.toFixed(1)}%`);
|
|
13855
|
+
lines.push(`\u2502 ${formatTokens(data.currentTokens)} / ${formatTokens(data.maxTokens)} tokens`);
|
|
13856
|
+
lines.push("");
|
|
13857
|
+
lines.push("\u2502 Messages:");
|
|
13858
|
+
lines.push(`\u2502 User: ${data.userMessages} Assistant: ${data.assistantMessages}`);
|
|
13859
|
+
lines.push(`\u2502 Tool calls: ${data.toolCalls} Results: ${data.toolResults}`);
|
|
13860
|
+
lines.push("");
|
|
13861
|
+
lines.push("\u2502 Token Distribution:");
|
|
13862
|
+
lines.push(`\u2502 User: ${formatTokens(data.tokensByRole.user)}`);
|
|
13863
|
+
lines.push(`\u2502 Assistant: ${formatTokens(data.tokensByRole.assistant)}`);
|
|
13864
|
+
lines.push("");
|
|
13865
|
+
lines.push("\u2502 Compression Stats:");
|
|
13866
|
+
lines.push(`\u2502 Count: ${data.compressionCount}`);
|
|
13867
|
+
lines.push(`\u2502 Avg ratio: ${(data.averageRatio * 100).toFixed(1)}%`);
|
|
13868
|
+
lines.push(`\u2502 Tokens saved: ${formatTokens(data.totalTokensSaved)}`);
|
|
13869
|
+
if (data.lastCompression) {
|
|
13870
|
+
const ago = Date.now() - data.lastCompression.timestamp;
|
|
13871
|
+
lines.push(`\u2502 Last: ${formatTimeAgo(ago)} ago`);
|
|
13872
|
+
}
|
|
13873
|
+
lines.push("");
|
|
13874
|
+
lines.push("\u2502 Cost Estimate:");
|
|
13875
|
+
lines.push(`\u2502 Current: $${data.estimatedCost.toFixed(4)}`);
|
|
13876
|
+
lines.push(`\u2502 Saved: $${data.costSaved.toFixed(4)}`);
|
|
13877
|
+
lines.push(`\u2502 Model: ${data.model}`);
|
|
13878
|
+
lines.push("");
|
|
13879
|
+
if (data.topics.length > 0) {
|
|
13880
|
+
lines.push("\u2502 Top Topics:");
|
|
13881
|
+
for (const topic of data.topics.slice(0, 5)) {
|
|
13882
|
+
lines.push(`\u2502 ${topic.topic}: ${topic.count} msgs (${formatTokens(topic.tokens)})`);
|
|
13883
|
+
}
|
|
13884
|
+
lines.push("");
|
|
13885
|
+
}
|
|
13886
|
+
lines.push("\u2502 Recommendations:");
|
|
13887
|
+
for (const rec of data.recommendations) {
|
|
13888
|
+
lines.push(`\u2502 \u2022 ${rec}`);
|
|
13889
|
+
}
|
|
13890
|
+
lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
13891
|
+
return lines.join("\n");
|
|
13892
|
+
}
|
|
13893
|
+
function formatTokens(tokens) {
|
|
13894
|
+
if (tokens >= 1e6) {
|
|
13895
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
13896
|
+
}
|
|
13897
|
+
if (tokens >= 1e3) {
|
|
13898
|
+
return `${(tokens / 1e3).toFixed(1)}K`;
|
|
13899
|
+
}
|
|
13900
|
+
return String(tokens);
|
|
13901
|
+
}
|
|
13902
|
+
function formatTimeAgo(ms) {
|
|
13903
|
+
const seconds = Math.floor(ms / 1e3);
|
|
13904
|
+
if (seconds < 60) return `${seconds}s`;
|
|
13905
|
+
const minutes = Math.floor(seconds / 60);
|
|
13906
|
+
if (minutes < 60) return `${minutes}m`;
|
|
13907
|
+
const hours = Math.floor(minutes / 60);
|
|
13908
|
+
return `${hours}h`;
|
|
13909
|
+
}
|
|
13910
|
+
|
|
13666
13911
|
// index.ts
|
|
13667
13912
|
var sessionStates = /* @__PURE__ */ new Map();
|
|
13668
13913
|
var sessionConfigs = /* @__PURE__ */ new Map();
|
|
@@ -13682,7 +13927,12 @@ var server = async (ctx) => {
|
|
|
13682
13927
|
const compressTool = tool({
|
|
13683
13928
|
description: getCompressToolDescription(),
|
|
13684
13929
|
args: {
|
|
13685
|
-
focus: external_exports.string().describe("
|
|
13930
|
+
focus: external_exports.string().describe("What to compress (e.g., 'old exploration', 'completed tasks')"),
|
|
13931
|
+
mode: external_exports.enum(["auto", "range", "topic"]).default("auto").describe("Compression mode"),
|
|
13932
|
+
start: external_exports.number().optional().describe("Start message index (for range mode)"),
|
|
13933
|
+
end: external_exports.number().optional().describe("End message index (for range mode)"),
|
|
13934
|
+
topic: external_exports.string().optional().describe("Topic to compress (for topic mode)"),
|
|
13935
|
+
keepRecent: external_exports.number().default(5).describe("Number of recent messages to always keep")
|
|
13686
13936
|
},
|
|
13687
13937
|
async execute(args, context) {
|
|
13688
13938
|
const config2 = getConfig(context.sessionID);
|
|
@@ -13699,17 +13949,42 @@ var server = async (ctx) => {
|
|
|
13699
13949
|
info: m.info,
|
|
13700
13950
|
parts: m.parts
|
|
13701
13951
|
}));
|
|
13952
|
+
let targetIndices = [];
|
|
13702
13953
|
let inputTokens = 0;
|
|
13703
|
-
|
|
13704
|
-
const
|
|
13705
|
-
|
|
13954
|
+
if (args.mode === "range" && args.start !== void 0 && args.end !== void 0) {
|
|
13955
|
+
const start = Math.max(0, args.start);
|
|
13956
|
+
const end = Math.min(messageWithParts.length, args.end);
|
|
13957
|
+
for (let i = start; i < end; i++) {
|
|
13958
|
+
targetIndices.push(i);
|
|
13959
|
+
const text = getMessageText(messageWithParts[i]) + getToolResultContent(messageWithParts[i]);
|
|
13960
|
+
inputTokens += await countTokens(text);
|
|
13961
|
+
}
|
|
13962
|
+
} else if (args.mode === "topic" && args.topic) {
|
|
13963
|
+
const topicLower = args.topic.toLowerCase();
|
|
13964
|
+
for (let i = 0; i < messageWithParts.length - args.keepRecent; i++) {
|
|
13965
|
+
const msg = messageWithParts[i];
|
|
13966
|
+
const text = getMessageText(msg) + getToolResultContent(msg);
|
|
13967
|
+
if (text.toLowerCase().includes(topicLower)) {
|
|
13968
|
+
targetIndices.push(i);
|
|
13969
|
+
inputTokens += await countTokens(text);
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13972
|
+
} else {
|
|
13973
|
+
const keepRecent = args.keepRecent;
|
|
13974
|
+
for (let i = 0; i < messageWithParts.length - keepRecent; i++) {
|
|
13975
|
+
const msg = messageWithParts[i];
|
|
13976
|
+
const text = getMessageText(msg) + getToolResultContent(msg);
|
|
13977
|
+
const tokens = await countTokens(text);
|
|
13978
|
+
if (tokens < 100) continue;
|
|
13979
|
+
targetIndices.push(i);
|
|
13980
|
+
inputTokens += tokens;
|
|
13981
|
+
}
|
|
13706
13982
|
}
|
|
13707
|
-
|
|
13708
|
-
if (targets.length === 0) {
|
|
13983
|
+
if (targetIndices.length === 0) {
|
|
13709
13984
|
return "Nothing to compress - context is already efficient";
|
|
13710
13985
|
}
|
|
13711
|
-
const
|
|
13712
|
-
const summary = buildCompressionSummary(
|
|
13986
|
+
const targetMessages = targetIndices.map((i) => messageWithParts[i]);
|
|
13987
|
+
const summary = buildCompressionSummary(targetMessages, args.focus);
|
|
13713
13988
|
const outputTokens = await countTokens(summary);
|
|
13714
13989
|
const ratio = inputTokens > 0 ? 1 - outputTokens / inputTokens : 0;
|
|
13715
13990
|
addCompressionRecord(
|
|
@@ -13719,19 +13994,20 @@ var server = async (ctx) => {
|
|
|
13719
13994
|
inputTokens,
|
|
13720
13995
|
outputTokens,
|
|
13721
13996
|
ratio,
|
|
13722
|
-
messageCount:
|
|
13997
|
+
messageCount: targetMessages.length,
|
|
13723
13998
|
success: true
|
|
13724
13999
|
},
|
|
13725
14000
|
config2.adaptive.learningRate
|
|
13726
14001
|
);
|
|
13727
14002
|
saveSessionState(state, config2.persistence.directory);
|
|
13728
14003
|
return {
|
|
13729
|
-
title: `Compressed ${
|
|
14004
|
+
title: `Compressed ${targetMessages.length} messages`,
|
|
13730
14005
|
output: summary,
|
|
13731
14006
|
metadata: {
|
|
13732
14007
|
inputTokens,
|
|
13733
14008
|
outputTokens,
|
|
13734
14009
|
ratio: Math.round(ratio * 100) + "%",
|
|
14010
|
+
mode: args.mode,
|
|
13735
14011
|
focus: args.focus
|
|
13736
14012
|
}
|
|
13737
14013
|
};
|
|
@@ -13740,6 +14016,55 @@ var server = async (ctx) => {
|
|
|
13740
14016
|
}
|
|
13741
14017
|
}
|
|
13742
14018
|
});
|
|
14019
|
+
const panelTool = tool({
|
|
14020
|
+
description: `Display a rich context usage panel showing:
|
|
14021
|
+
- Current token usage vs model limit
|
|
14022
|
+
- Message breakdown (user/assistant/tools)
|
|
14023
|
+
- Token distribution by role
|
|
14024
|
+
- Compression history and savings
|
|
14025
|
+
- Cost estimate
|
|
14026
|
+
- Topic distribution
|
|
14027
|
+
- Smart recommendations`,
|
|
14028
|
+
args: {},
|
|
14029
|
+
async execute(_args, context) {
|
|
14030
|
+
const config2 = getConfig(context.sessionID);
|
|
14031
|
+
const state = getState(context.sessionID, config2);
|
|
14032
|
+
try {
|
|
14033
|
+
const response = await ctx.client.session.messages({
|
|
14034
|
+
path: { id: context.sessionID }
|
|
14035
|
+
});
|
|
14036
|
+
if (!response.data || response.error) {
|
|
14037
|
+
return "Failed to fetch messages";
|
|
14038
|
+
}
|
|
14039
|
+
const messageList = response.data;
|
|
14040
|
+
const messageWithParts = messageList.map((m) => ({
|
|
14041
|
+
info: m.info,
|
|
14042
|
+
parts: m.parts
|
|
14043
|
+
}));
|
|
14044
|
+
const modelId = context.model?.id || "unknown";
|
|
14045
|
+
const panelData = await buildPanelData(
|
|
14046
|
+
context.sessionID,
|
|
14047
|
+
messageWithParts,
|
|
14048
|
+
state,
|
|
14049
|
+
config2,
|
|
14050
|
+
modelId
|
|
14051
|
+
);
|
|
14052
|
+
const panel = renderPanel(panelData);
|
|
14053
|
+
return {
|
|
14054
|
+
title: "Context Panel",
|
|
14055
|
+
output: panel,
|
|
14056
|
+
metadata: {
|
|
14057
|
+
usagePercent: panelData.usagePercent,
|
|
14058
|
+
status: panelData.status,
|
|
14059
|
+
currentTokens: panelData.currentTokens,
|
|
14060
|
+
maxTokens: panelData.maxTokens
|
|
14061
|
+
}
|
|
14062
|
+
};
|
|
14063
|
+
} catch (error45) {
|
|
14064
|
+
return `Error generating panel: ${error45 instanceof Error ? error45.message : "Unknown error"}`;
|
|
14065
|
+
}
|
|
14066
|
+
}
|
|
14067
|
+
});
|
|
13743
14068
|
return {
|
|
13744
14069
|
config: async (opencodeConfig) => {
|
|
13745
14070
|
if (!opencodeConfig.permission) {
|
|
@@ -13747,9 +14072,11 @@ var server = async (ctx) => {
|
|
|
13747
14072
|
}
|
|
13748
14073
|
;
|
|
13749
14074
|
opencodeConfig.permission.compress = globalConfig2.compress.permission;
|
|
14075
|
+
opencodeConfig.permission.panel = "allow";
|
|
13750
14076
|
},
|
|
13751
14077
|
tool: {
|
|
13752
|
-
compress: compressTool
|
|
14078
|
+
compress: compressTool,
|
|
14079
|
+
panel: panelTool
|
|
13753
14080
|
},
|
|
13754
14081
|
"experimental.chat.system.transform": async (input, output) => {
|
|
13755
14082
|
const config2 = getConfig(input.sessionID || "");
|