@docyrus/docyrus 0.0.34 → 0.0.36

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 (66) hide show
  1. package/README.md +25 -0
  2. package/agent-loader.js +3 -2
  3. package/agent-loader.js.map +2 -2
  4. package/main.js +82162 -46093
  5. package/main.js.map +4 -4
  6. package/package.json +12 -3
  7. package/resources/chrome-tools/browser-content.js +46 -46
  8. package/resources/chrome-tools/browser-cookies.js +16 -16
  9. package/resources/chrome-tools/browser-eval.js +27 -27
  10. package/resources/chrome-tools/browser-hn-scraper.js +1 -1
  11. package/resources/chrome-tools/browser-nav.js +23 -23
  12. package/resources/chrome-tools/browser-pick.js +127 -127
  13. package/resources/chrome-tools/browser-screenshot.js +10 -10
  14. package/resources/chrome-tools/browser-start.js +38 -38
  15. package/resources/pi-agent/extensions/answer.ts +392 -384
  16. package/resources/pi-agent/extensions/context.ts +415 -415
  17. package/resources/pi-agent/extensions/control.ts +1287 -1287
  18. package/resources/pi-agent/extensions/diff.ts +171 -171
  19. package/resources/pi-agent/extensions/files.ts +155 -155
  20. package/resources/pi-agent/extensions/knowledge.ts +664 -0
  21. package/resources/pi-agent/extensions/loop.ts +375 -375
  22. package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
  23. package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
  24. package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
  25. package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
  26. package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
  27. package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
  28. package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
  29. package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
  30. package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
  54. package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
  55. package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
  56. package/resources/pi-agent/extensions/redraws.ts +14 -14
  57. package/resources/pi-agent/extensions/review.ts +1533 -1533
  58. package/resources/pi-agent/extensions/todos.ts +1735 -1735
  59. package/resources/pi-agent/extensions/tps.ts +40 -40
  60. package/resources/pi-agent/extensions/whimsical.ts +3 -3
  61. package/resources/pi-agent/prompts/agent-system.md +2 -0
  62. package/resources/pi-agent/prompts/coder-system.md +2 -0
  63. package/server-loader.js +82 -1
  64. package/server-loader.js.map +3 -3
  65. package/tui.mjs +2 -0
  66. package/tui.mjs.map +1 -1
@@ -17,100 +17,100 @@ import fs from "node:fs/promises";
17
17
  import { existsSync } from "node:fs";
18
18
 
19
19
  function formatUsd(cost: number): string {
20
- if (!Number.isFinite(cost) || cost <= 0) return "$0.00";
21
- if (cost >= 1) return `$${cost.toFixed(2)}`;
22
- if (cost >= 0.1) return `$${cost.toFixed(3)}`;
23
- return `$${cost.toFixed(4)}`;
20
+ if (!Number.isFinite(cost) || cost <= 0) {return "$0.00";}
21
+ if (cost >= 1) {return `$${cost.toFixed(2)}`;}
22
+ if (cost >= 0.1) {return `$${cost.toFixed(3)}`;}
23
+ return `$${cost.toFixed(4)}`;
24
24
  }
25
25
 
26
26
  function estimateTokens(text: string): number {
27
27
  // Deliberately fuzzy (good enough for “how big-ish is this”).
28
- return Math.max(0, Math.ceil(text.length / 4));
28
+ return Math.max(0, Math.ceil(text.length / 4));
29
29
  }
30
30
 
31
31
  function normalizeReadPath(inputPath: string, cwd: string): string {
32
32
  // Similar to pi's resolveToCwd/resolveReadPath, but simplified.
33
- let p = inputPath;
34
- if (p.startsWith("@")) p = p.slice(1);
35
- if (p === "~") p = os.homedir();
36
- else if (p.startsWith("~/")) p = path.join(os.homedir(), p.slice(2));
37
- if (!path.isAbsolute(p)) p = path.resolve(cwd, p);
38
- return path.resolve(p);
33
+ let p = inputPath;
34
+ if (p.startsWith("@")) {p = p.slice(1);}
35
+ if (p === "~") {p = os.homedir();}
36
+ else if (p.startsWith("~/")) {p = path.join(os.homedir(), p.slice(2));}
37
+ if (!path.isAbsolute(p)) {p = path.resolve(cwd, p);}
38
+ return path.resolve(p);
39
39
  }
40
40
 
41
41
  function getAgentDir(): string {
42
42
  // Mirrors pi's behavior reasonably well.
43
- const envCandidates = ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"];
44
- let envDir: string | undefined;
45
- for (const k of envCandidates) {
46
- if (process.env[k]) {
47
- envDir = process.env[k];
48
- break;
49
- }
50
- }
51
- if (!envDir) {
52
- for (const [k, v] of Object.entries(process.env)) {
53
- if (k.endsWith("_CODING_AGENT_DIR") && v) {
54
- envDir = v;
55
- break;
56
- }
57
- }
58
- }
59
-
60
- if (envDir) {
61
- if (envDir === "~") return os.homedir();
62
- if (envDir.startsWith("~/")) return path.join(os.homedir(), envDir.slice(2));
63
- return envDir;
64
- }
65
- return path.join(os.homedir(), ".pi", "agent");
43
+ const envCandidates = ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"];
44
+ let envDir: string | undefined;
45
+ for (const k of envCandidates) {
46
+ if (process.env[k]) {
47
+ envDir = process.env[k];
48
+ break;
49
+ }
50
+ }
51
+ if (!envDir) {
52
+ for (const [k, v] of Object.entries(process.env)) {
53
+ if (k.endsWith("_CODING_AGENT_DIR") && v) {
54
+ envDir = v;
55
+ break;
56
+ }
57
+ }
58
+ }
59
+
60
+ if (envDir) {
61
+ if (envDir === "~") {return os.homedir();}
62
+ if (envDir.startsWith("~/")) {return path.join(os.homedir(), envDir.slice(2));}
63
+ return envDir;
64
+ }
65
+ return path.join(os.homedir(), ".pi", "agent");
66
66
  }
