decorated-pi 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codegraph/daemon.pid +6 -0
- package/AGENTS.md +92 -0
- package/README.md +53 -64
- package/commands/dp-model.ts +23 -0
- package/commands/dp-settings.ts +23 -0
- package/commands/mcp-status.ts +62 -0
- package/commands/retry.ts +19 -0
- package/commands/usage.ts +544 -0
- package/hooks/compaction.ts +204 -0
- package/hooks/externalize.ts +70 -0
- package/hooks/image-vision.ts +132 -0
- package/hooks/inject-agents-md.ts +164 -0
- package/hooks/mcp.ts +340 -0
- package/hooks/normalize-codeblocks.ts +88 -0
- package/hooks/pi-tool-filter.ts +28 -0
- package/{extensions/safety → hooks/redact}/types.ts +1 -8
- package/hooks/redact.ts +104 -0
- package/{extensions → hooks}/rtk.ts +92 -115
- package/hooks/session-title.ts +54 -0
- package/hooks/skeleton.ts +212 -0
- package/hooks/smart-at.ts +318 -0
- package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
- package/{extensions → hooks}/wakatime.ts +120 -122
- package/index.ts +155 -1
- package/package.json +5 -4
- package/{extensions/settings.ts → settings.ts} +32 -0
- package/{extensions → tools}/lsp/client.ts +0 -25
- package/{extensions → tools}/lsp/format.ts +2 -99
- package/{extensions → tools}/lsp/index.ts +1 -1
- package/{extensions → tools}/lsp/servers.ts +1 -1
- package/{extensions → tools}/lsp/tools.ts +1 -66
- package/{extensions → tools}/lsp/types.ts +0 -11
- package/tools/mcp/builtin/codegraph.ts +45 -0
- package/tools/mcp/builtin/context7.ts +10 -0
- package/tools/mcp/builtin/exa.ts +10 -0
- package/tools/mcp/builtin/index.ts +19 -0
- package/tools/mcp/cache.ts +124 -0
- package/{extensions → tools}/mcp/client.ts +2 -2
- package/{extensions/mcp/builtin.ts → tools/mcp/config.ts} +21 -181
- package/tools/mcp/index.ts +44 -0
- package/tools/mcp/tool-definition.ts +112 -0
- package/{extensions/patch.ts → tools/patch/core.ts} +336 -65
- package/{extensions/io.ts → tools/patch/index.ts} +29 -205
- package/tsconfig.json +10 -1
- package/ui/mcp-status.ts +202 -0
- package/ui/model-picker.ts +162 -0
- package/ui/module-settings.ts +83 -0
- package/ui/usage.ts +396 -0
- package/extensions/index.ts +0 -154
- package/extensions/io-tool-output.ts +0 -33
- package/extensions/mcp/index.ts +0 -435
- package/extensions/model-integration.ts +0 -531
- package/extensions/providers/ark-coding.ts +0 -75
- package/extensions/providers/index.ts +0 -9
- package/extensions/providers/ollama-cloud.ts +0 -101
- package/extensions/providers/qianfan-coding.ts +0 -71
- package/extensions/safety/index.ts +0 -102
- package/extensions/session-title.ts +0 -40
- package/extensions/slash.ts +0 -458
- package/extensions/smart-at.ts +0 -481
- package/extensions/subdir-agents.ts +0 -151
- /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
- /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
- /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
- /package/{extensions → tools}/lsp/env.ts +0 -0
- /package/{extensions → tools}/lsp/manager.ts +0 -0
- /package/{extensions → tools}/lsp/prompt.ts +0 -0
- /package/{extensions → tools}/lsp/protocol.ts +0 -0
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model Integration — 模型集成
|
|
3
|
-
*
|
|
4
|
-
* 对外接口:
|
|
5
|
-
* analyzeImage(model, base64, mediaType, apiKey, headers) → Promise<string>
|
|
6
|
-
* getConfiguredImageModel(registry) → Model | null
|
|
7
|
-
*
|
|
8
|
-
* 内部事件:
|
|
9
|
-
* tool_call / tool_result: 图片 read → Vision API 回退
|
|
10
|
-
* session_before_compact: 自定义压缩模型
|
|
11
|
-
* session_compact: 压缩后自动继续
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
-
generateSummary, convertToLlm, serializeConversation,
|
|
17
|
-
DynamicBorder, keyHint, rawKeyHint, Theme,
|
|
18
|
-
} from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import {
|
|
20
|
-
Container, fuzzyFilter, getKeybindings, Input,
|
|
21
|
-
Spacer, Text, type TUI,
|
|
22
|
-
} from "@earendil-works/pi-tui";
|
|
23
|
-
import OpenAI from "openai";
|
|
24
|
-
import { fileTypeFromFile } from "file-type";
|
|
25
|
-
import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
|
|
26
|
-
import {
|
|
27
|
-
loadConfig, saveConfig, parseModelKey, formatModelKey,
|
|
28
|
-
getImageModelKey, getCompactModelKey,
|
|
29
|
-
setImageModelKey, setCompactModelKey,
|
|
30
|
-
} from "./settings.js";
|
|
31
|
-
import * as fs from "node:fs";
|
|
32
|
-
import * as os from "node:os";
|
|
33
|
-
import { extname, resolve } from "node:path";
|
|
34
|
-
|
|
35
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
-
// SDK 接口
|
|
37
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
-
|
|
39
|
-
export function getConfiguredImageModel(registry: any): Model<any> | null {
|
|
40
|
-
const key = getImageModelKey();
|
|
41
|
-
if (!key) return null;
|
|
42
|
-
const parsed = parseModelKey(key);
|
|
43
|
-
if (!parsed) return null;
|
|
44
|
-
return registry.find(parsed.provider, parsed.modelId) ?? null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const DEFAULT_PROMPT =
|
|
48
|
-
"Please describe this image in detail, including any text, diagrams, UI elements, or code visible in it.";
|
|
49
|
-
|
|
50
|
-
export async function analyzeImage(
|
|
51
|
-
model: Model<any>, imageBase64: string, mediaType: string,
|
|
52
|
-
apiKey: string, extraHeaders: Record<string, string>
|
|
53
|
-
): Promise<string> {
|
|
54
|
-
if (model.api === "anthropic-messages") {
|
|
55
|
-
return analyzeAnthropic(model, imageBase64, mediaType, apiKey, extraHeaders);
|
|
56
|
-
}
|
|
57
|
-
return analyzeOpenAI(model, imageBase64, mediaType, apiKey, extraHeaders);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function analyzeOpenAI(
|
|
61
|
-
model: Model<any>, imageBase64: string, mediaType: string,
|
|
62
|
-
apiKey: string, extraHeaders: Record<string, string>
|
|
63
|
-
): Promise<string> {
|
|
64
|
-
const client = new OpenAI({ apiKey, baseURL: model.baseUrl, defaultHeaders: extraHeaders });
|
|
65
|
-
const resp = await client.chat.completions.create({
|
|
66
|
-
model: model.id,
|
|
67
|
-
messages: [{ role: "user", content: [
|
|
68
|
-
{ type: "text", text: DEFAULT_PROMPT },
|
|
69
|
-
{ type: "image_url", image_url: { url: `data:${mediaType};base64,${imageBase64}` } },
|
|
70
|
-
]}],
|
|
71
|
-
max_completion_tokens: 4096,
|
|
72
|
-
}, { signal: AbortSignal.timeout(60_000) });
|
|
73
|
-
return resp.choices[0]?.message?.content ?? "No analysis returned.";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function analyzeAnthropic(
|
|
77
|
-
model: Model<any>, imageBase64: string, mediaType: string,
|
|
78
|
-
apiKey: string, extraHeaders: Record<string, string>
|
|
79
|
-
): Promise<string> {
|
|
80
|
-
const ep = `${model.baseUrl.replace(/\/+$/, "")}/messages`;
|
|
81
|
-
const resp = await fetch(ep, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01", ...extraHeaders },
|
|
84
|
-
body: JSON.stringify({
|
|
85
|
-
model: model.id, max_tokens: 4096,
|
|
86
|
-
messages: [{ role: "user", content: [
|
|
87
|
-
{ type: "text", text: DEFAULT_PROMPT },
|
|
88
|
-
{ type: "image", source: { type: "base64", media_type: mediaType, data: imageBase64 } },
|
|
89
|
-
]}],
|
|
90
|
-
}),
|
|
91
|
-
signal: AbortSignal.timeout(60_000),
|
|
92
|
-
});
|
|
93
|
-
if (!resp.ok) throw new Error(`Vision API error ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
|
|
94
|
-
const data = (await resp.json()) as any;
|
|
95
|
-
return data.content?.[0]?.text ?? "No analysis returned.";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
-
// 图片检测(magic bytes)
|
|
100
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
|
-
|
|
102
|
-
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
103
|
-
|
|
104
|
-
async function detectImageMimeType(filePath: string): Promise<string | null> {
|
|
105
|
-
try {
|
|
106
|
-
const type = await fileTypeFromFile(filePath);
|
|
107
|
-
if (!type || !SUPPORTED_IMAGE_TYPES.has(type.mime)) return null;
|
|
108
|
-
return type.mime;
|
|
109
|
-
} catch { return null; }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
-
// 图片 read 回退
|
|
114
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
115
|
-
|
|
116
|
-
function setupImageReadFallback(pi: ExtensionAPI) {
|
|
117
|
-
const pendingFallbacks = new Set<string>();
|
|
118
|
-
|
|
119
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
120
|
-
if (event.toolName !== "read") return;
|
|
121
|
-
const filePath: string | undefined = (event.input as any)?.file ?? (event.input as any)?.path;
|
|
122
|
-
if (!filePath) return;
|
|
123
|
-
|
|
124
|
-
const mimeType = await detectImageMimeType(resolve(ctx.cwd, filePath));
|
|
125
|
-
if (!mimeType) return;
|
|
126
|
-
if (!getImageModelKey()) return;
|
|
127
|
-
|
|
128
|
-
pendingFallbacks.add(event.toolCallId);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
pi.on("tool_result", async (event, ctx) => {
|
|
132
|
-
if (!pendingFallbacks.delete(event.toolCallId)) return;
|
|
133
|
-
const filePath: string | undefined = (event.input as any)?.file ?? (event.input as any)?.path;
|
|
134
|
-
if (!filePath) return;
|
|
135
|
-
|
|
136
|
-
const imageKey = getImageModelKey();
|
|
137
|
-
if (!imageKey) return;
|
|
138
|
-
const parsed = parseModelKey(imageKey);
|
|
139
|
-
if (!parsed) return;
|
|
140
|
-
|
|
141
|
-
const imageModel = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
142
|
-
if (!imageModel) return;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const absPath = resolve(ctx.cwd, filePath);
|
|
146
|
-
const imageData = fs.readFileSync(absPath);
|
|
147
|
-
const imageBase64 = imageData.toString("base64");
|
|
148
|
-
const mimeType = await detectImageMimeType(absPath) ?? "image/png";
|
|
149
|
-
|
|
150
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(imageModel as Model<any>);
|
|
151
|
-
if (!auth.ok) return;
|
|
152
|
-
|
|
153
|
-
const analysis = await analyzeImage(
|
|
154
|
-
imageModel as Model<any>, imageBase64, mimeType,
|
|
155
|
-
auth.apiKey ?? "", auth.headers ?? {}
|
|
156
|
-
);
|
|
157
|
-
return {
|
|
158
|
-
content: [{ type: "text", text: `[Image analysis via ${parsed.provider}/${parsed.modelId}]\n\n${analysis}` }],
|
|
159
|
-
details: { imageModel: imageKey, originalPath: filePath },
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return {
|
|
163
|
-
content: [{ type: "text", text: `Image analysis failed: ${error instanceof Error ? error.message : error}` }],
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
-
// 模型选择器组件
|
|
171
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
-
|
|
173
|
-
const TAB_IMAGE = 0;
|
|
174
|
-
const TAB_COMPACT = 1;
|
|
175
|
-
|
|
176
|
-
export class ModelPickerComponent extends Container {
|
|
177
|
-
private searchInput: Input;
|
|
178
|
-
private tui: TUI;
|
|
179
|
-
private theme: Theme;
|
|
180
|
-
private registry: any;
|
|
181
|
-
private onDone: () => void;
|
|
182
|
-
private activeTab = TAB_IMAGE;
|
|
183
|
-
private imageKey: string | null;
|
|
184
|
-
private compactKey: string | null;
|
|
185
|
-
private allItems: { label: string; desc: string; model: Model<any> | null; modelName?: string }[] = [];
|
|
186
|
-
private filtered: typeof this.allItems = [];
|
|
187
|
-
private selectedIndex = 0;
|
|
188
|
-
private tabTitle = new Text("", 1, 0);
|
|
189
|
-
private subtitleText: Text;
|
|
190
|
-
private listContainer: Container;
|
|
191
|
-
|
|
192
|
-
constructor(tui: TUI, theme: unknown, registry: any, onDone: () => void) {
|
|
193
|
-
super();
|
|
194
|
-
this.tui = tui;
|
|
195
|
-
this.theme = theme as Theme;
|
|
196
|
-
this.registry = registry;
|
|
197
|
-
this.onDone = onDone;
|
|
198
|
-
this.imageKey = getImageModelKey();
|
|
199
|
-
this.compactKey = getCompactModelKey();
|
|
200
|
-
|
|
201
|
-
this.addChild(new DynamicBorder());
|
|
202
|
-
this.addChild(new Spacer(1));
|
|
203
|
-
this.addChild(this.tabTitle);
|
|
204
|
-
this.subtitleText = new Text("", 1, 0);
|
|
205
|
-
this.addChild(this.subtitleText);
|
|
206
|
-
this.addChild(new Spacer(1));
|
|
207
|
-
|
|
208
|
-
this.searchInput = new Input();
|
|
209
|
-
this.searchInput.onSubmit = () => { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); };
|
|
210
|
-
this.addChild(this.searchInput);
|
|
211
|
-
this.addChild(new Spacer(1));
|
|
212
|
-
|
|
213
|
-
this.listContainer = new Container();
|
|
214
|
-
this.addChild(this.listContainer);
|
|
215
|
-
this.addChild(new Spacer(1));
|
|
216
|
-
|
|
217
|
-
this.addChild(new Text(
|
|
218
|
-
rawKeyHint("↑↓", "navigate") + " " + keyHint("tui.input.tab", "switch") + " " +
|
|
219
|
-
keyHint("tui.select.confirm", "select") + " " + keyHint("tui.select.cancel", "cancel"), 1, 0));
|
|
220
|
-
this.addChild(new Spacer(1));
|
|
221
|
-
this.addChild(new DynamicBorder());
|
|
222
|
-
|
|
223
|
-
this.loadModels().then(() => { this.switchTab(TAB_IMAGE); this.tui.requestRender(); });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private async loadModels() {
|
|
227
|
-
this.registry.refresh();
|
|
228
|
-
const available = this.registry.getAvailable() as Model<any>[];
|
|
229
|
-
this.allItems = [{ label: "clear", desc: "(unset)", model: null }];
|
|
230
|
-
for (const m of available) {
|
|
231
|
-
this.allItems.push({ label: m.id, desc: `[${m.provider}]`, model: m as Model<any>, modelName: m.name });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.compactKey; }
|
|
236
|
-
private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : "compact"; }
|
|
237
|
-
|
|
238
|
-
private switchTab(tab: number) {
|
|
239
|
-
this.activeTab = tab;
|
|
240
|
-
const key = this.currentKey();
|
|
241
|
-
const [clearItem, ...rest] = this.allItems;
|
|
242
|
-
const items = rest.map(it => {
|
|
243
|
-
const isCurrent = it.model && formatModelKey(it.model) === key;
|
|
244
|
-
return { ...it, desc: `${it.desc}${isCurrent ? " ✓" : ""}` };
|
|
245
|
-
});
|
|
246
|
-
items.sort((a, b) => {
|
|
247
|
-
const aCur = a.model && formatModelKey(a.model) === key;
|
|
248
|
-
const bCur = b.model && formatModelKey(b.model) === key;
|
|
249
|
-
if (aCur && !bCur) return -1; if (!aCur && bCur) return 1; return 0;
|
|
250
|
-
});
|
|
251
|
-
this.filtered = [clearItem, ...items];
|
|
252
|
-
this.selectedIndex = 0;
|
|
253
|
-
if (key) { const ix = this.filtered.findIndex(m => m.model && formatModelKey(m.model) === key); if (ix >= 0) this.selectedIndex = ix; }
|
|
254
|
-
this.searchInput.setValue("");
|
|
255
|
-
this.updateHeader();
|
|
256
|
-
this.updateList();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private updateHeader() {
|
|
260
|
-
const t = this.theme;
|
|
261
|
-
const im = this.activeTab === TAB_IMAGE ? t.fg("accent", "●") : "○";
|
|
262
|
-
const cm = this.activeTab === TAB_COMPACT ? t.fg("accent", "●") : "○";
|
|
263
|
-
const il = this.activeTab === TAB_IMAGE ? t.bold("Image") : t.fg("dim", "Image");
|
|
264
|
-
const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact") : t.fg("dim", "Compact");
|
|
265
|
-
this.tabTitle.setText(`${im} ${il} | ${cm} ${cl}`);
|
|
266
|
-
const key = this.currentKey();
|
|
267
|
-
this.subtitleText.setText(key ? t.fg("warning", `Current ${this.currentKind()} model: ${key}`) : t.fg("warning", `No ${this.currentKind()} model set`));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
handleInput(keyData: string) {
|
|
271
|
-
const kb = getKeybindings();
|
|
272
|
-
if (kb.matches(keyData, "tui.input.tab")) {
|
|
273
|
-
const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT : TAB_IMAGE;
|
|
274
|
-
this.switchTab(next); this.tui.requestRender(); return;
|
|
275
|
-
}
|
|
276
|
-
if (kb.matches(keyData, "tui.select.up")) { this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1; this.updateList(); return; }
|
|
277
|
-
if (kb.matches(keyData, "tui.select.down")) { this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); return; }
|
|
278
|
-
if (kb.matches(keyData, "tui.select.confirm")) { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); return; }
|
|
279
|
-
if (kb.matches(keyData, "tui.select.cancel")) { this.onDone(); return; }
|
|
280
|
-
this.searchInput.handleInput(keyData); this.applyFilter();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private applyFilter() {
|
|
284
|
-
const raw = this.searchInput.getValue();
|
|
285
|
-
if (!raw) { this.switchTab(this.activeTab); return; }
|
|
286
|
-
const [clearItem, ...rest] = this.filtered;
|
|
287
|
-
this.filtered = [clearItem, ...fuzzyFilter(rest, raw, ({ label, desc }) => `${label} ${desc}`)];
|
|
288
|
-
this.selectedIndex = 0; this.updateList();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private selectModel(model: Model<any> | null) {
|
|
292
|
-
const kind = this.currentKind();
|
|
293
|
-
if (model) {
|
|
294
|
-
if (kind === "image") setImageModelKey(formatModelKey(model));
|
|
295
|
-
else setCompactModelKey(formatModelKey(model));
|
|
296
|
-
} else {
|
|
297
|
-
if (kind === "image") setImageModelKey(null);
|
|
298
|
-
else setCompactModelKey(null);
|
|
299
|
-
}
|
|
300
|
-
if (kind === "image") this.imageKey = model ? formatModelKey(model) : null;
|
|
301
|
-
else this.compactKey = model ? formatModelKey(model) : null;
|
|
302
|
-
this.switchTab(this.activeTab); this.tui.requestRender();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private updateList() {
|
|
306
|
-
this.listContainer.clear();
|
|
307
|
-
const t = this.theme;
|
|
308
|
-
const mv = 10;
|
|
309
|
-
const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(mv / 2), Math.max(0, this.filtered.length - mv)));
|
|
310
|
-
const end = Math.min(start + mv, this.filtered.length);
|
|
311
|
-
for (let i = start; i < end; i++) {
|
|
312
|
-
const item = this.filtered[i]; if (!item) continue;
|
|
313
|
-
const isClear = item.model === null;
|
|
314
|
-
const isSel = i === this.selectedIndex;
|
|
315
|
-
const line = isClear
|
|
316
|
-
? (isSel ? t.fg("accent", "→ ") + t.fg("error", "clear") + t.fg("muted", " (unset)") : " " + t.fg("muted", "clear (unset)"))
|
|
317
|
-
: (isSel ? t.fg("accent", "→ ") + t.fg("accent", item.label) + " " + t.fg("muted", item.desc) : " " + item.label + " " + t.fg("muted", item.desc));
|
|
318
|
-
this.listContainer.addChild(new Text(line, 0, 0));
|
|
319
|
-
}
|
|
320
|
-
if (start > 0 || end < this.filtered.length) this.listContainer.addChild(new Text(t.fg("muted", ` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0));
|
|
321
|
-
const sel = this.filtered[this.selectedIndex];
|
|
322
|
-
if (sel?.modelName) { this.listContainer.addChild(new Spacer(1)); this.listContainer.addChild(new Text(t.fg("muted", ` Name: ${sel.modelName}`), 0, 0)); }
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
-
// 压缩辅助
|
|
328
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
-
|
|
330
|
-
const TURN_PREFIX_PROMPT = `Summarize this turn prefix to provide context for the retained suffix.
|
|
331
|
-
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
332
|
-
|
|
333
|
-
async function generateTurnPrefixSummary(
|
|
334
|
-
messages: Parameters<typeof generateSummary>[0],
|
|
335
|
-
model: Parameters<typeof generateSummary>[1], reserveTokens: number,
|
|
336
|
-
apiKey: string, headers: Record<string, string> | undefined, signal: AbortSignal,
|
|
337
|
-
): Promise<string> {
|
|
338
|
-
const { complete } = await import("@earendil-works/pi-ai");
|
|
339
|
-
const ct = serializeConversation(convertToLlm(messages));
|
|
340
|
-
const resp = await complete(model, {
|
|
341
|
-
systemPrompt: "You are a context summarization assistant. Produce a structured summary only.",
|
|
342
|
-
messages: [{ role: "user" as const, content: [{ type: "text" as const, text: `<conversation>\n${ct}\n</conversation>\n\n${TURN_PREFIX_PROMPT}` }], timestamp: Date.now() }],
|
|
343
|
-
}, { maxTokens: Math.floor(0.5 * reserveTokens), signal, apiKey, headers });
|
|
344
|
-
if (resp.stopReason === "error") throw new Error(resp.errorMessage ?? "Turn prefix summarization failed");
|
|
345
|
-
return resp.content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text).join("\n");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function getConfiguredCompactModel(registry: any): Model<any> | null {
|
|
349
|
-
const key = getCompactModelKey();
|
|
350
|
-
if (!key) return null;
|
|
351
|
-
const parsed = parseModelKey(key);
|
|
352
|
-
if (!parsed) return null;
|
|
353
|
-
return registry.find(parsed.provider, parsed.modelId) ?? null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
interface PiCompactionSettings {
|
|
357
|
-
enabled: boolean;
|
|
358
|
-
reserveTokens: number;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
interface AutoCompactionCandidate {
|
|
362
|
-
messages: any[];
|
|
363
|
-
usage: { tokens: number | null; contextWindow: number } | undefined;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const DEFAULT_PI_COMPACTION_SETTINGS: PiCompactionSettings = {
|
|
367
|
-
enabled: true,
|
|
368
|
-
reserveTokens: 16_384,
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
function readJsonObject(filePath: string): any | undefined {
|
|
372
|
-
try {
|
|
373
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
374
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
375
|
-
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
376
|
-
} catch {
|
|
377
|
-
return undefined;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function loadPiCompactionSettings(cwd: string): PiCompactionSettings {
|
|
382
|
-
const globalSettings = readJsonObject(resolve(os.homedir(), ".pi", "agent", "settings.json"));
|
|
383
|
-
const projectSettings = readJsonObject(resolve(cwd, ".pi", "settings.json"));
|
|
384
|
-
const merged = {
|
|
385
|
-
...DEFAULT_PI_COMPACTION_SETTINGS,
|
|
386
|
-
...(globalSettings?.compaction ?? {}),
|
|
387
|
-
...(projectSettings?.compaction ?? {}),
|
|
388
|
-
};
|
|
389
|
-
return {
|
|
390
|
-
enabled: merged.enabled !== false,
|
|
391
|
-
reserveTokens: typeof merged.reserveTokens === "number" ? merged.reserveTokens : DEFAULT_PI_COMPACTION_SETTINGS.reserveTokens,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function getLastAssistantMessage(messages: any[]): any | undefined {
|
|
396
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
397
|
-
if (messages[i]?.role === "assistant") return messages[i];
|
|
398
|
-
}
|
|
399
|
-
return undefined;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function shouldExpectAutoCompaction(
|
|
403
|
-
messages: any[],
|
|
404
|
-
usage: { tokens: number | null; contextWindow: number } | undefined,
|
|
405
|
-
settings: PiCompactionSettings,
|
|
406
|
-
): boolean {
|
|
407
|
-
if (!settings.enabled) return false;
|
|
408
|
-
|
|
409
|
-
const lastAssistant = getLastAssistantMessage(messages);
|
|
410
|
-
if (!lastAssistant) return false;
|
|
411
|
-
|
|
412
|
-
const contextWindow = usage?.contextWindow ?? 0;
|
|
413
|
-
if (contextWindow > 0 && isContextOverflow(lastAssistant, contextWindow)) {
|
|
414
|
-
return true;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (!usage || usage.tokens === null) return false;
|
|
418
|
-
return usage.tokens > usage.contextWindow - settings.reserveTokens;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function shouldAutoResumeCompaction(
|
|
422
|
-
prePromptCompactionPending: boolean,
|
|
423
|
-
postAgentEndCandidate: AutoCompactionCandidate | null,
|
|
424
|
-
settings: PiCompactionSettings,
|
|
425
|
-
customInstructions?: string,
|
|
426
|
-
): boolean {
|
|
427
|
-
if (customInstructions !== undefined) return false;
|
|
428
|
-
if (prePromptCompactionPending) return true;
|
|
429
|
-
if (!postAgentEndCandidate) return false;
|
|
430
|
-
return shouldExpectAutoCompaction(postAgentEndCandidate.messages, postAgentEndCandidate.usage, settings);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export const __modelIntegrationTest = {
|
|
434
|
-
shouldExpectAutoCompaction,
|
|
435
|
-
shouldAutoResumeCompaction,
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
439
|
-
// 主入口(注册所有事件)
|
|
440
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
441
|
-
|
|
442
|
-
export function setupModelIntegration(pi: ExtensionAPI) {
|
|
443
|
-
setupImageReadFallback(pi);
|
|
444
|
-
|
|
445
|
-
let prePromptCompactionPending = false;
|
|
446
|
-
let postAgentEndCandidate: AutoCompactionCandidate | null = null;
|
|
447
|
-
let currentCompactionIsAuto = false;
|
|
448
|
-
|
|
449
|
-
pi.on("input", () => {
|
|
450
|
-
prePromptCompactionPending = true;
|
|
451
|
-
postAgentEndCandidate = null;
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
pi.on("before_agent_start", () => {
|
|
455
|
-
prePromptCompactionPending = false;
|
|
456
|
-
postAgentEndCandidate = null;
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
pi.on("agent_start", () => {
|
|
460
|
-
prePromptCompactionPending = false;
|
|
461
|
-
postAgentEndCandidate = null;
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
pi.on("agent_end", (event, ctx) => {
|
|
465
|
-
prePromptCompactionPending = false;
|
|
466
|
-
postAgentEndCandidate = {
|
|
467
|
-
messages: event.messages,
|
|
468
|
-
usage: ctx.getContextUsage(),
|
|
469
|
-
};
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// 自定义压缩模型
|
|
473
|
-
pi.on("session_before_compact", async (event, ctx) => {
|
|
474
|
-
const compactionSettings = loadPiCompactionSettings(ctx.cwd);
|
|
475
|
-
currentCompactionIsAuto = shouldAutoResumeCompaction(
|
|
476
|
-
prePromptCompactionPending,
|
|
477
|
-
postAgentEndCandidate,
|
|
478
|
-
compactionSettings,
|
|
479
|
-
event.customInstructions,
|
|
480
|
-
);
|
|
481
|
-
prePromptCompactionPending = false;
|
|
482
|
-
postAgentEndCandidate = null;
|
|
483
|
-
|
|
484
|
-
const model = getConfiguredCompactModel(ctx.modelRegistry);
|
|
485
|
-
if (!model) return; // 没配 → Pi 默认
|
|
486
|
-
|
|
487
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
488
|
-
if (!auth.ok) { ctx.ui.notify(`Compact model auth failed: ${auth.error}`, "warning"); return; }
|
|
489
|
-
|
|
490
|
-
const { preparation, customInstructions, signal } = event;
|
|
491
|
-
const { messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore,
|
|
492
|
-
firstKeptEntryId, previousSummary, fileOps, settings } = preparation;
|
|
493
|
-
|
|
494
|
-
ctx.ui.notify(`🗜️ Compacting with ${model.id} (${tokensBefore.toLocaleString()} tokens)...`, "info");
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
let summary: string;
|
|
498
|
-
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
499
|
-
const [hs, ps] = await Promise.all([
|
|
500
|
-
messagesToSummarize.length > 0
|
|
501
|
-
? generateSummary(messagesToSummarize, model, settings.reserveTokens,
|
|
502
|
-
auth.apiKey ?? "", auth.headers, signal, customInstructions, previousSummary)
|
|
503
|
-
: Promise.resolve("No prior history."),
|
|
504
|
-
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens,
|
|
505
|
-
auth.apiKey ?? "", auth.headers, signal),
|
|
506
|
-
]);
|
|
507
|
-
summary = `${hs}\n\n---\n\n**Turn Context (split turn):**\n\n${ps}`;
|
|
508
|
-
} else {
|
|
509
|
-
summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens,
|
|
510
|
-
auth.apiKey ?? "", auth.headers, signal, customInstructions, previousSummary);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return { compaction: { summary, firstKeptEntryId, tokensBefore } };
|
|
514
|
-
} catch (err) {
|
|
515
|
-
if (signal.aborted) return;
|
|
516
|
-
ctx.ui.notify(`Compact failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// 压缩后自动继续(仅自动压缩)
|
|
521
|
-
pi.on("session_compact", () => {
|
|
522
|
-
const shouldResume = currentCompactionIsAuto;
|
|
523
|
-
currentCompactionIsAuto = false;
|
|
524
|
-
if (!shouldResume) return;
|
|
525
|
-
pi.sendMessage({
|
|
526
|
-
customType: "auto_compact_resume",
|
|
527
|
-
content: "The context was just auto-compacted. Continue the current task based on the summary above. Do not repeat completed work. If unsure about progress, briefly summarize current state then continue.",
|
|
528
|
-
display: false,
|
|
529
|
-
}, { triggerTurn: true });
|
|
530
|
-
});
|
|
531
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ARK Coding Plan — OAuth/subscription provider with hardcoded models
|
|
3
|
-
*
|
|
4
|
-
* Provider: "ark-coding"
|
|
5
|
-
* Base URL: https://ark.cn-beijing.volces.com/api/coding/v3 (OpenAI-compatible)
|
|
6
|
-
* Auth: OAuth/subscription login → prompt for API key
|
|
7
|
-
*
|
|
8
|
-
* All models hardcoded. No dynamic fetching, no config file caching.
|
|
9
|
-
* - No auth → no models in /model (clean UX, via hasConfiguredAuth)
|
|
10
|
-
* - Login → models become available immediately
|
|
11
|
-
* - Startup → models registered unconditionally (hardcoded)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
16
|
-
|
|
17
|
-
const PROVIDER_ID = "ark-coding";
|
|
18
|
-
const PROVIDER_DISPLAY_NAME = "ARK Coding Plan";
|
|
19
|
-
const BASE_URL = "https://ark.cn-beijing.volces.com/api/coding/v3";
|
|
20
|
-
|
|
21
|
-
// ── Hardcoded models (parameters from models.dev) ─────────────────────────
|
|
22
|
-
|
|
23
|
-
const MODELS: ProviderModelConfig[] = [
|
|
24
|
-
{ id: "deepseek-v3.2", name: "DeepSeek V3.2", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 163_840, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
25
|
-
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 1_048_576, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
26
|
-
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 1_048_576, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
27
|
-
{ id: "glm-4.7", name: "GLM 4.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
28
|
-
{ id: "glm-5.1", name: "GLM 5.1", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
29
|
-
{ id: "kimi-k2.5", name: "Kimi K2.5", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
30
|
-
{ id: "kimi-k2.6", name: "Kimi K2.6", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
31
|
-
{ id: "minimax-m2.5", name: "MiniMax M2.5", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204_800, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
32
|
-
{ id: "minimax-m2.7", name: "MiniMax M2.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 196_608, maxTokens: 196_608, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
33
|
-
{ id: "doubao-seed-2.0-code", name: "Doubao Seed 2.0 Code", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
34
|
-
{ id: "doubao-seed-2.0-pro", name: "Doubao Seed 2.0 Pro", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
35
|
-
{ id: "doubao-seed-2.0-lite", name: "Doubao Seed 2.0 Lite", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 32_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
36
|
-
{ id: "doubao-seed-code", name: "Doubao Seed Code", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 16_384, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
// ── Entry ──────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
export function setupArkCoding(pi: ExtensionAPI) {
|
|
42
|
-
pi.registerProvider(PROVIDER_ID, {
|
|
43
|
-
name: PROVIDER_DISPLAY_NAME,
|
|
44
|
-
baseUrl: BASE_URL,
|
|
45
|
-
api: "openai-completions",
|
|
46
|
-
authHeader: true,
|
|
47
|
-
models: MODELS,
|
|
48
|
-
oauth: {
|
|
49
|
-
name: PROVIDER_DISPLAY_NAME,
|
|
50
|
-
|
|
51
|
-
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
52
|
-
const apiKey = (await callbacks.onPrompt({
|
|
53
|
-
message: "Enter ARK Coding Plan API key:",
|
|
54
|
-
placeholder: "your-api-key",
|
|
55
|
-
})).trim();
|
|
56
|
-
|
|
57
|
-
if (!apiKey) throw new Error("API key cannot be empty.");
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
refresh: apiKey,
|
|
61
|
-
access: apiKey,
|
|
62
|
-
expires: Date.now() + 1000 * 365.24 * 24 * 3600 * 1000, // ~1000 years
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
refreshToken(cred: OAuthCredentials): Promise<OAuthCredentials> {
|
|
67
|
-
return Promise.resolve(cred); // API key never expires
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
getApiKey(cred: OAuthCredentials): string {
|
|
71
|
-
return cred.access;
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { setupArkCoding } from "./ark-coding.js";
|
|
2
|
-
import { setupOllamaCloud } from "./ollama-cloud.js";
|
|
3
|
-
import { setupQianfanCoding } from "./qianfan-coding.js";
|
|
4
|
-
|
|
5
|
-
export function setupProviders(pi: any) {
|
|
6
|
-
setupArkCoding(pi);
|
|
7
|
-
setupOllamaCloud(pi);
|
|
8
|
-
setupQianfanCoding(pi);
|
|
9
|
-
}
|