@agnishc/edb-context-viewer 0.10.8 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0] - 2026-05-22
4
+
5
+ ### Changed
6
+ - Show skills and safe-available tokens in stats tab
7
+
8
+ ## [0.10.9] - 2026-05-18
9
+
3
10
  ## [0.10.8] - 2026-05-18
4
11
 
5
12
  ## [0.10.6] - 2026-05-15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-context-viewer",
3
- "version": "0.10.8",
3
+ "version": "0.12.0",
4
4
  "description": "Pi extension: inspect the system prompt and full LLM context in scrollable overlay popups",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  buildSessionContext,
19
19
  type ContextUsage,
20
+ DEFAULT_COMPACTION_SETTINGS,
20
21
  type ExtensionAPI,
21
22
  type ExtensionCommandContext,
22
23
  type SessionContext,
@@ -197,6 +198,16 @@ function buildToolsText(activeToolDefs: { name: string; description?: string; pa
197
198
  }
198
199
 
199
200
  /** Build the token breakdown, scaling raw char-based estimates to match actual token count. */
201
+ function isSkillPath(path: unknown): boolean {
202
+ if (typeof path !== "string") return false;
203
+ return /(^|\/)\.agents\/skills\/|(^|\/)\.pi\/agent\/.*\/skills\/|(^|\/)skills\/[^/]+\/SKILL\.md$/i.test(path);
204
+ }
205
+
206
+ function isSkillReadToolCall(block: any): boolean {
207
+ if (block?.name !== "read") return false;
208
+ return isSkillPath(block.arguments?.path ?? block.input?.path ?? block.args?.path);
209
+ }
210
+
200
211
  function buildTokenBreakdown(
201
212
  systemPrompt: string,
202
213
  activeToolDefs: unknown[],
@@ -206,6 +217,7 @@ function buildTokenBreakdown(
206
217
  if (!usage?.tokens || !usage.contextWindow) return null;
207
218
 
208
219
  const estimateTokens = (text: string) => Math.ceil(text.length / 4);
220
+ const reserveTokens = Math.min(DEFAULT_COMPACTION_SETTINGS.reserveTokens, usage.contextWindow);
209
221
 
210
222
  const systemRaw = estimateTokens(systemPrompt);
211
223
  const toolDefsRaw = estimateTokens(JSON.stringify(activeToolDefs));
@@ -213,6 +225,8 @@ function buildTokenBreakdown(
213
225
  let msgTokensRaw = 0;
214
226
  let toolCallTokensRaw = 0;
215
227
  let toolResultTokensRaw = 0;
228
+ let skillsRaw = 0;
229
+ const skillToolCallIds = new Set<string>();
216
230
 
217
231
  for (const entry of branch) {
218
232
  if (entry.type === "message" && entry.message) {
@@ -230,13 +244,24 @@ function buildTokenBreakdown(
230
244
  else if (Array.isArray(m.content)) {
231
245
  for (const p of m.content) {
232
246
  if (p?.type === "text") msgTokensRaw += estimateTokens(p.text ?? "");
233
- else if (p?.type === "toolCall") toolCallTokensRaw += estimateTokens(JSON.stringify(p));
247
+ else if (p?.type === "toolCall") {
248
+ if (isSkillReadToolCall(p)) {
249
+ skillsRaw += estimateTokens(JSON.stringify(p));
250
+ if (p.id) skillToolCallIds.add(p.id);
251
+ } else {
252
+ toolCallTokensRaw += estimateTokens(JSON.stringify(p));
253
+ }
254
+ }
234
255
  }
235
256
  }
236
257
  } else if (m.role === "toolResult") {
258
+ const isSkillResult = skillToolCallIds.has(m.toolCallId);
237
259
  if (Array.isArray(m.content)) {
238
260
  for (const p of m.content) {
239
- if (p?.type === "text") toolResultTokensRaw += estimateTokens(p.text ?? "");
261
+ if (p?.type === "text") {
262
+ if (isSkillResult) skillsRaw += estimateTokens(p.text ?? "");
263
+ else toolResultTokensRaw += estimateTokens(p.text ?? "");
264
+ }
240
265
  }
241
266
  }
242
267
  } else if (m.role === "bashExecution") {
@@ -248,22 +273,26 @@ function buildTokenBreakdown(
248
273
  }
249
274
  }
250
275
 
251
- const totalRaw = systemRaw + toolDefsRaw + msgTokensRaw + toolCallTokensRaw + toolResultTokensRaw;
276
+ const totalRaw = systemRaw + skillsRaw + toolDefsRaw + msgTokensRaw + toolCallTokensRaw + toolResultTokensRaw;
252
277
  const ratio = totalRaw > 0 ? usage.tokens / totalRaw : 1;
253
278
 
254
279
  const sys = Math.round(systemRaw * ratio);
255
- const tools = Math.round(toolDefsRaw * ratio);
280
+ const skills = Math.round(skillsRaw * ratio);
281
+ const systemTools = Math.round(toolDefsRaw * ratio);
256
282
  const msgs = Math.round(msgTokensRaw * ratio);
257
- const toolCalls = Math.round((toolCallTokensRaw + toolResultTokensRaw) * ratio);
258
- const accounted = sys + tools + msgs + toolCalls;
283
+ const tools = Math.round((toolCallTokensRaw + toolResultTokensRaw) * ratio);
284
+ const accounted = sys + skills + systemTools + msgs + tools;
259
285
 
260
286
  return {
261
287
  total: usage.tokens,
262
288
  contextWindow: usage.contextWindow,
263
289
  percent: usage.percent ?? (usage.tokens / usage.contextWindow) * 100,
290
+ reserveTokens,
291
+ safeAvailable: Math.max(0, usage.contextWindow - reserveTokens - usage.tokens),
264
292
  systemPrompt: sys,
265
- systemTools: tools,
266
- toolCalls,
293
+ systemTools,
294
+ tools,
295
+ skills,
267
296
  messages: msgs,
268
297
  other: Math.max(0, usage.tokens - accounted),
269
298
  };
@@ -322,8 +351,13 @@ export default function contextViewerExtension(pi: ExtensionAPI): void {
322
351
  }
323
352
  const messagesText = messagesLines.join("\n");
324
353
 
354
+ const modelName = ctx.model?.id ?? (ctx.model as any)?.modelId ?? "unknown model";
355
+
325
356
  const tabs = [
326
- new StatsTabContent(breakdown, theme),
357
+ new StatsTabContent(breakdown, theme, {
358
+ name: modelName,
359
+ contextWindow: ctx.model?.contextWindow ?? usage?.contextWindow,
360
+ }),
327
361
  new ScrollableTabContent(
328
362
  { rawText: systemPrompt, displayLines: buildNumberedLines(systemPrompt, theme), theme },
329
363
  "System",
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * StatsTabContent — token distribution grid + category breakdown table.
3
3
  *
4
- * Adapts the visual dashboard from pi-context (github.com/ttttmr/pi-context)
5
- * for use inside TabbedOverlay. Shows a colored 10×5 block grid on the left and
6
- * a per-category token breakdown on the right.
4
+ * Shows a compact model/context summary, then a colored 10×5 grid beside an
5
+ * estimated per-category usage breakdown. The final grid segment is reserved for
6
+ * Pi's auto-compaction response buffer.
7
7
  */
8
8
 
9
9
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -15,23 +15,106 @@ const GRID_WIDTH = 10;
15
15
  const GRID_HEIGHT = 5;
16
16
  const TOTAL_BLOCKS = GRID_WIDTH * GRID_HEIGHT; // 50 blocks = 2% each
17
17
 
18
+ const ANSI_RESET = "\x1b[0m";
19
+ const ANSI_BOLD = "\x1b[1m";
20
+
21
+ function fg(hex: string, text: string): string {
22
+ const normalized = hex.replace("#", "");
23
+ const r = Number.parseInt(normalized.slice(0, 2), 16);
24
+ const g = Number.parseInt(normalized.slice(2, 4), 16);
25
+ const b = Number.parseInt(normalized.slice(4, 6), 16);
26
+ return `\x1b[38;2;${r};${g};${b}m${text}${ANSI_RESET}`;
27
+ }
28
+
29
+ function bold(text: string): string {
30
+ return `${ANSI_BOLD}${text}${ANSI_RESET}`;
31
+ }
32
+
18
33
  export interface ContextTokenBreakdown {
19
34
  total: number;
20
35
  contextWindow: number;
21
36
  percent: number;
37
+ reserveTokens: number;
38
+ safeAvailable: number;
22
39
  systemPrompt: number;
23
40
  systemTools: number;
24
- toolCalls: number;
41
+ tools: number;
42
+ skills: number;
25
43
  messages: number;
26
44
  other: number;
27
45
  }
28
46
 
47
+ export interface StatsModelInfo {
48
+ name: string;
49
+ contextWindow?: number;
50
+ }
51
+
29
52
  interface Category {
53
+ key: keyof Pick<
54
+ ContextTokenBreakdown,
55
+ "systemPrompt" | "systemTools" | "tools" | "skills" | "messages" | "safeAvailable" | "reserveTokens"
56
+ >;
30
57
  label: string;
58
+ icon: string;
59
+ fallbackIcon: string;
31
60
  value: number;
32
- color: string;
61
+ hex: string;
62
+ block: string;
63
+ includeInGrid: boolean;
33
64
  }
34
65
 
66
+ const CATEGORY_META = {
67
+ systemPrompt: {
68
+ label: "System prompt",
69
+ icon: "󰈙",
70
+ fallbackIcon: "●",
71
+ hex: "#A78BFA",
72
+ block: "󰈙",
73
+ },
74
+ systemTools: {
75
+ label: "System tools",
76
+ icon: "󰒓",
77
+ fallbackIcon: "◆",
78
+ hex: "#22D3EE",
79
+ block: "󰒓",
80
+ },
81
+ tools: {
82
+ label: "Tools",
83
+ icon: "󰐥",
84
+ fallbackIcon: "▲",
85
+ hex: "#34D399",
86
+ block: "󰐥",
87
+ },
88
+ skills: {
89
+ label: "Skills",
90
+ icon: "󰌵",
91
+ fallbackIcon: "✦",
92
+ hex: "#FBBF24",
93
+ block: "󰌵",
94
+ },
95
+ messages: {
96
+ label: "Messages",
97
+ icon: "󰍩",
98
+ fallbackIcon: "■",
99
+ hex: "#60A5FA",
100
+ block: "󰍩",
101
+ },
102
+ safeAvailable: {
103
+ label: "Available",
104
+ icon: "󰋙",
105
+ fallbackIcon: "□",
106
+ hex: "#6B7280",
107
+ block: "󰋙",
108
+ },
109
+ reserveTokens: {
110
+ label: "Auto-compact buffer",
111
+ icon: "󰅐",
112
+ fallbackIcon: "▨",
113
+ hex: "#FB923C",
114
+ block: "󰅐",
115
+ },
116
+ } as const;
117
+
35
118
  export class StatsTabContent implements TabContent {
36
119
  readonly name = "Stats";
37
120
  readonly footerHints = "";
@@ -39,6 +122,7 @@ export class StatsTabContent implements TabContent {
39
122
  constructor(
40
123
  private breakdown: ContextTokenBreakdown | null,
41
124
  private theme: Theme,
125
+ private modelInfo?: StatsModelInfo,
42
126
  ) {}
43
127
 
44
128
  /** Stats view has no interactive search bar — always use border separator. */
@@ -48,8 +132,8 @@ export class StatsTabContent implements TabContent {
48
132
 
49
133
  getFooterLeft(): string {
50
134
  if (!this.breakdown) return "";
51
- const { total, contextWindow, percent } = this.breakdown;
52
- return `${formatTokens(total)} / ${formatTokens(contextWindow)} (${percent.toFixed(1)}%)`;
135
+ const { total, contextWindow, percent, safeAvailable } = this.breakdown;
136
+ return `${formatTokens(total)} / ${formatTokens(contextWindow)} (${percent.toFixed(1)}%) · ${formatTokens(safeAvailable)} safe left`;
53
137
  }
54
138
 
55
139
  /** Stats view has no keyboard interactions. */
@@ -72,103 +156,112 @@ export class StatsTabContent implements TabContent {
72
156
  return lines;
73
157
  }
74
158
 
75
- const { total, contextWindow, percent, systemPrompt, systemTools, toolCalls, messages, other } = this.breakdown;
159
+ const { total, contextWindow, percent, reserveTokens, safeAvailable } = this.breakdown;
160
+ const modelName = this.modelInfo?.name ?? "unknown model";
161
+ const safeLeftText =
162
+ safeAvailable > 0 ? `${formatTokens(safeAvailable)} safe left` : "auto-compact threshold reached";
163
+
164
+ const categories = this.getCategories();
165
+ const gridLines = this.renderGrid(categories);
166
+ const breakdownLines = this.renderBreakdown(categories);
76
167
 
77
- const categories: Category[] = [
78
- { label: "System Prompt", value: systemPrompt, color: "muted" },
79
- { label: "System Tools", value: systemTools, color: "dim" },
80
- { label: "Tool Calls", value: toolCalls, color: "success" },
81
- { label: "Messages", value: messages, color: "accent" },
168
+ const lines: string[] = [
169
+ ` ${bold(`${modelName} · ${formatTokens(total)}/${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`)} ${th.fg("dim", ${safeLeftText}`)}`,
170
+ "",
171
+ "",
172
+ ` ${th.fg("dim", "Estimated usage by category")}`,
173
+ "",
82
174
  ];
83
175
 
84
- if (other > 10) {
85
- categories.push({ label: "Other", value: other, color: "dim" });
176
+ const GRID_VIS_W = GRID_WIDTH * 2 - 1;
177
+ const maxRows = Math.max(gridLines.length, breakdownLines.length);
178
+ for (let i = 0; i < maxRows; i++) {
179
+ const leftRaw = gridLines[i] ?? "";
180
+ const leftVisW = visibleWidth(leftRaw);
181
+ const pad = " ".repeat(Math.max(0, GRID_VIS_W - leftVisW));
182
+ const right = breakdownLines[i] ?? "";
183
+ lines.push(` ${leftRaw}${pad} ${right}`);
184
+ }
185
+
186
+ if (safeAvailable <= 0) {
187
+ lines.push("");
188
+ lines.push(` ${fg(CATEGORY_META.reserveTokens.hex, "Auto-compact buffer is being used")}`);
189
+ } else {
190
+ lines.push("");
191
+ lines.push(
192
+ ` ${th.fg("dim", `Auto-compact starts after ${formatTokens(contextWindow - reserveTokens)} tokens`)}`,
193
+ );
86
194
  }
87
195
 
88
- const available = Math.max(0, contextWindow - total);
196
+ while (lines.length < height) lines.push("");
197
+ return lines.slice(0, height);
198
+ }
199
+
200
+ private getCategories(): Category[] {
201
+ const b = this.breakdown!;
202
+ return [
203
+ this.category("systemPrompt", b.systemPrompt, true),
204
+ this.category("systemTools", b.systemTools, true),
205
+ this.category("tools", b.tools, true),
206
+ this.category("skills", b.skills, true),
207
+ this.category("messages", b.messages + b.other, true),
208
+ this.category("safeAvailable", b.safeAvailable, true),
209
+ this.category("reserveTokens", b.reserveTokens, true),
210
+ ];
211
+ }
212
+
213
+ private category(key: Category["key"], value: number, includeInGrid: boolean): Category {
214
+ const meta = CATEGORY_META[key];
215
+ return { key, value, includeInGrid, ...meta };
216
+ }
217
+
218
+ private renderGrid(categories: Category[]): string[] {
219
+ const blocks: string[] = [];
220
+ const { contextWindow, reserveTokens } = this.breakdown!;
221
+ const reserveBlockCount = Math.max(1, Math.round((reserveTokens / contextWindow) * TOTAL_BLOCKS));
222
+ const safeBlockCount = Math.max(0, TOTAL_BLOCKS - reserveBlockCount);
223
+ const safeWindow = Math.max(1, contextWindow - reserveTokens);
224
+ const safeCategories = categories.filter((cat) => cat.key !== "reserveTokens");
89
225
 
90
- // ── Build grid blocks ────────────────────────────────────────────────────
91
- const blocks: { color: string; filled: boolean }[] = [];
92
- for (const cat of categories) {
93
- let count = Math.round((cat.value / contextWindow) * TOTAL_BLOCKS);
226
+ for (const cat of safeCategories) {
227
+ let count = Math.round((cat.value / safeWindow) * safeBlockCount);
94
228
  if (count === 0 && cat.value > 0) count = 1;
95
- for (let i = 0; i < count && blocks.length < TOTAL_BLOCKS; i++) {
96
- blocks.push({ color: cat.color, filled: true });
229
+ for (let j = 0; j < count && blocks.length < safeBlockCount; j++) {
230
+ blocks.push(fg(cat.hex, cat.block));
97
231
  }
98
232
  }
99
- while (blocks.length < TOTAL_BLOCKS) {
100
- blocks.push({ color: "borderMuted", filled: false });
233
+
234
+ while (blocks.length < safeBlockCount) {
235
+ blocks.push(fg(CATEGORY_META.safeAvailable.hex, CATEGORY_META.safeAvailable.block));
236
+ }
237
+
238
+ for (let i = 0; i < reserveBlockCount && blocks.length < TOTAL_BLOCKS; i++) {
239
+ blocks.push(fg(CATEGORY_META.reserveTokens.hex, CATEGORY_META.reserveTokens.block));
101
240
  }
102
241
 
103
- // ── Render grid rows ─────────────────────────────────────────────────────
104
242
  const gridLines: string[] = [];
105
243
  for (let r = 0; r < GRID_HEIGHT; r++) {
106
244
  let row = "";
107
245
  for (let c = 0; c < GRID_WIDTH; c++) {
108
- const b = blocks[r * GRID_WIDTH + c]!;
109
- row += th.fg(b.color as Parameters<typeof th.fg>[0], b.filled ? "■" : "□");
246
+ row += blocks[r * GRID_WIDTH + c] ?? fg(CATEGORY_META.safeAvailable.hex, CATEGORY_META.safeAvailable.block);
110
247
  if (c < GRID_WIDTH - 1) row += " ";
111
248
  }
112
249
  gridLines.push(row);
113
250
  }
251
+ return gridLines;
252
+ }
114
253
 
115
- // ── Build legend ─────────────────────────────────────────────────────────
116
- const LABEL_W = 14;
254
+ private renderBreakdown(categories: Category[]): string[] {
255
+ const LABEL_W = 21;
117
256
  const TOKEN_W = 7;
118
257
 
119
- const legendLines: string[] = [];
120
-
121
- // Total usage line (bold, no icon)
122
- legendLines.push(
123
- ` ${th.bold(th.fg("text", "Total Usage".padEnd(LABEL_W + 2)))} ` +
124
- `${th.fg("accent", formatTokens(total).padStart(TOKEN_W))} ` +
125
- `${th.fg("dim", `(${percent.toFixed(1).padStart(5)}%)`)}`,
126
- );
127
- legendLines.push(""); // blank separator before categories
128
-
129
- // Per-category lines
130
- for (const cat of categories) {
131
- const pct = ((cat.value / contextWindow) * 100).toFixed(1);
132
- legendLines.push(
133
- `${th.fg(cat.color as Parameters<typeof th.fg>[0], "■")} ` +
134
- `${th.fg("text", cat.label.padEnd(LABEL_W))} ` +
135
- `${th.fg("accent", formatTokens(cat.value).padStart(TOKEN_W))} ` +
136
- `${th.fg("dim", `(${pct.padStart(5)}%)`)}`,
137
- );
138
- }
139
-
140
- // Available line
141
- const availPct = ((available / contextWindow) * 100).toFixed(1);
142
- legendLines.push(
143
- `${th.fg("borderMuted" as Parameters<typeof th.fg>[0], "□")} ` +
144
- `${th.fg("dim", "Available".padEnd(LABEL_W))} ` +
145
- `${th.fg("dim", formatTokens(available).padStart(TOKEN_W))} ` +
146
- `${th.fg("dim", `(${availPct.padStart(5)}%)`)}`,
147
- );
148
-
149
- // ── Combine grid + legend side by side ───────────────────────────────────
150
- // Grid visible width: GRID_WIDTH * 2 - 1 (each "■ " or "□ " = 2, minus trailing space)
151
- const GRID_VIS_W = GRID_WIDTH * 2 - 1;
152
- const maxRows = Math.max(gridLines.length, legendLines.length);
153
-
154
- const combined: string[] = [];
155
- combined.push(""); // top padding
156
-
157
- for (let i = 0; i < maxRows; i++) {
158
- const leftRaw = gridLines[i] ?? "";
159
- const leftVisW = visibleWidth(leftRaw);
160
- const pad = " ".repeat(Math.max(0, GRID_VIS_W - leftVisW));
161
- const right = legendLines[i] ?? "";
162
- combined.push(` ${leftRaw}${pad} ${right}`);
163
- }
164
-
165
- combined.push(""); // bottom padding
166
-
167
- // Fill or truncate to content height
168
- const result: string[] = [];
169
- for (let i = 0; i < height; i++) {
170
- result.push(combined[i] ?? "");
171
- }
172
- return result;
258
+ return categories.map((cat) => {
259
+ const pct = this.breakdown!.contextWindow > 0 ? (cat.value / this.breakdown!.contextWindow) * 100 : 0;
260
+ const icon = fg(cat.hex, cat.icon);
261
+ const label = fg(cat.hex, cat.label.padEnd(LABEL_W));
262
+ const tokens = fg(cat.hex, formatTokens(cat.value).padStart(TOKEN_W));
263
+ const percent = this.theme.fg("dim", `(${pct.toFixed(1).padStart(5)}%)`);
264
+ return `${icon} ${label} ${tokens} ${percent}`;
265
+ });
173
266
  }
174
267
  }