decorated-pi 0.2.2 → 0.3.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/README.md +44 -60
- package/extensions/file-times.ts +66 -0
- package/extensions/index.ts +4 -2
- package/extensions/io.ts +406 -0
- package/extensions/lsp/tools.ts +59 -1
- package/extensions/{extend-model.ts → model-integration.ts} +127 -4
- package/extensions/patch.ts +624 -0
- package/extensions/safety/detect.ts +170 -75
- package/extensions/safety/index.ts +54 -15
- package/extensions/settings.ts +2 -0
- package/extensions/slash.ts +6 -4
- package/extensions/smart-at.ts +339 -111
- package/package.json +2 -2
package/extensions/lsp/tools.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Modifications: added lsp_find_symbol, lsp_rename, multi-file lsp_diagnostics
|
|
8
8
|
*/
|
|
9
|
-
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { defineTool, keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
11
|
import { Type } from "typebox";
|
|
11
12
|
import { list_supported_languages } from "./servers.js";
|
|
12
13
|
import {
|
|
@@ -29,6 +30,7 @@ const SYMBOL_KIND_SCHEMA = Type.Union(
|
|
|
29
30
|
);
|
|
30
31
|
|
|
31
32
|
const DIAGNOSTICS_MANY_CONCURRENCY = 8;
|
|
33
|
+
const LSP_RESULT_FOLD_LINES = 20;
|
|
32
34
|
|
|
33
35
|
function make_tool_result(
|
|
34
36
|
text: string,
|
|
@@ -47,6 +49,55 @@ function make_tool_error(details: any) {
|
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function trim_trailing_empty_lines(lines: string[]): string[] {
|
|
53
|
+
let end = lines.length;
|
|
54
|
+
while (end > 0 && lines[end - 1] === "") {
|
|
55
|
+
end -= 1;
|
|
56
|
+
}
|
|
57
|
+
return lines.slice(0, end);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collapse_lsp_text(text: string, maxLines = LSP_RESULT_FOLD_LINES) {
|
|
61
|
+
const lines = trim_trailing_empty_lines(text.split("\n"));
|
|
62
|
+
const totalLines = lines.length;
|
|
63
|
+
const displayLines = lines.slice(0, maxLines);
|
|
64
|
+
return {
|
|
65
|
+
totalLines,
|
|
66
|
+
displayLines,
|
|
67
|
+
remainingLines: Math.max(0, totalLines - displayLines.length),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function get_text_content(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
72
|
+
return (result.content ?? [])
|
|
73
|
+
.filter((item): item is { type: "text"; text?: string } => item.type === "text")
|
|
74
|
+
.map((item) => item.text ?? "")
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function format_lsp_result_text(text: string, expanded: boolean, theme: any): string {
|
|
79
|
+
const { totalLines, displayLines, remainingLines } = collapse_lsp_text(
|
|
80
|
+
text,
|
|
81
|
+
expanded ? Number.MAX_SAFE_INTEGER : LSP_RESULT_FOLD_LINES,
|
|
82
|
+
);
|
|
83
|
+
const body = displayLines.join("\n");
|
|
84
|
+
let rendered = body ? theme.fg("toolOutput", body) : "";
|
|
85
|
+
if (!expanded && remainingLines > 0) {
|
|
86
|
+
rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
87
|
+
}
|
|
88
|
+
return rendered;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function render_lsp_result(result: any, options: { expanded: boolean }, theme: any, context: any) {
|
|
92
|
+
const component = context.lastComponent ?? new Text("", 0, 0);
|
|
93
|
+
component.setText(format_lsp_result_text(get_text_content(result), options.expanded, theme));
|
|
94
|
+
return component;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const __lspToolsTest = {
|
|
98
|
+
collapse_lsp_text,
|
|
99
|
+
};
|
|
100
|
+
|
|
50
101
|
async function map_with_concurrency<T, R>(
|
|
51
102
|
items: T[],
|
|
52
103
|
concurrency: number,
|
|
@@ -106,6 +157,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
106
157
|
defineTool({
|
|
107
158
|
name: "lsp_diagnostics",
|
|
108
159
|
label: "LSP: diagnostics",
|
|
160
|
+
renderResult: render_lsp_result,
|
|
109
161
|
description:
|
|
110
162
|
"Get language server diagnostics for one or more files. Default filter: error. Supports optional severity filtering.",
|
|
111
163
|
promptSnippet: "Get language server diagnostics for one or more files",
|
|
@@ -234,6 +286,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
234
286
|
defineTool({
|
|
235
287
|
name: "lsp_find_symbol",
|
|
236
288
|
label: "LSP: find symbol",
|
|
289
|
+
renderResult: render_lsp_result,
|
|
237
290
|
description:
|
|
238
291
|
"Find symbols in a file by name or detail text using document symbols. Supports exact matching, kind filters, and top-level-only mode.",
|
|
239
292
|
promptSnippet: "Find symbols in a file by name, kind, or match mode",
|
|
@@ -305,6 +358,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
305
358
|
defineTool({
|
|
306
359
|
name: "lsp_hover",
|
|
307
360
|
label: "LSP: hover",
|
|
361
|
+
renderResult: render_lsp_result,
|
|
308
362
|
description:
|
|
309
363
|
"Get hover info (types, docs) at a position in a file. Positions are zero-based.",
|
|
310
364
|
promptSnippet: "Get types and documentation at a symbol position",
|
|
@@ -348,6 +402,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
348
402
|
defineTool({
|
|
349
403
|
name: "lsp_definition",
|
|
350
404
|
label: "LSP: go to definition",
|
|
405
|
+
renderResult: render_lsp_result,
|
|
351
406
|
description:
|
|
352
407
|
"Find definition locations for the symbol at a position. Positions are zero-based.",
|
|
353
408
|
promptSnippet: "Find definition locations for a symbol at a position",
|
|
@@ -394,6 +449,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
394
449
|
defineTool({
|
|
395
450
|
name: "lsp_references",
|
|
396
451
|
label: "LSP: find references",
|
|
452
|
+
renderResult: render_lsp_result,
|
|
397
453
|
description:
|
|
398
454
|
"Find references to the symbol at a position. Positions are zero-based.",
|
|
399
455
|
promptSnippet: "Find references to a symbol at a position",
|
|
@@ -444,6 +500,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
444
500
|
defineTool({
|
|
445
501
|
name: "lsp_document_symbols",
|
|
446
502
|
label: "LSP: document symbols",
|
|
503
|
+
renderResult: render_lsp_result,
|
|
447
504
|
description:
|
|
448
505
|
"List symbols in a file (functions, classes, variables) using the language server.",
|
|
449
506
|
promptSnippet: "List functions, classes, and variables in a file",
|
|
@@ -479,6 +536,7 @@ export function register_lsp_tools(pi: ExtensionAPI, manager: LspServerManager)
|
|
|
479
536
|
defineTool({
|
|
480
537
|
name: "lsp_rename",
|
|
481
538
|
label: "LSP: rename symbol",
|
|
539
|
+
renderResult: render_lsp_result,
|
|
482
540
|
description:
|
|
483
541
|
"Rename a symbol at a position. Returns all locations that need to be updated with the new name. Use the edit tool to apply the changes.",
|
|
484
542
|
promptSnippet: "Compute symbol rename updates across affected files",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Model Integration — 模型集成
|
|
3
3
|
*
|
|
4
4
|
* 对外接口:
|
|
5
5
|
* analyzeImage(model, base64, mediaType, apiKey, headers) → Promise<string>
|
|
@@ -22,13 +22,14 @@ import {
|
|
|
22
22
|
} from "@earendil-works/pi-tui";
|
|
23
23
|
import OpenAI from "openai";
|
|
24
24
|
import { fileTypeFromFile } from "file-type";
|
|
25
|
-
import type
|
|
25
|
+
import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
|
|
26
26
|
import {
|
|
27
27
|
loadConfig, saveConfig, parseModelKey, formatModelKey,
|
|
28
28
|
getImageModelKey, getCompactModelKey,
|
|
29
29
|
setImageModelKey, setCompactModelKey,
|
|
30
30
|
} from "./settings.js";
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
|
+
import * as os from "node:os";
|
|
32
33
|
import { extname, resolve } from "node:path";
|
|
33
34
|
|
|
34
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -349,15 +350,134 @@ function getConfiguredCompactModel(registry: any): Model<any> | null {
|
|
|
349
350
|
return registry.find(parsed.provider, parsed.modelId) ?? null;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
interface PiCompactionSettings {
|
|
354
|
+
enabled: boolean;
|
|
355
|
+
reserveTokens: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface AutoCompactionCandidate {
|
|
359
|
+
messages: any[];
|
|
360
|
+
usage: { tokens: number | null; contextWindow: number } | undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const DEFAULT_PI_COMPACTION_SETTINGS: PiCompactionSettings = {
|
|
364
|
+
enabled: true,
|
|
365
|
+
reserveTokens: 16_384,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
function readJsonObject(filePath: string): any | undefined {
|
|
369
|
+
try {
|
|
370
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
371
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
372
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
373
|
+
} catch {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function loadPiCompactionSettings(cwd: string): PiCompactionSettings {
|
|
379
|
+
const globalSettings = readJsonObject(resolve(os.homedir(), ".pi", "agent", "settings.json"));
|
|
380
|
+
const projectSettings = readJsonObject(resolve(cwd, ".pi", "settings.json"));
|
|
381
|
+
const merged = {
|
|
382
|
+
...DEFAULT_PI_COMPACTION_SETTINGS,
|
|
383
|
+
...(globalSettings?.compaction ?? {}),
|
|
384
|
+
...(projectSettings?.compaction ?? {}),
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
enabled: merged.enabled !== false,
|
|
388
|
+
reserveTokens: typeof merged.reserveTokens === "number" ? merged.reserveTokens : DEFAULT_PI_COMPACTION_SETTINGS.reserveTokens,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getLastAssistantMessage(messages: any[]): any | undefined {
|
|
393
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
394
|
+
if (messages[i]?.role === "assistant") return messages[i];
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function shouldExpectAutoCompaction(
|
|
400
|
+
messages: any[],
|
|
401
|
+
usage: { tokens: number | null; contextWindow: number } | undefined,
|
|
402
|
+
settings: PiCompactionSettings,
|
|
403
|
+
): boolean {
|
|
404
|
+
if (!settings.enabled) return false;
|
|
405
|
+
|
|
406
|
+
const lastAssistant = getLastAssistantMessage(messages);
|
|
407
|
+
if (!lastAssistant) return false;
|
|
408
|
+
|
|
409
|
+
const contextWindow = usage?.contextWindow ?? 0;
|
|
410
|
+
if (contextWindow > 0 && isContextOverflow(lastAssistant, contextWindow)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!usage || usage.tokens === null) return false;
|
|
415
|
+
return usage.tokens > usage.contextWindow - settings.reserveTokens;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function shouldAutoResumeCompaction(
|
|
419
|
+
prePromptCompactionPending: boolean,
|
|
420
|
+
postAgentEndCandidate: AutoCompactionCandidate | null,
|
|
421
|
+
settings: PiCompactionSettings,
|
|
422
|
+
customInstructions?: string,
|
|
423
|
+
): boolean {
|
|
424
|
+
if (customInstructions !== undefined) return false;
|
|
425
|
+
if (prePromptCompactionPending) return true;
|
|
426
|
+
if (!postAgentEndCandidate) return false;
|
|
427
|
+
return shouldExpectAutoCompaction(postAgentEndCandidate.messages, postAgentEndCandidate.usage, settings);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const __modelIntegrationTest = {
|
|
431
|
+
shouldExpectAutoCompaction,
|
|
432
|
+
shouldAutoResumeCompaction,
|
|
433
|
+
};
|
|
434
|
+
|
|
352
435
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
353
436
|
// 主入口(注册所有事件)
|
|
354
437
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
355
438
|
|
|
356
|
-
export function
|
|
439
|
+
export function setupModelIntegration(pi: ExtensionAPI) {
|
|
357
440
|
setupImageReadFallback(pi);
|
|
358
441
|
|
|
442
|
+
let prePromptCompactionPending = false;
|
|
443
|
+
let postAgentEndCandidate: AutoCompactionCandidate | null = null;
|
|
444
|
+
let currentCompactionIsAuto = false;
|
|
445
|
+
|
|
446
|
+
pi.on("input", () => {
|
|
447
|
+
prePromptCompactionPending = true;
|
|
448
|
+
postAgentEndCandidate = null;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
pi.on("before_agent_start", () => {
|
|
452
|
+
prePromptCompactionPending = false;
|
|
453
|
+
postAgentEndCandidate = null;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
pi.on("agent_start", () => {
|
|
457
|
+
prePromptCompactionPending = false;
|
|
458
|
+
postAgentEndCandidate = null;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
pi.on("agent_end", (event, ctx) => {
|
|
462
|
+
prePromptCompactionPending = false;
|
|
463
|
+
postAgentEndCandidate = {
|
|
464
|
+
messages: event.messages,
|
|
465
|
+
usage: ctx.getContextUsage(),
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
|
|
359
469
|
// 自定义压缩模型
|
|
360
470
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
471
|
+
const compactionSettings = loadPiCompactionSettings(ctx.cwd);
|
|
472
|
+
currentCompactionIsAuto = shouldAutoResumeCompaction(
|
|
473
|
+
prePromptCompactionPending,
|
|
474
|
+
postAgentEndCandidate,
|
|
475
|
+
compactionSettings,
|
|
476
|
+
event.customInstructions,
|
|
477
|
+
);
|
|
478
|
+
prePromptCompactionPending = false;
|
|
479
|
+
postAgentEndCandidate = null;
|
|
480
|
+
|
|
361
481
|
const model = getConfiguredCompactModel(ctx.modelRegistry);
|
|
362
482
|
if (!model) return; // 没配 → Pi 默认
|
|
363
483
|
|
|
@@ -394,8 +514,11 @@ export function setupExtendModel(pi: ExtensionAPI) {
|
|
|
394
514
|
}
|
|
395
515
|
});
|
|
396
516
|
|
|
397
|
-
//
|
|
517
|
+
// 压缩后自动继续(仅自动压缩)
|
|
398
518
|
pi.on("session_compact", () => {
|
|
519
|
+
const shouldResume = currentCompactionIsAuto;
|
|
520
|
+
currentCompactionIsAuto = false;
|
|
521
|
+
if (!shouldResume) return;
|
|
399
522
|
pi.sendMessage({
|
|
400
523
|
customType: "auto_compact_resume",
|
|
401
524
|
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.",
|