pi-mono-all 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.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Context — visualizes current context-window usage.
3
+ *
4
+ * Usage:
5
+ * /context
6
+ */
7
+
8
+ import type { ContextUsage, ExtensionAPI, ExtensionCommandContext, Theme, ToolInfo } from "@mariozechner/pi-coding-agent";
9
+ import type { SlashCommandInfo } from "@mariozechner/pi-coding-agent";
10
+ import { Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
+
12
+ type TextBlock = { type?: string; text?: string };
13
+ type ImageBlock = { type?: string; data?: string; mimeType?: string };
14
+ type ThinkingBlock = { type?: string; thinking?: string };
15
+ type ToolCallBlock = { type?: string; name?: string; arguments?: Record<string, unknown>; toolCallId?: string };
16
+ type ContentBlock = TextBlock | ImageBlock | ThinkingBlock | ToolCallBlock | Record<string, unknown>;
17
+
18
+ type MessageLike = {
19
+ role?: string;
20
+ content?: string | ContentBlock[];
21
+ usage?: {
22
+ input?: number;
23
+ output?: number;
24
+ cacheRead?: number;
25
+ cacheWrite?: number;
26
+ cost?: { total?: number };
27
+ };
28
+ toolName?: string;
29
+ summary?: string;
30
+ customType?: string;
31
+ };
32
+
33
+ type SessionEntryLike = {
34
+ type?: string;
35
+ message?: MessageLike;
36
+ customType?: string;
37
+ content?: string | ContentBlock[];
38
+ summary?: string;
39
+ };
40
+
41
+ type CategoryKey =
42
+ | "system"
43
+ | "systemTools"
44
+ | "custom"
45
+ | "compaction"
46
+ | "skills"
47
+ | "messages"
48
+ | "free";
49
+
50
+ interface Category {
51
+ key: CategoryKey;
52
+ label: string;
53
+ tokens: number;
54
+ glyph: string;
55
+ color: (theme: Theme, text: string) => string;
56
+ }
57
+
58
+ interface SectionItem {
59
+ label: string;
60
+ tokens?: number;
61
+ }
62
+
63
+ interface DetailSection {
64
+ title: string;
65
+ subtitle?: string;
66
+ groups: Array<{ title?: string; items: SectionItem[] }>;
67
+ }
68
+
69
+ interface ExtensionAllocation {
70
+ name: string;
71
+ tokens: number;
72
+ tools: number;
73
+ commands: number;
74
+ customMessages: number;
75
+ }
76
+
77
+ interface ContextBreakdown {
78
+ categories: Category[];
79
+ totalTokens: number;
80
+ contextWindow: number;
81
+ percent: number | null;
82
+ cacheRead: number;
83
+ cacheWrite: number;
84
+ totalCost: number;
85
+ messageCount: number;
86
+ turnCount: number;
87
+ modelLabel: string;
88
+ modelId: string;
89
+ detailSections: DetailSection[];
90
+ extensionAllocations: ExtensionAllocation[];
91
+ }
92
+
93
+ const CUSTOM_TYPE = "pi-mono-context";
94
+ const IMAGE_TOKEN_ESTIMATE = 1600;
95
+ const GRID_COLS = 20;
96
+ const GRID_ROWS = 10;
97
+ const MAX_DETAIL_ITEMS = 48;
98
+
99
+ const ansi256Fg = (code: number, text: string) => `\x1b[38;5;${code}m${text}\x1b[0m`;
100
+
101
+ function estimateStringTokens(text: string): number {
102
+ return Math.ceil(text.length / 4);
103
+ }
104
+
105
+ function estimateContentTokens(content: unknown): { text: number; images: number } {
106
+ if (typeof content === "string") return { text: estimateStringTokens(content), images: 0 };
107
+ if (!Array.isArray(content)) return { text: 0, images: 0 };
108
+
109
+ let text = 0;
110
+ let images = 0;
111
+ for (const rawBlock of content) {
112
+ if (!rawBlock || typeof rawBlock !== "object") continue;
113
+ const block = rawBlock as ContentBlock;
114
+ if (block.type === "text" && typeof (block as TextBlock).text === "string") {
115
+ text += estimateStringTokens((block as TextBlock).text ?? "");
116
+ } else if (block.type === "image") {
117
+ images += IMAGE_TOKEN_ESTIMATE;
118
+ }
119
+ }
120
+
121
+ return { text, images };
122
+ }
123
+
124
+ function formatTokens(n: number): string {
125
+ if (n >= 1_000_000) {
126
+ const value = n / 1_000_000;
127
+ return `${value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, "")}m`;
128
+ }
129
+ if (n >= 1_000) {
130
+ const value = n / 1_000;
131
+ return `${value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, "")}k`;
132
+ }
133
+ return String(n);
134
+ }
135
+
136
+ function formatPercent(tokens: number, contextWindow: number): string {
137
+ if (contextWindow <= 0) return "0.0%";
138
+ return `${((tokens / contextWindow) * 100).toFixed(1)}%`;
139
+ }
140
+
141
+ function safeJsonTokens(value: unknown): number {
142
+ try {
143
+ return estimateStringTokens(JSON.stringify(value ?? {}) ?? "{}");
144
+ } catch {
145
+ return 0;
146
+ }
147
+ }
148
+
149
+ function getModelId(ctx: ExtensionCommandContext): string {
150
+ return ctx.model?.id ?? "unknown-model";
151
+ }
152
+
153
+ function getModelLabel(ctx: ExtensionCommandContext, contextWindow: number): string {
154
+ const model = ctx.model as { name?: string; id?: string } | undefined;
155
+ if (model?.name) return `${model.name} (${formatTokens(contextWindow)} context)`;
156
+ const id = model?.id ?? "Unknown model";
157
+ const short = id.split("/").pop() ?? id;
158
+ return `${short} (${formatTokens(contextWindow)} context)`;
159
+ }
160
+
161
+ function sourceAllocationName(value: unknown): string {
162
+ if (!value || typeof value !== "object") return "unknown";
163
+ const source = value as { sourceInfo?: { source?: string; baseDir?: string; path?: string }; customType?: string };
164
+ if (source.customType) return source.customType;
165
+ const info = source.sourceInfo;
166
+ const raw = info?.source ?? info?.baseDir ?? info?.path ?? "unknown";
167
+ const parts = raw.split(/[\\/]/).filter(Boolean);
168
+ const extensionIndex = parts.lastIndexOf("extensions");
169
+ if (extensionIndex >= 0 && parts[extensionIndex + 1]) return parts[extensionIndex + 1]!;
170
+ const packageIndex = parts.lastIndexOf("node_modules");
171
+ if (packageIndex >= 0 && parts[packageIndex + 1]) return parts[packageIndex + 1]!;
172
+ return parts.at(-1)?.replace(/\.ts$/, "") || raw;
173
+ }
174
+
175
+ function addAllocation(
176
+ allocations: Map<string, ExtensionAllocation>,
177
+ name: string,
178
+ kind: "tools" | "commands" | "customMessages",
179
+ tokens: number,
180
+ ): void {
181
+ if (tokens <= 0) return;
182
+ const current = allocations.get(name) ?? { name, tokens: 0, tools: 0, commands: 0, customMessages: 0 };
183
+ current.tokens += tokens;
184
+ current[kind] += tokens;
185
+ allocations.set(name, current);
186
+ }
187
+
188
+ function mergeAllocations(...groups: ExtensionAllocation[][]): ExtensionAllocation[] {
189
+ const merged = new Map<string, ExtensionAllocation>();
190
+ for (const group of groups) {
191
+ for (const item of group) {
192
+ const current = merged.get(item.name) ?? { name: item.name, tokens: 0, tools: 0, commands: 0, customMessages: 0 };
193
+ current.tokens += item.tokens;
194
+ current.tools += item.tools;
195
+ current.commands += item.commands;
196
+ current.customMessages += item.customMessages;
197
+ merged.set(item.name, current);
198
+ }
199
+ }
200
+ return Array.from(merged.values()).sort((a, b) => b.tokens - a.tokens);
201
+ }
202
+
203
+ function buildToolSections(pi: ExtensionAPI): { systemToolsTokens: number; detailSections: DetailSection[]; allocations: ExtensionAllocation[] } {
204
+ let allTools: ToolInfo[] = [];
205
+ let activeToolNames: string[] = [];
206
+ try {
207
+ allTools = pi.getAllTools();
208
+ activeToolNames = pi.getActiveTools();
209
+ } catch {
210
+ return { systemToolsTokens: 0, detailSections: [], allocations: [] };
211
+ }
212
+
213
+ const active = new Set(activeToolNames);
214
+ const activeTools = allTools.filter((tool) => active.has(tool.name));
215
+ const allocationsByExtension = new Map<string, ExtensionAllocation>();
216
+ const systemToolsTokens = activeTools.reduce((sum, tool) => {
217
+ const tokens = estimateStringTokens(`${tool.name}\n${tool.description ?? ""}\n${JSON.stringify(tool.parameters ?? {})}`);
218
+ addAllocation(allocationsByExtension, sourceAllocationName(tool), "tools", tokens);
219
+ return sum + tokens;
220
+ }, 0);
221
+
222
+ const items = activeTools
223
+ .slice()
224
+ .sort((a, b) => a.name.localeCompare(b.name))
225
+ .slice(0, MAX_DETAIL_ITEMS)
226
+ .map((tool) => ({ label: tool.name }));
227
+
228
+ if (activeTools.length > MAX_DETAIL_ITEMS) {
229
+ items.push({ label: `… ${activeTools.length - MAX_DETAIL_ITEMS} more tools` });
230
+ }
231
+
232
+ return {
233
+ systemToolsTokens,
234
+ allocations: Array.from(allocationsByExtension.values()).sort((a, b) => b.tokens - a.tokens),
235
+ detailSections: items.length
236
+ ? [
237
+ {
238
+ title: "Tools · active in current session",
239
+ subtitle: "Available",
240
+ groups: [{ items }],
241
+ },
242
+ ]
243
+ : [],
244
+ };
245
+ }
246
+
247
+ function buildCommandSections(pi: ExtensionAPI): { commandTokens: number; detailSections: DetailSection[]; allocations: ExtensionAllocation[] } {
248
+ let commands: SlashCommandInfo[] = [];
249
+ try {
250
+ commands = pi.getCommands();
251
+ } catch {
252
+ return { commandTokens: 0, detailSections: [], allocations: [] };
253
+ }
254
+
255
+ const skillItems = commands
256
+ .filter((command) => command.source === "skill")
257
+ .sort((a, b) => a.name.localeCompare(b.name))
258
+ .map((command) => ({ label: command.name, tokens: estimateStringTokens(command.description ?? command.name) }));
259
+
260
+ const extensionItems = commands
261
+ .filter((command) => command.source === "extension")
262
+ .sort((a, b) => a.name.localeCompare(b.name))
263
+ .map((command) => ({ label: command.name, tokens: estimateStringTokens(command.description ?? command.name) }));
264
+
265
+ const allocationsByExtension = new Map<string, ExtensionAllocation>();
266
+ for (const command of commands.filter((command) => command.source === "extension" || command.source === "skill")) {
267
+ const tokens = estimateStringTokens(command.description ?? command.name);
268
+ addAllocation(allocationsByExtension, sourceAllocationName(command), "commands", tokens);
269
+ }
270
+ const commandTokens = [...skillItems, ...extensionItems].reduce((sum, item) => sum + (item.tokens ?? 0), 0);
271
+ const sections: DetailSection[] = [];
272
+ if (skillItems.length) {
273
+ sections.push({
274
+ title: "Skills · /skills",
275
+ groups: [{ title: "Available", items: skillItems.slice(0, MAX_DETAIL_ITEMS) }],
276
+ });
277
+ }
278
+ if (extensionItems.length) {
279
+ sections.push({
280
+ title: "Extension commands · /",
281
+ groups: [{ title: "Available", items: extensionItems.slice(0, MAX_DETAIL_ITEMS) }],
282
+ });
283
+ }
284
+ return { commandTokens, detailSections: sections, allocations: Array.from(allocationsByExtension.values()).sort((a, b) => b.tokens - a.tokens) };
285
+ }
286
+
287
+ function computeBreakdown(ctx: ExtensionCommandContext, pi: ExtensionAPI): ContextBreakdown | null {
288
+ const usage: ContextUsage | undefined = ctx.getContextUsage();
289
+ if (!usage || usage.contextWindow <= 0) return null;
290
+
291
+ const contextWindow = usage.contextWindow;
292
+ const branch = ctx.sessionManager.getBranch() as SessionEntryLike[];
293
+ const toolSections = buildToolSections(pi);
294
+ const commandSections = buildCommandSections(pi);
295
+
296
+ let systemPromptTokens = 0;
297
+ try {
298
+ systemPromptTokens = estimateStringTokens(ctx.getSystemPrompt() ?? "");
299
+ } catch {
300
+ systemPromptTokens = 0;
301
+ }
302
+
303
+ let messageTokens = 0;
304
+ let assistantTokens = 0;
305
+ let thinkingTokens = 0;
306
+ let toolResultTokens = 0;
307
+ let customTokens = 0;
308
+ let compactionTokens = 0;
309
+ let imageTokens = 0;
310
+ let cacheRead = 0;
311
+ let cacheWrite = 0;
312
+ let totalCost = 0;
313
+ let messageCount = 0;
314
+ let turnCount = 0;
315
+ const customAllocations = new Map<string, ExtensionAllocation>();
316
+
317
+ for (const entry of branch) {
318
+ const msg = entry.message;
319
+ if (entry.type === "message" && msg?.role) {
320
+ if (msg.role === "custom" && msg.customType === CUSTOM_TYPE) continue;
321
+ messageCount++;
322
+
323
+ if (msg.role === "user") {
324
+ const tokens = estimateContentTokens(msg.content);
325
+ messageTokens += tokens.text;
326
+ imageTokens += tokens.images;
327
+ } else if (msg.role === "assistant") {
328
+ turnCount++;
329
+ cacheRead += msg.usage?.cacheRead ?? 0;
330
+ cacheWrite += msg.usage?.cacheWrite ?? 0;
331
+ totalCost += msg.usage?.cost?.total ?? 0;
332
+
333
+ if (Array.isArray(msg.content)) {
334
+ for (const rawBlock of msg.content) {
335
+ if (!rawBlock || typeof rawBlock !== "object") continue;
336
+ const block = rawBlock as ContentBlock;
337
+ if (block.type === "text" && typeof (block as TextBlock).text === "string") {
338
+ assistantTokens += estimateStringTokens((block as TextBlock).text ?? "");
339
+ } else if (block.type === "thinking" && typeof (block as ThinkingBlock).thinking === "string") {
340
+ thinkingTokens += estimateStringTokens((block as ThinkingBlock).thinking ?? "");
341
+ } else if (block.type === "toolCall" || block.type === "tool_use" || "toolCallId" in block) {
342
+ const toolBlock = block as ToolCallBlock;
343
+ assistantTokens += estimateStringTokens(toolBlock.name ?? "tool") + safeJsonTokens(toolBlock.arguments);
344
+ }
345
+ }
346
+ }
347
+ } else if (msg.role === "toolResult") {
348
+ const tokens = estimateContentTokens(msg.content);
349
+ toolResultTokens += tokens.text;
350
+ imageTokens += tokens.images;
351
+ } else if (msg.role === "custom") {
352
+ const tokens = estimateContentTokens(msg.content);
353
+ customTokens += tokens.text;
354
+ imageTokens += tokens.images;
355
+ addAllocation(customAllocations, msg.customType ?? "custom", "customMessages", tokens.text + tokens.images);
356
+ } else if (msg.role === "branchSummary" || msg.role === "compactionSummary") {
357
+ compactionTokens += estimateStringTokens(msg.summary ?? "");
358
+ }
359
+ } else if (entry.type === "compaction" || entry.type === "branch_summary") {
360
+ compactionTokens += estimateStringTokens(entry.summary ?? "");
361
+ } else if (entry.type === "custom_message") {
362
+ if (entry.customType === CUSTOM_TYPE) continue;
363
+ const tokens = estimateContentTokens(entry.content);
364
+ customTokens += tokens.text;
365
+ imageTokens += tokens.images;
366
+ addAllocation(customAllocations, entry.customType ?? "custom", "customMessages", tokens.text + tokens.images);
367
+ }
368
+ }
369
+
370
+ const mk = (key: CategoryKey, label: string, tokens: number, glyph: string, code: number): Category | null => {
371
+ if (tokens <= 0 && key !== "free") return null;
372
+ return { key, label, tokens, glyph, color: (_theme, text) => ansi256Fg(code, text) };
373
+ };
374
+
375
+ const usedEstimateBeforeFree =
376
+ systemPromptTokens +
377
+ toolSections.systemToolsTokens +
378
+ messageTokens +
379
+ assistantTokens +
380
+ thinkingTokens +
381
+ toolResultTokens +
382
+ customTokens +
383
+ compactionTokens +
384
+ imageTokens +
385
+ commandSections.commandTokens;
386
+ const totalTokens = usage.tokens ?? usedEstimateBeforeFree;
387
+ const freeTokens = Math.max(0, contextWindow - totalTokens);
388
+
389
+ const conversationTokens = messageTokens + assistantTokens + thinkingTokens + toolResultTokens + imageTokens;
390
+ const categories = [
391
+ mk("system", "System prompts", systemPromptTokens, "⛁", 245),
392
+ mk("systemTools", "System tools", toolSections.systemToolsTokens, "⛁", 245),
393
+ mk("custom", "Custom agents", customTokens, "⛁", 117),
394
+ mk("compaction", "Memory files", compactionTokens, "⛁", 208),
395
+ mk("skills", "Skills", commandSections.commandTokens, "⛁", 220),
396
+ mk("messages", "Messages", conversationTokens, "⛁", 141),
397
+ mk("free", "Free space", freeTokens, "⛶", 240),
398
+ ].filter((category): category is Category => category !== null);
399
+
400
+ return {
401
+ categories,
402
+ totalTokens,
403
+ contextWindow,
404
+ percent: usage.percent,
405
+ cacheRead,
406
+ cacheWrite,
407
+ totalCost,
408
+ messageCount,
409
+ turnCount,
410
+ modelLabel: getModelLabel(ctx, contextWindow),
411
+ modelId: getModelId(ctx),
412
+ detailSections: [...toolSections.detailSections, ...commandSections.detailSections],
413
+ extensionAllocations: mergeAllocations(
414
+ toolSections.allocations,
415
+ commandSections.allocations,
416
+ Array.from(customAllocations.values()),
417
+ ),
418
+ };
419
+ }
420
+
421
+ function renderGrid(breakdown: ContextBreakdown, color = true): string[] {
422
+ const cellsTotal = GRID_COLS * GRID_ROWS;
423
+ const tokensPerCell = Math.max(1, breakdown.contextWindow / cellsTotal);
424
+ const cells: string[] = [];
425
+ const nonFreeCategories = breakdown.categories.filter((category) => category.key !== "free");
426
+ const nonFreeEstimate = nonFreeCategories.reduce((sum, category) => sum + category.tokens, 0);
427
+ const nonFreeScale = nonFreeEstimate > breakdown.totalTokens && nonFreeEstimate > 0 ? breakdown.totalTokens / nonFreeEstimate : 1;
428
+
429
+ for (const category of nonFreeCategories) {
430
+ const scaledTokens = category.tokens * nonFreeScale;
431
+ const minCells = category.tokens > 0 ? 1 : 0;
432
+ const cellCount = Math.max(minCells, Math.round(scaledTokens / tokensPerCell));
433
+ for (let i = 0; i < cellCount && cells.length < cellsTotal; i++) {
434
+ cells.push(color ? category.color({} as Theme, category.glyph) : category.glyph);
435
+ }
436
+ }
437
+
438
+ const free = breakdown.categories.find((category) => category.key === "free");
439
+ while (cells.length < cellsTotal) cells.push(free ? (color ? free.color({} as Theme, free.glyph) : free.glyph) : "⛶");
440
+
441
+ const lines: string[] = [];
442
+ for (let row = 0; row < GRID_ROWS; row++) {
443
+ const start = row * GRID_COLS;
444
+ lines.push(cells.slice(start, start + GRID_COLS).join(" "));
445
+ }
446
+ return lines;
447
+ }
448
+
449
+ function padVisible(text: string, width: number): string {
450
+ const current = visibleWidth(text);
451
+ if (current >= width) return truncateToWidth(text, width);
452
+ return text + " ".repeat(width - current);
453
+ }
454
+
455
+ function formatLegendEntry(category: Category, breakdown: ContextBreakdown, theme?: Theme): string {
456
+ const marker = theme ? category.color(theme, category.glyph) : category.glyph;
457
+ return `${marker} ${category.label}: ${formatTokens(category.tokens)} tokens (${formatPercent(category.tokens, breakdown.contextWindow)})`;
458
+ }
459
+
460
+ function pushTreeItems(lines: string[], items: SectionItem[], indent = " "): void {
461
+ items.forEach((item, index) => {
462
+ const branch = index === items.length - 1 ? "└" : "├";
463
+ const suffix = typeof item.tokens === "number" ? `: ${formatTokens(item.tokens)} tokens` : "";
464
+ lines.push(`${indent}${branch} ${item.label}${suffix}`);
465
+ });
466
+ }
467
+
468
+ function pushExtensionAllocations(lines: string[], allocations: ExtensionAllocation[], contextWindow: number): void {
469
+ if (!allocations.length) return;
470
+ lines.push("");
471
+ lines.push(" Extension allocation · estimated");
472
+ pushTreeItems(
473
+ lines,
474
+ allocations.slice(0, MAX_DETAIL_ITEMS).map((allocation) => ({
475
+ label: `${allocation.name}: ${formatTokens(allocation.tokens)} tokens (${formatPercent(allocation.tokens, contextWindow)}) · tools ${formatTokens(allocation.tools)} · commands ${formatTokens(allocation.commands)} · custom ${formatTokens(allocation.customMessages)}`,
476
+ })),
477
+ );
478
+ if (allocations.length > MAX_DETAIL_ITEMS) {
479
+ lines.push(` └ … ${allocations.length - MAX_DETAIL_ITEMS} more extensions`);
480
+ }
481
+ }
482
+
483
+ function buildOverlay(breakdown: ContextBreakdown, theme: Theme, width: number): string[] {
484
+ const maxWidth = Math.max(56, Math.min(width, 110));
485
+ const gridLines = renderGrid(breakdown);
486
+ const leftWidth = Math.max(...gridLines.map((line) => visibleWidth(line)));
487
+ const gap = " ";
488
+ const legendCategories = breakdown.categories.filter((category) => category.tokens > 0);
489
+
490
+ const rightLines = [
491
+ breakdown.modelLabel,
492
+ breakdown.modelId,
493
+ `${formatTokens(breakdown.totalTokens)}/${formatTokens(breakdown.contextWindow)} tokens (${breakdown.percent?.toFixed(0) ?? "?"}%)`,
494
+ "",
495
+ "Estimated usage by category",
496
+ ...legendCategories.map((category) => formatLegendEntry(category, breakdown, theme)),
497
+ ];
498
+
499
+ const lines: string[] = [theme.bold("Context Usage")];
500
+ const pairCount = Math.max(gridLines.length, rightLines.length);
501
+ for (let i = 0; i < pairCount; i++) {
502
+ const left = gridLines[i] ? ` ${padVisible(gridLines[i]!, leftWidth)}` : ` ${" ".repeat(leftWidth)}`;
503
+ const right = rightLines[i] ?? "";
504
+ lines.push(truncateToWidth(`${left}${gap}${right}`, maxWidth));
505
+ }
506
+
507
+ lines.push("");
508
+ lines.push(` Session Stats · Turns ${breakdown.turnCount} · Messages ${breakdown.messageCount} · Cache R ${formatTokens(breakdown.cacheRead)} · Cache W ${formatTokens(breakdown.cacheWrite)} · Cost $${breakdown.totalCost.toFixed(4)}`);
509
+
510
+ if (breakdown.percent !== null && breakdown.percent >= 95) {
511
+ lines.push(theme.fg("error", " Near context limit — compaction strongly recommended"));
512
+ } else if (breakdown.percent !== null && breakdown.percent >= 80) {
513
+ lines.push(theme.fg("warning", " Context usage above 80% — consider /compact"));
514
+ }
515
+ pushExtensionAllocations(lines, breakdown.extensionAllocations, breakdown.contextWindow);
516
+ for (const section of breakdown.detailSections) {
517
+ lines.push("");
518
+ lines.push(` ${section.title}`);
519
+ if (section.subtitle) {
520
+ lines.push("");
521
+ lines.push(` ${section.subtitle}`);
522
+ }
523
+ for (const group of section.groups) {
524
+ if (group.title) {
525
+ lines.push("");
526
+ lines.push(` ${group.title}`);
527
+ }
528
+ pushTreeItems(lines, group.items);
529
+ }
530
+ }
531
+
532
+ lines.push("");
533
+ lines.push(theme.fg("dim", " Press Escape, q, or Enter to close"));
534
+
535
+ return lines.map((line) => truncateToWidth(line, maxWidth));
536
+ }
537
+
538
+ function colorizeContextReport(report: string, theme: Theme): string {
539
+ return report
540
+ .split("\n")
541
+ .map((line) => {
542
+ let colored = line;
543
+
544
+ if (line === "Context Usage") return theme.bold(theme.fg("accent", line));
545
+ if (line.includes("Estimated usage by category")) colored = colored.replace("Estimated usage by category", theme.bold("Estimated usage by category"));
546
+ if (line.trim().endsWith("· active in current session") || line.trim().startsWith("Skills ·") || line.trim().startsWith("Extension commands ·") || line.trim().startsWith("Extension allocation ·")) {
547
+ colored = theme.fg("accent", colored);
548
+ }
549
+ if (line.includes("Near context limit") || line.includes("Context usage above") || line.includes("Tool results are")) {
550
+ colored = theme.fg("warning", colored);
551
+ }
552
+ if (line.includes("Session Stats")) colored = theme.fg("muted", colored);
553
+ return colored;
554
+ })
555
+ .join("\n");
556
+ }
557
+
558
+ function buildContextReport(breakdown: ContextBreakdown, width = 110): string {
559
+ const maxWidth = Math.max(56, Math.min(width, 110));
560
+ const gridLines = renderGrid(breakdown, true);
561
+ const leftWidth = Math.max(...gridLines.map((line) => visibleWidth(line)));
562
+ const gap = " ";
563
+ const legendCategories = breakdown.categories.filter((category) => category.tokens > 0);
564
+ const rightLines = [
565
+ breakdown.modelLabel,
566
+ breakdown.modelId,
567
+ `${formatTokens(breakdown.totalTokens)}/${formatTokens(breakdown.contextWindow)} tokens (${breakdown.percent?.toFixed(0) ?? "?"}%)`,
568
+ "",
569
+ "Estimated usage by category",
570
+ ...legendCategories.map((category) => formatLegendEntry(category, breakdown, {} as Theme)),
571
+ ];
572
+
573
+ const lines: string[] = ["Context Usage"];
574
+ const pairCount = Math.max(gridLines.length, rightLines.length);
575
+ for (let i = 0; i < pairCount; i++) {
576
+ const left = gridLines[i] ? ` ${padVisible(gridLines[i]!, leftWidth)}` : ` ${" ".repeat(leftWidth)}`;
577
+ const right = rightLines[i] ?? "";
578
+ lines.push(truncateToWidth(`${left}${gap}${right}`, maxWidth));
579
+ }
580
+
581
+ lines.push("");
582
+ lines.push(` Session Stats · Turns ${breakdown.turnCount} · Messages ${breakdown.messageCount} · Cache R ${formatTokens(breakdown.cacheRead)} · Cache W ${formatTokens(breakdown.cacheWrite)} · Cost $${breakdown.totalCost.toFixed(4)}`);
583
+
584
+ if (breakdown.percent !== null && breakdown.percent >= 95) {
585
+ lines.push(" Near context limit — compaction strongly recommended");
586
+ } else if (breakdown.percent !== null && breakdown.percent >= 80) {
587
+ lines.push(" Context usage above 80% — consider /compact");
588
+ }
589
+ pushExtensionAllocations(lines, breakdown.extensionAllocations, breakdown.contextWindow);
590
+ for (const section of breakdown.detailSections) {
591
+ lines.push("");
592
+ lines.push(` ${section.title}`);
593
+ if (section.subtitle) {
594
+ lines.push("");
595
+ lines.push(` ${section.subtitle}`);
596
+ }
597
+ for (const group of section.groups) {
598
+ if (group.title) {
599
+ lines.push("");
600
+ lines.push(` ${group.title}`);
601
+ }
602
+ pushTreeItems(lines, group.items);
603
+ }
604
+ }
605
+
606
+ return lines.map((line) => truncateToWidth(line, maxWidth)).join("\n");
607
+ }
608
+
609
+ export default function (pi: ExtensionAPI) {
610
+ pi.registerMessageRenderer(CUSTOM_TYPE, (message, _options, theme) => {
611
+ const content = typeof message.content === "string" ? message.content : "";
612
+ return new Text(colorizeContextReport(content, theme), 0, 0);
613
+ });
614
+
615
+ pi.on("context", (event) => {
616
+ return {
617
+ messages: event.messages.filter((message) => {
618
+ const maybeCustom = message as { role?: string; customType?: string };
619
+ return !(maybeCustom.role === "custom" && maybeCustom.customType === CUSTOM_TYPE);
620
+ }),
621
+ };
622
+ });
623
+
624
+ pi.registerCommand("context", {
625
+ description: "Print current context-window usage without adding it to LLM context",
626
+ handler: async (_args, ctx) => {
627
+ const breakdown = computeBreakdown(ctx, pi);
628
+ if (!breakdown) {
629
+ ctx.ui.notify("No context usage data available yet. Send a message first.", "warning");
630
+ return;
631
+ }
632
+
633
+ pi.sendMessage({
634
+ customType: CUSTOM_TYPE,
635
+ content: buildContextReport(breakdown),
636
+ display: true,
637
+ details: { excludedFromContext: true },
638
+ });
639
+ },
640
+ });
641
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pi-mono-context",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension that prints current context-window usage without adding the report to future LLM context",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "context"
9
+ ],
10
+ "peerDependencies": {
11
+ "@mariozechner/pi-ai": "*",
12
+ "@mariozechner/pi-coding-agent": "*",
13
+ "@mariozechner/pi-tui": "*"
14
+ },
15
+ "pi": {
16
+ "extensions": [
17
+ "./index.ts"
18
+ ]
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/emanuelcasco/pi-mono-extensions.git",
23
+ "directory": "extensions/context"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/emanuelcasco/pi-mono-extensions/issues"
27
+ },
28
+ "homepage": "https://github.com/emanuelcasco/pi-mono-extensions#readme"
29
+ }