@docyrus/docyrus 0.0.19 → 0.0.21
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/agent-loader.js +37 -3
- package/agent-loader.js.map +2 -2
- package/main.js +498 -93
- package/main.js.map +4 -4
- package/package.json +14 -4
- package/resources/chrome-tools/browser-content.js +103 -0
- package/resources/chrome-tools/browser-cookies.js +35 -0
- package/resources/chrome-tools/browser-eval.js +53 -0
- package/resources/chrome-tools/browser-hn-scraper.js +108 -0
- package/resources/chrome-tools/browser-nav.js +44 -0
- package/resources/chrome-tools/browser-pick.js +162 -0
- package/resources/chrome-tools/browser-screenshot.js +34 -0
- package/resources/chrome-tools/browser-start.js +86 -0
- package/resources/pi-agent/extensions/answer.ts +532 -0
- package/resources/pi-agent/extensions/context.ts +578 -0
- package/resources/pi-agent/extensions/control.ts +1779 -0
- package/resources/pi-agent/extensions/diff.ts +218 -0
- package/resources/pi-agent/extensions/files.ts +199 -0
- package/resources/pi-agent/extensions/loop.ts +446 -0
- package/resources/pi-agent/extensions/multi-edit.ts +835 -0
- package/resources/pi-agent/extensions/notify.ts +88 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
- package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
- package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
- package/resources/pi-agent/extensions/redraws.ts +24 -0
- package/resources/pi-agent/extensions/review.ts +2160 -0
- package/resources/pi-agent/extensions/todos.ts +2076 -0
- package/resources/pi-agent/extensions/tps.ts +47 -0
- package/resources/pi-agent/extensions/whimsical.ts +474 -0
- package/resources/pi-agent/prompts/coder-system.md +106 -0
- package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
- package/resources/pi-agent/skills/docyrus-platform/SKILL.md +71 -0
- package/resources/pi-agent/skills/docyrus-platform/references/ai-capabilities.md +43 -0
- package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +35 -0
- package/resources/pi-agent/skills/docyrus-platform/references/automation-and-workflows.md +30 -0
- package/resources/pi-agent/skills/docyrus-platform/references/core-building-blocks.md +53 -0
- package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/data-source-query-guide.md +32 -28
- package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +28 -0
- package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +554 -0
- package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/formula-design-guide-llm.md +15 -23
- package/resources/pi-agent/skills/docyrus-platform/references/integrations-and-events.md +60 -0
- package/resources/pi-agent/skills/docyrus-platform/references/platform-services.md +58 -0
- package/resources/pi-agent/skills/docyrus-platform/references/querying-and-data-operations.md +27 -0
- package/resources/pi-agent/prompts/coder-append-system.md +0 -19
- package/resources/pi-agent/skills/docyrus-ai/SKILL.md +0 -28
- package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +0 -161
- package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +0 -349
- package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +0 -238
- package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +0 -592
- package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +0 -70
- package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +0 -588
- package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +0 -159
- package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +0 -275
- package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +0 -352
- package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +0 -2059
- package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +0 -320
- package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +0 -525
- package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +0 -466
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +0 -602
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +0 -463
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +0 -242
- package/resources/pi-agent/skills/docyrus-apps/SKILL.md +0 -54
- package/resources/pi-agent/skills/docyrus-architect/SKILL.md +0 -174
- package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +0 -410
- package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +0 -2059
- package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +0 -320
- package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +0 -145
- package/resources/pi-agent/skills/docyrus-auth/SKILL.md +0 -100
- package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +0 -279
- package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +0 -532
- package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +0 -248
- package/resources/pi-agent/skills/docyrus-curl/SKILL.md +0 -32
- package/resources/pi-agent/skills/docyrus-discover/SKILL.md +0 -63
- package/resources/pi-agent/skills/docyrus-ds/SKILL.md +0 -95
- package/resources/pi-agent/skills/docyrus-env/SKILL.md +0 -21
- package/resources/pi-agent/skills/docyrus-studio/SKILL.md +0 -369
- package/resources/pi-agent/skills/docyrus-tui/SKILL.md +0 -15
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /context
|
|
3
|
+
*
|
|
4
|
+
* Small TUI view showing what's loaded/available:
|
|
5
|
+
* - extensions (best-effort from registered extension slash commands)
|
|
6
|
+
* - skills
|
|
7
|
+
* - project context files (AGENTS.md / CLAUDE.md)
|
|
8
|
+
* - current context window usage + session totals (tokens/cost)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { Container, Key, Text, matchesKey, type Component, type TUI } from "@mariozechner/pi-tui";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
|
|
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)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function estimateTokens(text: string): number {
|
|
27
|
+
// Deliberately fuzzy (good enough for “how big-ish is this”).
|
|
28
|
+
return Math.max(0, Math.ceil(text.length / 4));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeReadPath(inputPath: string, cwd: string): string {
|
|
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);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getAgentDir(): string {
|
|
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");
|
|
66
|
+
}
|
|
67
|
+
|
|
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
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
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 });
|
|
89
|
+
// pi loads at most one of those per dir
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await loadFromDir(getAgentDir());
|
|
96
|
+
|
|
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;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeSkillName(name: string): string {
|
|
113
|
+
return name.startsWith("skill:") ? name.slice("skill:".length) : name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type SkillIndexEntry = {
|
|
117
|
+
name: string;
|
|
118
|
+
skillFilePath: string;
|
|
119
|
+
skillDir: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
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);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const SKILL_LOADED_ENTRY = "context:skill_loaded";
|
|
138
|
+
|
|
139
|
+
type SkillLoadedEntryData = {
|
|
140
|
+
name: string;
|
|
141
|
+
path: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
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;
|
|
153
|
+
}
|
|
154
|
+
|
|
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;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sumSessionUsage(ctx: ExtensionCommandContext): {
|
|
173
|
+
input: number;
|
|
174
|
+
output: number;
|
|
175
|
+
cacheRead: number;
|
|
176
|
+
cacheWrite: number;
|
|
177
|
+
totalTokens: number;
|
|
178
|
+
totalCost: number;
|
|
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
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
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;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderUsageBar(
|
|
218
|
+
theme: any,
|
|
219
|
+
parts: { system: number; tools: number; convo: number; remaining: number },
|
|
220
|
+
total: number,
|
|
221
|
+
width: number,
|
|
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;
|
|
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}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function joinComma(items: string[]): string {
|
|
245
|
+
return items.join(", ");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function joinCommaStyled(items: string[], renderItem: (item: string) => string, sep: string): string {
|
|
249
|
+
return items.map(renderItem).join(sep);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type ContextViewData = {
|
|
253
|
+
usage:
|
|
254
|
+
| {
|
|
255
|
+
// message-based context usage estimate from ctx.getContextUsage()
|
|
256
|
+
messageTokens: number;
|
|
257
|
+
contextWindow: number;
|
|
258
|
+
// effective usage incl. a rough tool-definition estimate
|
|
259
|
+
effectiveTokens: number;
|
|
260
|
+
percent: number;
|
|
261
|
+
remainingTokens: number;
|
|
262
|
+
systemPromptTokens: number;
|
|
263
|
+
agentTokens: number;
|
|
264
|
+
toolsTokens: number;
|
|
265
|
+
activeTools: number;
|
|
266
|
+
}
|
|
267
|
+
| null;
|
|
268
|
+
agentFiles: string[];
|
|
269
|
+
extensions: string[];
|
|
270
|
+
skills: string[];
|
|
271
|
+
loadedSkills: string[];
|
|
272
|
+
session: { totalTokens: number; totalCost: number };
|
|
273
|
+
};
|
|
274
|
+
|
|
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[] = [];
|
|
314
|
+
|
|
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: ") +
|
|
322
|
+
text(`~${u.effectiveTokens.toLocaleString()} / ${u.contextWindow.toLocaleString()}`) +
|
|
323
|
+
muted(` (${u.percent.toFixed(1)}% used, ~${u.remainingTokens.toLocaleString()} left)`),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// bar width tries to fit within the viewport
|
|
327
|
+
const barWidth = Math.max(10, Math.min(36, width - 10));
|
|
328
|
+
|
|
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 =
|
|
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,
|
|
343
|
+
) +
|
|
344
|
+
" " +
|
|
345
|
+
dim("sys") +
|
|
346
|
+
this.theme.fg("accent", "█") +
|
|
347
|
+
" " +
|
|
348
|
+
dim("tools") +
|
|
349
|
+
this.theme.fg("warning", "█") +
|
|
350
|
+
" " +
|
|
351
|
+
dim("convo") +
|
|
352
|
+
this.theme.fg("success", "█") +
|
|
353
|
+
" " +
|
|
354
|
+
dim("free") +
|
|
355
|
+
this.theme.fg("dim", "█");
|
|
356
|
+
lines.push(bar);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
lines.push("");
|
|
360
|
+
|
|
361
|
+
// System prompt + tools totals (approx)
|
|
362
|
+
if (this.data.usage) {
|
|
363
|
+
const u = this.data.usage;
|
|
364
|
+
lines.push(
|
|
365
|
+
muted("System: ") +
|
|
366
|
+
text(`~${u.systemPromptTokens.toLocaleString()} tok`) +
|
|
367
|
+
muted(` (AGENTS ~${u.agentTokens.toLocaleString()})`),
|
|
368
|
+
);
|
|
369
|
+
lines.push(
|
|
370
|
+
muted("Tools: ") +
|
|
371
|
+
text(`~${u.toolsTokens.toLocaleString()} tok`) +
|
|
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: ") +
|
|
392
|
+
text(`${this.data.session.totalTokens.toLocaleString()} tokens`) +
|
|
393
|
+
muted(" · ") +
|
|
394
|
+
text(formatUsd(this.data.session.totalCost)),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
this.body.setText(lines.join("\n"));
|
|
398
|
+
this.cachedWidth = width;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
handleInput(data: string): void {
|
|
402
|
+
if (
|
|
403
|
+
matchesKey(data, Key.escape) ||
|
|
404
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
405
|
+
data.toLowerCase() === "q" ||
|
|
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
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export default function contextExtension(pi: ExtensionAPI) {
|
|
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) => {
|
|
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;
|
|
505
|
+
|
|
506
|
+
// Tool definitions are not part of ctx.getContextUsage() (it estimates message tokens).
|
|
507
|
+
// We approximate their token impact from tool name + description, and apply a fudge
|
|
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
|
+
});
|
|
578
|
+
}
|