@docyrus/docyrus 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/agent-loader.js +3 -2
- package/agent-loader.js.map +2 -2
- package/main.js +82162 -46093
- package/main.js.map +4 -4
- package/package.json +12 -3
- package/resources/chrome-tools/browser-content.js +46 -46
- package/resources/chrome-tools/browser-cookies.js +16 -16
- package/resources/chrome-tools/browser-eval.js +27 -27
- package/resources/chrome-tools/browser-hn-scraper.js +1 -1
- package/resources/chrome-tools/browser-nav.js +23 -23
- package/resources/chrome-tools/browser-pick.js +127 -127
- package/resources/chrome-tools/browser-screenshot.js +10 -10
- package/resources/chrome-tools/browser-start.js +38 -38
- package/resources/pi-agent/extensions/answer.ts +392 -384
- package/resources/pi-agent/extensions/context.ts +415 -415
- package/resources/pi-agent/extensions/control.ts +1287 -1287
- package/resources/pi-agent/extensions/diff.ts +171 -171
- package/resources/pi-agent/extensions/files.ts +155 -155
- package/resources/pi-agent/extensions/knowledge.ts +664 -0
- package/resources/pi-agent/extensions/loop.ts +375 -375
- package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
- package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
- package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
- package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
- package/resources/pi-agent/extensions/redraws.ts +14 -14
- package/resources/pi-agent/extensions/review.ts +1533 -1533
- package/resources/pi-agent/extensions/todos.ts +1735 -1735
- package/resources/pi-agent/extensions/tps.ts +40 -40
- package/resources/pi-agent/extensions/whimsical.ts +3 -3
- package/resources/pi-agent/prompts/agent-system.md +2 -0
- package/resources/pi-agent/prompts/coder-system.md +2 -0
- package/server-loader.js +82 -1
- package/server-loader.js.map +3 -3
- package/tui.mjs +2 -0
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
await loadFromDir(getAgentDir());
|
|
96
96
|
|
|
97
97
|
// Ancestors: root → cwd (same order as pi)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
theme: any,
|
|
219
|
+
parts: { system: number; tools: number; convo: number; remaining: number },
|
|
220
|
+
total: number,
|
|
221
|
+
width: number,
|
|
222
222
|
): string {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
+
return items.join(", ");
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
function joinCommaStyled(items: string[], renderItem: (item: string) => string, sep: string): string {
|
|
249
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
357
|
-
|
|
356
|
+
lines.push(bar);
|
|
357
|
+
}
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
lines.push("");
|
|
360
360
|
|
|
361
361
|
// System prompt + tools totals (approx)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
370
|
-
|
|
368
|
+
);
|
|
369
|
+
lines.push(
|
|
370
|
+
muted("Tools: ") +
|
|
371
371
|
text(`~${u.toolsTokens.toLocaleString()} tok`) +
|
|
372
372
|
muted(` (${u.activeTools} active)`),
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
397
|
+
this.body.setText(lines.join("\n"));
|
|
398
|
+
this.cachedWidth = width;
|
|
399
|
+
}
|
|
400
400
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
}
|