67
67
 
68
68
  async function readFileIfExists(filePath: string): Promise<{ path: string; content: string; bytes: number } | null> {
69
- if (!existsSync(filePath)) return null;
70
- try {
71
- const buf = await fs.readFile(filePath);
72
- return { path: filePath, content: buf.toString("utf8"), bytes: buf.byteLength };
73
- } catch {
74
- return null;
75
- }
69
+ if (!existsSync(filePath)) {return null;}
70
+ try {
71
+ const buf = await fs.readFile(filePath);
72
+ return { path: filePath, content: buf.toString("utf8"), bytes: buf.byteLength };
73
+ } catch {
74
+ return null;
75
+ }
76
76
  }
77
77
 
78
78
  async function loadProjectContextFiles(cwd: string): Promise<Array<{ path: string; tokens: number; bytes: number }>> {
79
- const out: Array<{ path: string; tokens: number; bytes: number }> = [];
80
- const seen = new Set<string>();
81
-
82
- const loadFromDir = async (dir: string) => {
83
- for (const name of ["AGENTS.md", "CLAUDE.md"]) {
84
- const p = path.join(dir, name);
85
- const f = await readFileIfExists(p);
86
- if (f && !seen.has(f.path)) {
87
- seen.add(f.path);
88
- out.push({ path: f.path, tokens: estimateTokens(f.content), bytes: f.bytes });
79
+ const out: Array<{ path: string; tokens: number; bytes: number }> = [];
80
+ const seen = new Set<string>();
81
+
82
+ const loadFromDir = async(dir: string) => {
83
+ for (const name of ["AGENTS.md", "CLAUDE.md"]) {
84
+ const p = path.join(dir, name);
85
+ const f = await readFileIfExists(p);
86
+ if (f && !seen.has(f.path)) {
87
+ seen.add(f.path);
88
+ out.push({ path: f.path, tokens: estimateTokens(f.content), bytes: f.bytes });
89
89
  // pi loads at most one of those per dir
90
- return;
91
- }
92
- }
93
- };
90
+ return;
91
+ }
92
+ }
93
+ };
94
94
 
95
- await loadFromDir(getAgentDir());
95
+ await loadFromDir(getAgentDir());
96
96
 
97
97
  // Ancestors: root → cwd (same order as pi)
98
- const stack: string[] = [];
99
- let current = path.resolve(cwd);
100
- while (true) {
101
- stack.push(current);
102
- const parent = path.resolve(current, "..");
103
- if (parent === current) break;
104
- current = parent;
105
- }
106
- stack.reverse();
107
- for (const dir of stack) await loadFromDir(dir);
108
-
109
- return out;
98
+ const stack: string[] = [];
99
+ let current = path.resolve(cwd);
100
+ while (true) {
101
+ stack.push(current);
102
+ const parent = path.resolve(current, "..");
103
+ if (parent === current) {break;}
104
+ current = parent;
105
+ }
106
+ stack.reverse();
107
+ for (const dir of stack) {await loadFromDir(dir);}
108
+
109
+ return out;
110
110
  }
111
111
 
112
112
  function normalizeSkillName(name: string): string {
113
- return name.startsWith("skill:") ? name.slice("skill:".length) : name;
113
+ return name.startsWith("skill:") ? name.slice("skill:".length) : name;
114
114
  }
115
115
 
