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.
@@ -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
- * Extend Model — 模型 SDK
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 { Model } from "@earendil-works/pi-ai";
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 setupExtendModel(pi: ExtensionAPI) {
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.",