@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 CHANGED
@@ -1,15 +1,22 @@
1
1
  # opencode-slim
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@serkanalgur/opencode-slim.svg)](https://www.npmjs.com/package/@serkanalgur/opencode-slim)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- - `/slim` - Show current context stats and compression history
84
- - `/slim compress` - Manually trigger compression
85
- - `/slim reset` - Reset adaptive learning
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
- | File count | ~8 | ~150 |
149
+ | Topic extraction | | |
150
+ | File count | ~10 | ~150 |
96
151
  | License | MIT | AGPL-3.0 |
97
152
  | Config complexity | Low | High |
98
153
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAoSjD,wBAA8C"}
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("Description of what content should be compressed")
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
- for (const msg of messageWithParts) {
13704
- const text = getMessageText(msg) + getToolResultContent(msg);
13705
- inputTokens += await countTokens(text);
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
- const targets = messageWithParts.length > 10 ? [{ start: 0, end: messageWithParts.length - 5, reason: "user_requested", estimatedTokens: inputTokens }] : [];
13708
- if (targets.length === 0) {
13983
+ if (targetIndices.length === 0) {
13709
13984
  return "Nothing to compress - context is already efficient";
13710
13985
  }
13711
- const compressedMessages = messageWithParts.slice(targets[0].start, targets[0].end);
13712
- const summary = buildCompressionSummary(compressedMessages, args.focus);
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: compressedMessages.length,
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 ${compressedMessages.length} messages`,
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 || "");