116
116
  type SkillIndexEntry = {
@@ -120,18 +120,18 @@ type SkillIndexEntry = {
120
120
  };
121
121
 
122
122
  function buildSkillIndex(pi: ExtensionAPI, cwd: string): SkillIndexEntry[] {
123
- return pi
124
- .getCommands()
125
- .filter((c) => c.source === "skill")
126
- .map((c) => {
127
- const p = c.path ? normalizeReadPath(c.path, cwd) : "";
128
- return {
129
- name: normalizeSkillName(c.name),
130
- skillFilePath: p,
131
- skillDir: p ? path.dirname(p) : "",
132
- };
133
- })
134
- .filter((x) => x.name && x.skillDir);
123
+ return pi
124
+ .getCommands()
125
+ .filter((c) => c.source === "skill")
126
+ .map((c) => {
127
+ const p = c.path ? normalizeReadPath(c.path, cwd) : "";
128
+ return {
129
+ name: normalizeSkillName(c.name),
130
+ skillFilePath: p,
131
+ skillDir: p ? path.dirname(p) : "",
132
+ };
133
+ })
134
+ .filter((x) => x.name && x.skillDir);
135
135
  }
136
136
 
137
137
  const SKILL_LOADED_ENTRY = "context:skill_loaded";
@@ -142,31 +142,31 @@ type SkillLoadedEntryData = {
142
142
  };
143
143
 
144
144
  function getLoadedSkillsFromSession(ctx: ExtensionContext): Set<string> {
145
- const out = new Set<string>();
146
- for (const e of ctx.sessionManager.getEntries()) {
147
- if ((e as any)?.type !== "custom") continue;
148
- if ((e as any)?.customType !== SKILL_LOADED_ENTRY) continue;
149
- const data = (e as any)?.data as SkillLoadedEntryData | undefined;
150
- if (data?.name) out.add(data.name);
151
- }
152
- return out;
145
+ const out = new Set<string>();
146
+ for (const e of ctx.sessionManager.getEntries()) {
147
+ if ((e as any)?.type !== "custom") {continue;}
148
+ if ((e as any)?.customType !== SKILL_LOADED_ENTRY) {continue;}
149
+ const data = (e as any)?.data as SkillLoadedEntryData | undefined;
150
+ if (data?.name) {out.add(data.name);}
151
+ }
152
+ return out;
153
153
  }
154
154
 
155
155
  function extractCostTotal(usage: any): number {
156
- if (!usage) return 0;
157
- const c = usage?.cost;
158
- if (typeof c === "number") return Number.isFinite(c) ? c : 0;
159
- if (typeof c === "string") {
160
- const n = Number(c);
161
- return Number.isFinite(n) ? n : 0;
162
- }
163
- const t = c?.total;
164
- if (typeof t === "number") return Number.isFinite(t) ? t : 0;
165
- if (typeof t === "string") {
166
- const n = Number(t);
167
- return Number.isFinite(n) ? n : 0;
168
- }
169
- return 0;
156
+ if (!usage) {return 0;}
157
+ const c = usage?.cost;
158
+ if (typeof c === "number") {return Number.isFinite(c) ? c : 0;}
159
+ if (typeof c === "string") {
160
+ const n = Number(c);
161
+ return Number.isFinite(n) ? n : 0;
162
+ }
163
+ const t = c?.total;
164
+ if (typeof t === "number") {return Number.isFinite(t) ? t : 0;}
165
+ if (typeof t === "string") {
166
+ const n = Number(t);
167
+ return Number.isFinite(n) ? n : 0;
168
+ }
169
+ return 0;
170
170
  }
171
171
 
172
172
  function sumSessionUsage(ctx: ExtensionCommandContext): {
@@ -177,76 +177,76 @@ function sumSessionUsage(ctx: ExtensionCommandContext): {
177
177
  totalTokens: number;
178
178
  totalCost: number;
179
179
  } {
180
- let input = 0;
181
- let output = 0;
182
- let cacheRead = 0;
183
- let cacheWrite = 0;
184
- let totalCost = 0;
185
-
186
- for (const entry of ctx.sessionManager.getEntries()) {
187
- if ((entry as any)?.type !== "message") continue;
188
- const msg = (entry as any)?.message;
189
- if (!msg || msg.role !== "assistant") continue;
190
- const usage = msg.usage;
191
- if (!usage) continue;
192
- input += Number(usage.inputTokens ?? 0) || 0;
193
- output += Number(usage.outputTokens ?? 0) || 0;
194
- cacheRead += Number(usage.cacheRead ?? 0) || 0;
195
- cacheWrite += Number(usage.cacheWrite ?? 0) || 0;
196
- totalCost += extractCostTotal(usage);
197
- }
198
-
199
- return {
200
- input,
201
- output,
202
- cacheRead,
203
- cacheWrite,
204
- totalTokens: input + output + cacheRead + cacheWrite,
205
- totalCost,
206
- };
180
+ let input = 0;
181
+ let output = 0;
182
+ let cacheRead = 0;
183
+ let cacheWrite = 0;
184
+ let totalCost = 0;
185
+
186
+ for (const entry of ctx.sessionManager.getEntries()) {
187
+ if ((entry as any)?.type !== "message") {continue;}
188
+ const msg = (entry as any)?.message;
189
+ if (!msg || msg.role !== "assistant") {continue;}
190
+ const usage = msg.usage;
191
+ if (!usage) {continue;}
192
+ input += Number(usage.inputTokens ?? 0) || 0;
193
+ output += Number(usage.outputTokens ?? 0) || 0;
194
+ cacheRead += Number(usage.cacheRead ?? 0) || 0;
195
+ cacheWrite += Number(usage.cacheWrite ?? 0) || 0;
196
+ totalCost += extractCostTotal(usage);
197
+ }
198
+
199
+ return {
200
+ input,
201
+ output,
202
+ cacheRead,
203
+ cacheWrite,
204
+ totalTokens: input + output + cacheRead + cacheWrite,
205
+ totalCost,
206
+ };
207
207
  }
208
208
 
209
209
  function shortenPath(p: string, cwd: string): string {
210
- const rp = path.resolve(p);
211
- const rc = path.resolve(cwd);
212
- if (rp === rc) return ".";
213
- if (rp.startsWith(rc + path.sep)) return "./" + rp.slice(rc.length + 1);
214
- return rp;
210
+ const rp = path.resolve(p);
211
+ const rc = path.resolve(cwd);
212
+ if (rp === rc) {return ".";}
213
+ if (rp.startsWith(rc + path.sep)) {return "./" + rp.slice(rc.length + 1);}
214
+ return rp;
215
215
  }
216
216
 
217
217
  function renderUsageBar(
218
- theme: any,
219
- parts: { system: number; tools: number; convo: number; remaining: number },
220
- total: number,
221
- width: number,
218
+ theme: any,
219
+ parts: { system: number; tools: number; convo: number; remaining: number },
220
+ total: number,
221
+ width: number,
222
222
  ): string {
223
- const w = Math.max(10, width);
224
- if (total <= 0) return "";
225
-
226
- const toCols = (n: number) => Math.round((n / total) * w);
227
- let sys = toCols(parts.system);
228
- let tools = toCols(parts.tools);
229
- let con = toCols(parts.convo);
230
- let rem = w - sys - tools - con;
231
- if (rem < 0) rem = 0;
223
+ const w = Math.max(10, width);
224
+ if (total <= 0) {return "";}
225
+
226
+ const toCols = (n: number) => Math.round((n / total) * w);
227
+ let sys = toCols(parts.system);
228
+ let tools = toCols(parts.tools);
229
+ let con = toCols(parts.convo);
230
+ let rem = w - sys - tools - con;
231
+ if (rem < 0) {rem = 0;}
232
232
  // adjust rounding drift
233
- while (sys + tools + con + rem < w) rem++;
234
- while (sys + tools + con + rem > w && rem > 0) rem--;
235
-
236
- const block = "█";
237
- const sysStr = theme.fg("accent", block.repeat(sys));
238
- const toolsStr = theme.fg("warning", block.repeat(tools));
239
- const conStr = theme.fg("success", block.repeat(con));
240
- const remStr = theme.fg("dim", block.repeat(rem));
241
- return `${sysStr}${toolsStr}${conStr}${remStr}`;
233
+ while (sys + tools + con + rem < w) {rem++;}
234
+ while (sys + tools + con + rem > w && rem > 0) {rem--;}
235
+
236
+ const block = "█";
237
+ const sysStr = theme.fg("accent", block.repeat(sys));
238
+ const toolsStr = theme.fg("warning", block.repeat(tools));
239
+ const conStr = theme.fg("success", block.repeat(con));
240
+ const remStr = theme.fg("dim", block.repeat(rem));
241
+ return `${sysStr}${toolsStr}${conStr}${remStr}`;
242
242
  }
243
243
 
244
244
  function joinComma(items: string[]): string {
245
- return items.join(", ");
245
+ return items.join(", ");
246
246
  }
247
247
 
248
248
  function joinCommaStyled(items: string[], renderItem: (item: string) => string, sep: string): string {
249
- return items.map(renderItem).join(sep);
249
+ return items.map(renderItem).join(sep);
250
250
  }
251
251
 
252
252
  type ContextViewData = {
@@ -273,73 +273,73 @@ type ContextViewData = {
273
273
  };
274
274
 
275
275
  class ContextView implements Component {
276
- private tui: TUI;
277
- private theme: any;
278
- private onDone: () => void;
279
- private data: ContextViewData;
280
- private container: Container;
281
- private body: Text;
282
- private cachedWidth?: number;
283
-
284
- constructor(tui: TUI, theme: any, data: ContextViewData, onDone: () => void) {
285
- this.tui = tui;
286
- this.theme = theme;
287
- this.data = data;
288
- this.onDone = onDone;
289
-
290
- this.container = new Container();
291
- this.container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
292
- this.container.addChild(
293
- new Text(
294
- theme.fg("accent", theme.bold("Context")) + theme.fg("dim", " (Esc/q/Enter to close)"),
295
- 1,
296
- 0,
297
- ),
298
- );
299
- this.container.addChild(new Text("", 1, 0));
300
-
301
- this.body = new Text("", 1, 0);
302
- this.container.addChild(this.body);
303
-
304
- this.container.addChild(new Text("", 1, 0));
305
- this.container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
306
- }
307
-
308
- private rebuild(width: number): void {
309
- const muted = (s: string) => this.theme.fg("muted", s);
310
- const dim = (s: string) => this.theme.fg("dim", s);
311
- const text = (s: string) => this.theme.fg("text", s);
312
-
313
- const lines: string[] = [];
276
+ private tui: TUI;
277
+ private theme: any;
278
+ private onDone: () => void;
279
+ private data: ContextViewData;
280
+ private container: Container;
281
+ private body: Text;
282
+ private cachedWidth?: number;
283
+
284
+ constructor(tui: TUI, theme: any, data: ContextViewData, onDone: () => void) {
285
+ this.tui = tui;
286
+ this.theme = theme;
287
+ this.data = data;
288
+ this.onDone = onDone;
289
+
290
+ this.container = new Container();
291
+ this.container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
292
+ this.container.addChild(
293
+ new Text(
294
+ theme.fg("accent", theme.bold("Context")) + theme.fg("dim", " (Esc/q/Enter to close)"),
295
+ 1,
296
+ 0,
297
+ ),
298
+ );
299
+ this.container.addChild(new Text("", 1, 0));
300
+
301
+ this.body = new Text("", 1, 0);
302
+ this.container.addChild(this.body);
303
+
304
+ this.container.addChild(new Text("", 1, 0));
305
+ this.container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
306
+ }
307
+
308
+ private rebuild(width: number): void {
309
+ const muted = (s: string) => this.theme.fg("muted", s);
310
+ const dim = (s: string) => this.theme.fg("dim", s);
311
+ const text = (s: string) => this.theme.fg("text", s);
312
+
313
+ const lines: string[] = [];
314
314
 
315
315
  // Window + bar
316
- if (!this.data.usage) {
317
- lines.push(muted("Window: ") + dim("(unknown)"));
318
- } else {
319
- const u = this.data.usage;
320
- lines.push(
321
- muted("Window: ") +
316
+ if (!this.data.usage) {
317
+ lines.push(muted("Window: ") + dim("(unknown)"));
318
+ } else {
319
+ const u = this.data.usage;
320
+ lines.push(
321
+ muted("Window: ") +
322
322
  text(`~${u.effectiveTokens.toLocaleString()} / ${u.contextWindow.toLocaleString()}`) +
323
323
  muted(` (${u.percent.toFixed(1)}% used, ~${u.remainingTokens.toLocaleString()} left)`),
324
- );
324
+ );
325
325
 
326
326
  // bar width tries to fit within the viewport
327
- const barWidth = Math.max(10, Math.min(36, width - 10));
327
+ const barWidth = Math.max(10, Math.min(36, width - 10));
328
328
 
329
329
  // Prorate system prompt into current message context estimate, then add tools estimate.
330
- const sysInMessages = Math.min(u.systemPromptTokens, u.messageTokens);
331
- const convoInMessages = Math.max(0, u.messageTokens - sysInMessages);
332
- const bar =
330
+ const sysInMessages = Math.min(u.systemPromptTokens, u.messageTokens);
331
+ const convoInMessages = Math.max(0, u.messageTokens - sysInMessages);
332
+ const bar =
333
333
  renderUsageBar(
334
- this.theme,
335
- {
336
- system: sysInMessages,
337
- tools: u.toolsTokens,
338
- convo: convoInMessages,
339
- remaining: u.remainingTokens,
340
- },
341
- u.contextWindow,
342
- barWidth,
334
+ this.theme,
335
+ {
336
+ system: sysInMessages,
337
+ tools: u.toolsTokens,
338
+ convo: convoInMessages,
339
+ remaining: u.remainingTokens,
340
+ },
341
+ u.contextWindow,
342
+ barWidth,
343
343
  ) +
344
344
  " " +
345
345
  dim("sys") +
@@ -353,226 +353,226 @@ class ContextView implements Component {
353
353
  " " +
354
354
  dim("free") +
355
355
  this.theme.fg("dim", "█");
356
- lines.push(bar);
357
- }
356
+ lines.push(bar);
357
+ }
358
358
 
359
- lines.push("");
359
+ lines.push("");
360
360
 
361
361
  // System prompt + tools totals (approx)
362
- if (this.data.usage) {
363
- const u = this.data.usage;
364
- lines.push(
365
- muted("System: ") +
362
+ if (this.data.usage) {
363
+ const u = this.data.usage;
364
+ lines.push(
365
+ muted("System: ") +
366
366
  text(`~${u.systemPromptTokens.toLocaleString()} tok`) +
367
367
  muted(` (AGENTS ~${u.agentTokens.toLocaleString()})`),
368
- );
369
- lines.push(
370
- muted("Tools: ") +
368
+ );
369
+ lines.push(
370
+ muted("Tools: ") +
371
371
  text(`~${u.toolsTokens.toLocaleString()} tok`) +
372
372
  muted(` (${u.activeTools} active)`),
373
- );
374
- }
375
-
376
- lines.push(muted(`AGENTS (${this.data.agentFiles.length}): `) + text(this.data.agentFiles.length ? joinComma(this.data.agentFiles) : "(none)"));
377
- lines.push("");
378
- lines.push(muted(`Extensions (${this.data.extensions.length}): `) + text(this.data.extensions.length ? joinComma(this.data.extensions) : "(none)"));
379
-
380
- const loaded = new Set(this.data.loadedSkills);
381
- const skillsRendered = this.data.skills.length
382
- ? joinCommaStyled(
383
- this.data.skills,
384
- (name) => (loaded.has(name) ? this.theme.fg("success", name) : this.theme.fg("muted", name)),
385
- this.theme.fg("muted", ", "),
386
- )
387
- : "(none)";
388
- lines.push(muted(`Skills (${this.data.skills.length}): `) + skillsRendered);
389
- lines.push("");
390
- lines.push(
391
- muted("Session: ") +
373
+ );
374
+ }
375
+
376
+ lines.push(muted(`AGENTS (${this.data.agentFiles.length}): `) + text(this.data.agentFiles.length ? joinComma(this.data.agentFiles) : "(none)"));
377
+ lines.push("");
378
+ lines.push(muted(`Extensions (${this.data.extensions.length}): `) + text(this.data.extensions.length ? joinComma(this.data.extensions) : "(none)"));
379
+
380
+ const loaded = new Set(this.data.loadedSkills);
381
+ const skillsRendered = this.data.skills.length
382
+ ? joinCommaStyled(
383
+ this.data.skills,
384
+ (name) => (loaded.has(name) ? this.theme.fg("success", name) : this.theme.fg("muted", name)),
385
+ this.theme.fg("muted", ", "),
386
+ )
387
+ : "(none)";
388
+ lines.push(muted(`Skills (${this.data.skills.length}): `) + skillsRendered);
389
+ lines.push("");
390
+ lines.push(
391
+ muted("Session: ") +
392
392
  text(`${this.data.session.totalTokens.toLocaleString()} tokens`) +
393
393
  muted(" · ") +
394
394
  text(formatUsd(this.data.session.totalCost)),
395
- );
395
+ );
396
396
 
397
- this.body.setText(lines.join("\n"));
398
- this.cachedWidth = width;
399
- }
397
+ this.body.setText(lines.join("\n"));
398
+ this.cachedWidth = width;
399
+ }
400
400
 
401
- handleInput(data: string): void {
402
- if (
403
- matchesKey(data, Key.escape) ||
401
+ handleInput(data: string): void {
402
+ if (
403
+ matchesKey(data, Key.escape) ||
404
404
  matchesKey(data, Key.ctrl("c")) ||
405
405
  data.toLowerCase() === "q" ||
406
406
  data === "\r"
407
- ) {
408
- this.onDone();
409
- return;
410
- }
411
- }
412
-
413
- invalidate(): void {
414
- this.container.invalidate();
415
- this.cachedWidth = undefined;
416
- }
417
-
418
- render(width: number): string[] {
419
- if (this.cachedWidth !== width) this.rebuild(width);
420
- return this.container.render(width);
421
- }
407
+ ) {
408
+ this.onDone();
409
+ return;
410
+ }
411
+ }
412
+
413
+ invalidate(): void {
414
+ this.container.invalidate();
415
+ this.cachedWidth = undefined;
416
+ }
417
+
418
+ render(width: number): string[] {
419
+ if (this.cachedWidth !== width) {this.rebuild(width);}
420
+ return this.container.render(width);
421
+ }
422
422
  }
423
423
 
424
424
  export default function contextExtension(pi: ExtensionAPI) {
425
425
  // Track which skills were actually pulled in via read tool calls.
426
- let lastSessionId: string | null = null;
427
- let cachedLoadedSkills = new Set<string>();
428
- let cachedSkillIndex: SkillIndexEntry[] = [];
429
-
430
- const ensureCaches = (ctx: ExtensionContext) => {
431
- const sid = ctx.sessionManager.getSessionId();
432
- if (sid !== lastSessionId) {
433
- lastSessionId = sid;
434
- cachedLoadedSkills = getLoadedSkillsFromSession(ctx);
435
- cachedSkillIndex = buildSkillIndex(pi, ctx.cwd);
436
- }
437
- if (cachedSkillIndex.length === 0) {
438
- cachedSkillIndex = buildSkillIndex(pi, ctx.cwd);
439
- }
440
- };
441
-
442
- const matchSkillForPath = (absPath: string): string | null => {
443
- let best: SkillIndexEntry | null = null;
444
- for (const s of cachedSkillIndex) {
445
- if (!s.skillDir) continue;
446
- if (absPath === s.skillFilePath || absPath.startsWith(s.skillDir + path.sep)) {
447
- if (!best || s.skillDir.length > best.skillDir.length) best = s;
448
- }
449
- }
450
- return best?.name ?? null;
451
- };
452
-
453
- pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
426
+ let lastSessionId: string | null = null;
427
+ let cachedLoadedSkills = new Set<string>();
428
+ let cachedSkillIndex: SkillIndexEntry[] = [];
429
+
430
+ const ensureCaches = (ctx: ExtensionContext) => {
431
+ const sid = ctx.sessionManager.getSessionId();
432
+ if (sid !== lastSessionId) {
433
+ lastSessionId = sid;
434
+ cachedLoadedSkills = getLoadedSkillsFromSession(ctx);
435
+ cachedSkillIndex = buildSkillIndex(pi, ctx.cwd);
436
+ }
437
+ if (cachedSkillIndex.length === 0) {
438
+ cachedSkillIndex = buildSkillIndex(pi, ctx.cwd);
439
+ }
440
+ };
441
+
442
+ const matchSkillForPath = (absPath: string): string | null => {
443
+ let best: SkillIndexEntry | null = null;
444
+ for (const s of cachedSkillIndex) {
445
+ if (!s.skillDir) {continue;}
446
+ if (absPath === s.skillFilePath || absPath.startsWith(s.skillDir + path.sep)) {
447
+ if (!best || s.skillDir.length > best.skillDir.length) {best = s;}
448
+ }
449
+ }
450
+ return best?.name ?? null;
451
+ };
452
+
453
+ pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
454
454
  // Only count successful reads.
455
- if ((event as any).toolName !== "read") return;
456
- if ((event as any).isError) return;
457
-
458
- const input = (event as any).input as { path?: unknown } | undefined;
459
- const p = typeof input?.path === "string" ? input.path : "";
460
- if (!p) return;
461
-
462
- ensureCaches(ctx);
463
- const abs = normalizeReadPath(p, ctx.cwd);
464
- const skillName = matchSkillForPath(abs);
465
- if (!skillName) return;
466
-
467
- if (!cachedLoadedSkills.has(skillName)) {
468
- cachedLoadedSkills.add(skillName);
469
- pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
470
- }
471
- });
472
-
473
- pi.registerCommand("context", {
474
- description: "Show loaded context overview",
475
- handler: async (_args, ctx: ExtensionCommandContext) => {
476
- const commands = pi.getCommands();
477
- const extensionCmds = commands.filter((c) => c.source === "extension");
478
- const skillCmds = commands.filter((c) => c.source === "skill");
479
-
480
- const extensionsByPath = new Map<string, string[]>();
481
- for (const c of extensionCmds) {
482
- const p = c.path ?? "<unknown>";
483
- const arr = extensionsByPath.get(p) ?? [];
484
- arr.push(c.name);
485
- extensionsByPath.set(p, arr);
486
- }
487
- const extensionFiles = [...extensionsByPath.keys()]
488
- .map((p) => (p === "<unknown>" ? p : path.basename(p)))
489
- .sort((a, b) => a.localeCompare(b));
490
-
491
- const skills = skillCmds
492
- .map((c) => normalizeSkillName(c.name))
493
- .sort((a, b) => a.localeCompare(b));
494
-
495
- const agentFiles = await loadProjectContextFiles(ctx.cwd);
496
- const agentFilePaths = agentFiles.map((f) => shortenPath(f.path, ctx.cwd));
497
- const agentTokens = agentFiles.reduce((a, f) => a + f.tokens, 0);
498
-
499
- const systemPrompt = ctx.getSystemPrompt();
500
- const systemPromptTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
501
-
502
- const usage = ctx.getContextUsage();
503
- const messageTokens = usage?.tokens ?? 0;
504
- const ctxWindow = usage?.contextWindow ?? 0;
455
+ if ((event as any).toolName !== "read") {return;}
456
+ if ((event as any).isError) {return;}
457
+
458
+ const input = (event as any).input as { path?: unknown } | undefined;
459
+ const p = typeof input?.path === "string" ? input.path : "";
460
+ if (!p) {return;}
461
+
462
+ ensureCaches(ctx);
463
+ const abs = normalizeReadPath(p, ctx.cwd);
464
+ const skillName = matchSkillForPath(abs);
465
+ if (!skillName) {return;}
466
+
467
+ if (!cachedLoadedSkills.has(skillName)) {
468
+ cachedLoadedSkills.add(skillName);
469
+ pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
470
+ }
471
+ });
472
+
473
+ pi.registerCommand("context", {
474
+ description: "Show loaded context overview",
475
+ handler: async(_args, ctx: ExtensionCommandContext) => {
476
+ const commands = pi.getCommands();
477
+ const extensionCmds = commands.filter((c) => c.source === "extension");
478
+ const skillCmds = commands.filter((c) => c.source === "skill");
479
+
480
+ const extensionsByPath = new Map<string, string[]>();
481
+ for (const c of extensionCmds) {
482
+ const p = c.path ?? "<unknown>";
483
+ const arr = extensionsByPath.get(p) ?? [];
484
+ arr.push(c.name);
485
+ extensionsByPath.set(p, arr);
486
+ }
487
+ const extensionFiles = [...extensionsByPath.keys()]
488
+ .map((p) => (p === "<unknown>" ? p : path.basename(p)))
489
+ .sort((a, b) => a.localeCompare(b));
490
+
491
+ const skills = skillCmds
492
+ .map((c) => normalizeSkillName(c.name))
493
+ .sort((a, b) => a.localeCompare(b));
494
+
495
+ const agentFiles = await loadProjectContextFiles(ctx.cwd);
496
+ const agentFilePaths = agentFiles.map((f) => shortenPath(f.path, ctx.cwd));
497
+ const agentTokens = agentFiles.reduce((a, f) => a + f.tokens, 0);
498
+
499
+ const systemPrompt = ctx.getSystemPrompt();
500
+ const systemPromptTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
501
+
502
+ const usage = ctx.getContextUsage();
503
+ const messageTokens = usage?.tokens ?? 0;
504
+ const ctxWindow = usage?.contextWindow ?? 0;
505
505
 
506
506
  // Tool definitions are not part of ctx.getContextUsage() (it estimates message tokens).
507
507
  // We approximate their token impact from tool name + description, and apply a fudge
508
508
  // factor to account for parameters/schema/formatting.
509
- const TOOL_FUDGE = 1.5;
510
- const activeToolNames = pi.getActiveTools();
511
- const toolInfoByName = new Map(pi.getAllTools().map((t) => [t.name, t] as const));
512
- let toolsTokens = 0;
513
- for (const name of activeToolNames) {
514
- const info = toolInfoByName.get(name);
515
- const blob = `${name}\n${info?.description ?? ""}`;
516
- toolsTokens += estimateTokens(blob);
517
- }
518
- toolsTokens = Math.round(toolsTokens * TOOL_FUDGE);
519
-
520
- const effectiveTokens = messageTokens + toolsTokens;
521
- const percent = ctxWindow > 0 ? (effectiveTokens / ctxWindow) * 100 : 0;
522
- const remainingTokens = ctxWindow > 0 ? Math.max(0, ctxWindow - effectiveTokens) : 0;
523
-
524
- const sessionUsage = sumSessionUsage(ctx);
525
-
526
- const makePlainText = () => {
527
- const lines: string[] = [];
528
- lines.push("Context");
529
- if (usage) {
530
- lines.push(
531
- `Window: ~${effectiveTokens.toLocaleString()} / ${ctxWindow.toLocaleString()} (${percent.toFixed(1)}% used, ~${remainingTokens.toLocaleString()} left)`,
532
- );
533
- } else {
534
- lines.push("Window: (unknown)");
535
- }
536
- lines.push(`System: ~${systemPromptTokens.toLocaleString()} tok (AGENTS ~${agentTokens.toLocaleString()})`);
537
- lines.push(`Tools: ~${toolsTokens.toLocaleString()} tok (${activeToolNames.length} active)`);
538
- lines.push(`AGENTS: ${agentFilePaths.length ? joinComma(agentFilePaths) : "(none)"}`);
539
- lines.push(`Extensions (${extensionFiles.length}): ${extensionFiles.length ? joinComma(extensionFiles) : "(none)"}`);
540
- lines.push(`Skills (${skills.length}): ${skills.length ? joinComma(skills) : "(none)"}`);
541
- lines.push(`Session: ${sessionUsage.totalTokens.toLocaleString()} tokens · ${formatUsd(sessionUsage.totalCost)}`);
542
- return lines.join("\n");
543
- };
544
-
545
- if (!ctx.hasUI) {
546
- pi.sendMessage({ customType: "context", content: makePlainText(), display: true }, { triggerTurn: false });
547
- return;
548
- }
549
-
550
- const loadedSkills = Array.from(getLoadedSkillsFromSession(ctx)).sort((a, b) => a.localeCompare(b));
551
-
552
- const viewData: ContextViewData = {
553
- usage: usage
554
- ? {
555
- messageTokens,
556
- contextWindow: ctxWindow,
557
- effectiveTokens,
558
- percent,
559
- remainingTokens,
560
- systemPromptTokens,
561
- agentTokens,
562
- toolsTokens,
563
- activeTools: activeToolNames.length,
564
- }
565
- : null,
566
- agentFiles: agentFilePaths,
567
- extensions: extensionFiles,
568
- skills,
569
- loadedSkills,
570
- session: { totalTokens: sessionUsage.totalTokens, totalCost: sessionUsage.totalCost },
571
- };
572
-
573
- await ctx.ui.custom<void>((tui, theme, _kb, done) => {
574
- return new ContextView(tui, theme, viewData, done);
575
- });
576
- },
577
- });
509
+ const TOOL_FUDGE = 1.5;
510
+ const activeToolNames = pi.getActiveTools();
511
+ const toolInfoByName = new Map(pi.getAllTools().map((t) => [t.name, t] as const));
512
+ let toolsTokens = 0;
513
+ for (const name of activeToolNames) {
514
+ const info = toolInfoByName.get(name);
515
+ const blob = `${name}\n${info?.description ?? ""}`;
516
+ toolsTokens += estimateTokens(blob);
517
+ }
518
+ toolsTokens = Math.round(toolsTokens * TOOL_FUDGE);
519
+
520
+ const effectiveTokens = messageTokens + toolsTokens;
521
+ const percent = ctxWindow > 0 ? (effectiveTokens / ctxWindow) * 100 : 0;
522
+ const remainingTokens = ctxWindow > 0 ? Math.max(0, ctxWindow - effectiveTokens) : 0;
523
+
524
+ const sessionUsage = sumSessionUsage(ctx);
525
+
526
+ const makePlainText = () => {
527
+ const lines: string[] = [];
528
+ lines.push("Context");
529
+ if (usage) {
530
+ lines.push(
531
+ `Window: ~${effectiveTokens.toLocaleString()} / ${ctxWindow.toLocaleString()} (${percent.toFixed(1)}% used, ~${remainingTokens.toLocaleString()} left)`,
532
+ );
533
+ } else {
534
+ lines.push("Window: (unknown)");
535
+ }
536
+ lines.push(`System: ~${systemPromptTokens.toLocaleString()} tok (AGENTS ~${agentTokens.toLocaleString()})`);
537
+ lines.push(`Tools: ~${toolsTokens.toLocaleString()} tok (${activeToolNames.length} active)`);
538
+ lines.push(`AGENTS: ${agentFilePaths.length ? joinComma(agentFilePaths) : "(none)"}`);
539
+ lines.push(`Extensions (${extensionFiles.length}): ${extensionFiles.length ? joinComma(extensionFiles) : "(none)"}`);
540
+ lines.push(`Skills (${skills.length}): ${skills.length ? joinComma(skills) : "(none)"}`);
541
+ lines.push(`Session: ${sessionUsage.totalTokens.toLocaleString()} tokens · ${formatUsd(sessionUsage.totalCost)}`);
542
+ return lines.join("\n");
543
+ };
544
+
545
+ if (!ctx.hasUI) {
546
+ pi.sendMessage({ customType: "context", content: makePlainText(), display: true }, { triggerTurn: false });
547
+ return;
548
+ }
549
+
550
+ const loadedSkills = Array.from(getLoadedSkillsFromSession(ctx)).sort((a, b) => a.localeCompare(b));
551
+
552
+ const viewData: ContextViewData = {
553
+ usage: usage
554
+ ? {
555
+ messageTokens,
556
+ contextWindow: ctxWindow,
557
+ effectiveTokens,
558
+ percent,
559
+ remainingTokens,
560
+ systemPromptTokens,
561
+ agentTokens,
562
+ toolsTokens,
563
+ activeTools: activeToolNames.length,
564
+ }
565
+ : null,
566
+ agentFiles: agentFilePaths,
567
+ extensions: extensionFiles,
568
+ skills,
569
+ loadedSkills,
570
+ session: { totalTokens: sessionUsage.totalTokens, totalCost: sessionUsage.totalCost },
571
+ };
572
+
573
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
574
+ return new ContextView(tui, theme, viewData, done);
575
+ });
576
+ },
577
+ });
578
578
  }