pi-ui-extend 0.1.3 → 0.1.5

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.
Files changed (43) hide show
  1. package/dist/app/app.d.ts +1 -1
  2. package/dist/app/app.js +14 -4
  3. package/dist/app/clipboard.d.ts +1 -0
  4. package/dist/app/clipboard.js +16 -0
  5. package/dist/app/conversation-entry-renderer.d.ts +0 -1
  6. package/dist/app/conversation-entry-renderer.js +2 -6
  7. package/dist/app/conversation-tool-renderer.js +2 -3
  8. package/dist/app/conversation-viewport.d.ts +0 -1
  9. package/dist/app/conversation-viewport.js +0 -1
  10. package/dist/app/startup-info.d.ts +1 -1
  11. package/dist/app/startup-info.js +3 -2
  12. package/dist/app/update.d.ts +2 -0
  13. package/dist/app/update.js +16 -0
  14. package/dist/config.d.ts +0 -3
  15. package/dist/config.js +0 -79
  16. package/dist/default-pix-config.js +2 -2
  17. package/dist/markdown-format.js +18 -1
  18. package/external/pi-tools-suite/README.md +4 -4
  19. package/external/pi-tools-suite/licenses/opencode-dynamic-context-pruning-AGPL-3.0.txt +619 -0
  20. package/external/pi-tools-suite/package.json +1 -1
  21. package/external/pi-tools-suite/src/config.ts +5 -1
  22. package/external/pi-tools-suite/src/{compress → dcp}/config.ts +10 -70
  23. package/external/pi-tools-suite/src/{compress → dcp}/index.ts +16 -66
  24. package/external/pi-tools-suite/src/dcp/ui.ts +45 -0
  25. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -2
  26. package/external/pi-tools-suite/src/index.ts +1 -1
  27. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  28. package/package.json +1 -1
  29. package/external/pi-tools-suite/src/compress/dcp-tui-filter.ts +0 -498
  30. package/external/pi-tools-suite/src/compress/ui.ts +0 -308
  31. /package/external/pi-tools-suite/src/{compress → dcp}/commands.ts +0 -0
  32. /package/external/pi-tools-suite/src/{compress → dcp}/compress-tool.ts +0 -0
  33. /package/external/pi-tools-suite/src/{compress → dcp}/compression-blocks.ts +0 -0
  34. /package/external/pi-tools-suite/src/{compress → dcp}/prompts.ts +0 -0
  35. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-candidates.ts +0 -0
  36. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-compression-blocks.ts +0 -0
  37. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-message-ids.ts +0 -0
  38. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-metadata.ts +0 -0
  39. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-nudge.ts +0 -0
  40. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-tools.ts +0 -0
  41. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-types.ts +0 -0
  42. /package/external/pi-tools-suite/src/{compress → dcp}/pruner.ts +0 -0
  43. /package/external/pi-tools-suite/src/{compress → dcp}/state.ts +0 -0
package/dist/app/app.d.ts CHANGED
@@ -19,7 +19,6 @@ export declare class PiUiExtendApp {
19
19
  private readonly extensionActions;
20
20
  private readonly pixConfig;
21
21
  private readonly outputFilters;
22
- private readonly suppressPendingDcpIdMetadata;
23
22
  private readonly commandController;
24
23
  private readonly inputActions;
25
24
  private readonly inputController;
@@ -57,6 +56,7 @@ export declare class PiUiExtendApp {
57
56
  private resumeLoading;
58
57
  constructor(options: AppOptions);
59
58
  start(): Promise<void>;
59
+ private checkPixUpdateOnStartup;
60
60
  private bindCurrentSession;
61
61
  private activateRuntime;
62
62
  private createExtensionEventBus;
package/dist/app/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { THEMES } from "../theme.js";
2
2
  import { InputEditor } from "../input-editor.js";
3
- import { compileOutputFilterPatterns, loadPixConfig, outputFiltersRemoveDcpIdMetadataLine, resolveToolRule, } from "../config.js";
3
+ import { compileOutputFilterPatterns, loadPixConfig, resolveToolRule, } from "../config.js";
4
4
  import { AppCommandController } from "./command-controller.js";
5
5
  import { ConversationViewport } from "./conversation-viewport.js";
6
6
  import { EditorLayoutRenderer } from "./editor-layout-renderer.js";
@@ -38,6 +38,7 @@ import { AppTabsController } from "./tabs-controller.js";
38
38
  import { TabLineRenderer } from "./tab-line-renderer.js";
39
39
  import { AppTerminalController } from "./terminal-controller.js";
40
40
  import { AppToastController } from "./toast-controller.js";
41
+ import { checkPixUpdate, formatPixStartupUpdateDialog } from "./update.js";
41
42
  import { AppVoiceController } from "./voice-controller.js";
42
43
  import { createIsolatedExtensionEventBus } from "./extension-event-bus.js";
43
44
  import { setAppIconTheme } from "./icons.js";
@@ -64,7 +65,6 @@ export class PiUiExtendApp {
64
65
  extensionActions;
65
66
  pixConfig;
66
67
  outputFilters;
67
- suppressPendingDcpIdMetadata;
68
68
  commandController;
69
69
  inputActions;
70
70
  inputController;
@@ -367,7 +367,6 @@ export class PiUiExtendApp {
367
367
  suppressExtensionWidget: (key) => this.extensionUiController.suppressWidget(key),
368
368
  });
369
369
  this.outputFilters = compileOutputFilterPatterns(this.pixConfig.outputFilters.patterns);
370
- this.suppressPendingDcpIdMetadata = outputFiltersRemoveDcpIdMetadataLine(this.outputFilters);
371
370
  this.conversationViewport = new ConversationViewport({
372
371
  get entries() { return app.entries; },
373
372
  get session() { return app.runtime?.session; },
@@ -379,7 +378,6 @@ export class PiUiExtendApp {
379
378
  colors: this.theme.colors,
380
379
  pixConfig: this.pixConfig,
381
380
  outputFilters: this.outputFilters,
382
- suppressPendingDcpIdMetadata: this.suppressPendingDcpIdMetadata,
383
381
  hasDynamicConversationBlock: () => this.popupMenus.hasDynamicConversationBlock(),
384
382
  isDynamicConversationBlock: (entry) => this.popupMenus.isDynamicConversationBlock(entry),
385
383
  renderInlineUserMessageMenu: (entry, context) => this.popupMenus.renderInlineUserMessageMenu(entry, context),
@@ -677,6 +675,18 @@ export class PiUiExtendApp {
677
675
  await this.sessionLifecycle.start();
678
676
  this.modelUsageController.startPolling();
679
677
  this.nerdFontController.ensureInstalledOnStartup();
678
+ void this.checkPixUpdateOnStartup();
679
+ }
680
+ async checkPixUpdateOnStartup() {
681
+ try {
682
+ const result = await checkPixUpdate();
683
+ if (result.status !== "newer")
684
+ return;
685
+ this.showToast(formatPixStartupUpdateDialog(result), "warning", { variant: "dialog" });
686
+ }
687
+ catch {
688
+ // Startup update checks should never interrupt the TUI.
689
+ }
680
690
  }
681
691
  async bindCurrentSession() {
682
692
  await this.sessionLifecycle.bindCurrentSession();
@@ -1,3 +1,4 @@
1
1
  export declare function copyTextToClipboard(text: string): void;
2
2
  export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): boolean;
3
3
  export declare function clipboardInstallHint(): string;
4
+ export declare function osc52ClipboardSequence(text: string, env?: NodeJS.ProcessEnv): string;
@@ -10,6 +10,8 @@ export function copyTextToClipboard(text) {
10
10
  }
11
11
  if (copyWithNativeClipboard(text))
12
12
  return;
13
+ if (copyWithOsc52(text))
14
+ return;
13
15
  throw new Error(`No clipboard command found. ${clipboardInstallHint()}`);
14
16
  }
15
17
  export function clipboardSupportAvailable(env = process.env) {
@@ -60,6 +62,20 @@ function copyWithNativeClipboard(text) {
60
62
  });
61
63
  return !result.error && result.status === 0;
62
64
  }
65
+ function copyWithOsc52(text) {
66
+ if (process.stdout.destroyed || (!process.stdout.isTTY && !process.env.TMUX && !process.env.STY))
67
+ return false;
68
+ process.stdout.write(osc52ClipboardSequence(text));
69
+ return true;
70
+ }
71
+ export function osc52ClipboardSequence(text, env = process.env) {
72
+ const sequence = `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
73
+ if (env.TMUX)
74
+ return `\x1bPtmux;${sequence.replaceAll("\x1b", "\x1b\x1b")}\x1b\\`;
75
+ if (env.STY)
76
+ return `\x1bP${sequence}\x1b\\`;
77
+ return sequence;
78
+ }
63
79
  function resolveNativeClipboardEntrypoint() {
64
80
  try {
65
81
  return require.resolve("@mariozechner/clipboard");
@@ -11,7 +11,6 @@ export type ConversationEntryRenderOptions = {
11
11
  colors: Theme["colors"];
12
12
  pixConfig: PixConfig;
13
13
  outputFilters: readonly RegExp[];
14
- suppressPendingDcpIdMetadata: boolean;
15
14
  superCompactTools?: boolean;
16
15
  allThinkingExpanded?: boolean;
17
16
  renderInlineUserMessageMenu: (entry: Extract<Entry, {
@@ -1,4 +1,4 @@
1
- import { applyOutputFilters, stripDcpDisplayMetadata, suppressPendingDcpIdMetadataLine } from "../config.js";
1
+ import { applyOutputFilters } from "../config.js";
2
2
  import { renderMarkdownTextLines } from "../markdown-format.js";
3
3
  import { attachImageClickTargets } from "./image-click-targets.js";
4
4
  import { horizontalPaddingLayout, padHorizontalText, wrapText } from "./render-text.js";
@@ -65,7 +65,7 @@ function renderCustomEntry(entry, width) {
65
65
  }));
66
66
  }
67
67
  function renderAssistantLines(text, width, options) {
68
- const displayText = displayAssistantText(text, options.outputFilters, options.suppressPendingDcpIdMetadata);
68
+ const displayText = applyOutputFilters(text, options.outputFilters).trimEnd();
69
69
  if (!displayText)
70
70
  return [];
71
71
  return renderMarkdownTextLines(displayText, width).map((line) => ({
@@ -75,7 +75,3 @@ function renderAssistantLines(text, width, options) {
75
75
  ...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
76
76
  }));
77
77
  }
78
- function displayAssistantText(text, outputFilters, suppressPendingDcpIdMetadata) {
79
- const filtered = stripDcpDisplayMetadata(applyOutputFilters(text, outputFilters)).trimEnd();
80
- return suppressPendingDcpIdMetadata ? suppressPendingDcpIdMetadataLine(filtered) : filtered;
81
- }
@@ -1,4 +1,4 @@
1
- import { resolveColor, resolveToolRule, stripDcpDisplayMetadata } from "../config.js";
1
+ import { resolveColor, resolveToolRule } from "../config.js";
2
2
  import { formatMarkdownTables, markdownSyntaxHighlightsForText } from "../markdown-format.js";
3
3
  import { renderToolDisplay } from "../tool-renderers/index.js";
4
4
  import { DEFAULT_THINKING_TOOL_RULE, SUBAGENT_STATUSES, THINKING_TOOL_NAME, TODO_TOOL_NAME } from "./constants.js";
@@ -47,8 +47,7 @@ export function renderConversationToolEntry(entry, width, options) {
47
47
  }
48
48
  export function renderThinkingEntry(entry, width, options) {
49
49
  const rule = resolveThinkingToolRule(options.pixConfig);
50
- const displayText = stripDcpDisplayMetadata(entry.text);
51
- const markdownText = displayText ? formatMarkdownTables(displayText, Math.max(1, width - 2)) : "";
50
+ const markdownText = entry.text ? formatMarkdownTables(entry.text, Math.max(1, width - 2)) : "";
52
51
  const expandedText = trimTrailingBlankLines(markdownText);
53
52
  const compactExpandedText = options.superCompactTools ? removeBlankLines(expandedText) : expandedText;
54
53
  const forceExpanded = Boolean(options.allThinkingExpanded);
@@ -12,7 +12,6 @@ export type ConversationViewportHost = {
12
12
  readonly colors: Theme["colors"];
13
13
  readonly pixConfig: PixConfig;
14
14
  readonly outputFilters: readonly RegExp[];
15
- readonly suppressPendingDcpIdMetadata: boolean;
16
15
  readonly superCompactTools?: boolean;
17
16
  readonly allThinkingExpanded?: boolean;
18
17
  hasDynamicConversationBlock?(): boolean;
@@ -65,7 +65,6 @@ export class ConversationViewport {
65
65
  colors: this.host.colors,
66
66
  pixConfig: this.host.pixConfig,
67
67
  outputFilters: this.host.outputFilters,
68
- suppressPendingDcpIdMetadata: this.host.suppressPendingDcpIdMetadata,
69
68
  superCompactTools: Boolean(this.host.superCompactTools),
70
69
  allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
71
70
  renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
@@ -1,3 +1,3 @@
1
- import { type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
2
  export declare function createStartupInfoMessage(runtime: AgentSessionRuntime): string;
3
3
  export declare function isEmptyStartupSession(runtime: AgentSessionRuntime): boolean;
@@ -1,11 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { basename, isAbsolute, relative, sep } from "node:path";
3
- import { VERSION } from "@earendil-works/pi-coding-agent";
3
+ import { getPixPackageVersion } from "./update.js";
4
4
  export function createStartupInfoMessage(runtime) {
5
5
  const sections = startupSections(runtime);
6
6
  return [
7
7
  formatModelLine(runtime),
8
- `pix · pi-sdk v${VERSION}`,
8
+ "",
9
+ `pix v${getPixPackageVersion()}`,
9
10
  "escape interrupt · ctrl+c/ctrl+d clear/exit · / commands",
10
11
  "",
11
12
  ...sections.flatMap(formatSection),
@@ -30,7 +30,9 @@ export type PixUpdateCheckOptions = {
30
30
  };
31
31
  export declare function pixUpdateUsage(): string;
32
32
  export declare function parsePixUpdateArgs(argv: readonly string[]): PixUpdateCliOptions;
33
+ export declare function getPixPackageVersion(packageRoot?: string): string;
33
34
  export declare function checkPixUpdate(options?: PixUpdateCheckOptions): Promise<PixUpdateCheckResult>;
34
35
  export declare function formatPixUpdateCheck(result: PixUpdateCheckResult): string;
36
+ export declare function formatPixStartupUpdateDialog(result: PixUpdateCheckResult): string;
35
37
  export declare function getPixSelfUpdateCommand(packageName: string, latestVersion?: string, packageRoot?: string): PixSelfUpdateCommand | undefined;
36
38
  export declare function runPixUpdateCli(argv?: readonly string[]): Promise<number>;
@@ -40,6 +40,9 @@ export function parsePixUpdateArgs(argv) {
40
40
  }
41
41
  return { checkOnly, force, help };
42
42
  }
43
+ export function getPixPackageVersion(packageRoot) {
44
+ return readPixPackageInfo(packageRoot).version;
45
+ }
43
46
  export async function checkPixUpdate(options = {}) {
44
47
  const packageInfo = readPixPackageInfo(options.packageRoot);
45
48
  const base = {
@@ -107,6 +110,19 @@ export function formatPixUpdateCheck(result) {
107
110
  lines.push("scope: Pix package, renderer extensions, bundled skills copied into ~/.agents/skills, and the pi-tools-suite payload linked into ~/.pi/agent/extensions");
108
111
  return lines.join("\n");
109
112
  }
113
+ export function formatPixStartupUpdateDialog(result) {
114
+ const lines = [
115
+ "A new Pix version is available.",
116
+ `current: ${result.packageName} v${result.currentVersion}`,
117
+ ...(result.latestVersion ? [`latest: ${result.latestVersion}`] : []),
118
+ "",
119
+ "To update:",
120
+ "1. Exit Pix.",
121
+ "2. Run `pix update` in your shell.",
122
+ "3. Start Pix again after the update completes.",
123
+ ];
124
+ return lines.join("\n");
125
+ }
110
126
  export function getPixSelfUpdateCommand(packageName, latestVersion, packageRoot = readPixPackageInfo().packageRoot) {
111
127
  if (!packageRootLooksPackageManaged(packageRoot))
112
128
  return undefined;
package/dist/config.d.ts CHANGED
@@ -54,9 +54,6 @@ export declare function savePixDictationLanguage(language: string): void;
54
54
  export declare function upsertPixDictationLanguageInJsonc(source: string, language: string): string;
55
55
  export declare function resolveModelColor(modelRef: string, config: ModelColorsConfig): string | undefined;
56
56
  export declare function compileOutputFilterPatterns(patterns: readonly string[]): RegExp[];
57
- export declare function stripDcpDisplayMetadata(text: string): string;
58
57
  export declare function applyOutputFilters(text: string, filters: readonly RegExp[]): string;
59
- export declare function outputFiltersRemoveDcpIdMetadataLine(filters: readonly RegExp[]): boolean;
60
- export declare function suppressPendingDcpIdMetadataLine(text: string): string;
61
58
  export declare function resolveToolRule(toolName: string, config: ToolRendererConfig): ResolvedToolRule;
62
59
  export declare function resolveColor(colorRef: string, themeColors: Record<string, string>): string;
package/dist/config.js CHANGED
@@ -77,17 +77,6 @@ const DEFAULT_DICTATION = {
77
77
  },
78
78
  },
79
79
  };
80
- const DCP_ID_METADATA_SAMPLE = "<dcp-id>m001</dcp-id>";
81
- const DCP_ID_METADATA_PREFIX = "<dcp-id>m";
82
- const DCP_ID_METADATA_SUFFIX = "</dcp-id>";
83
- const DCP_XML_PAIRED_TAG_RE = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
84
- const DCP_XML_OPEN_TAG_TO_END_RE = /<dcp[^>]*>[\s\S]*$/gi;
85
- const DCP_XML_UNPAIRED_TAG_RE = /<\/?dcp[^>]*>/gi;
86
- const DCP_MARKDOWN_REFERENCE_RE = /[ \t]*\[dcp(?:-[a-z0-9-]+)?\]:[ \t]*#(?:[ \t]+\([^\n]*\))?[ \t]*/gi;
87
- const DCP_MARKDOWN_REFERENCE_LINE_RE = /^[ \t]*\[dcp(?:-[a-z0-9-]+)?\]:[ \t]*#(?:[ \t]+\([^\n]*\))?[ \t]*$/i;
88
- const DCP_MARKDOWN_REFERENCE_PENDING_RE = /^\[d(?:c(?:p(?:-[a-z0-9-]*)?)?)?(?:\]?(?::[ \t]*#?(?:[ \t]*\([^\)\n]*)?)?)?$/i;
89
- const DCP_XML_METADATA_LINE_RE = /^[ \t]*<dcp[^>]*>(?:[\s\S]*?<\/dcp[^>]*>)?[ \t]*$/i;
90
- const DCP_DISPLAY_QUICK_CHECK_RE = /<\/?d(?:c(?:p)?)?|\[d(?:c(?:p)?)?/i;
91
80
  function parseJsonc(text) {
92
81
  return parse(text, undefined, { allowTrailingComma: true });
93
82
  }
@@ -322,37 +311,6 @@ export function resolveModelColor(modelRef, config) {
322
311
  export function compileOutputFilterPatterns(patterns) {
323
312
  return patterns.flatMap((pattern) => compileOutputFilterPattern(pattern));
324
313
  }
325
- export function stripDcpDisplayMetadata(text) {
326
- if (text.length === 0 || !DCP_DISPLAY_QUICK_CHECK_RE.test(text))
327
- return text;
328
- let cleaned = stripDcpDisplayMetadataLines(text);
329
- // Strip fully paired XML-style DCP tags first. During streaming, strip an
330
- // unterminated opening XML tag and everything after it before removing
331
- // orphan tags, otherwise `<dcp-id>m123` would leave `m123` behind.
332
- cleaned = cleaned
333
- .replace(DCP_XML_PAIRED_TAG_RE, "")
334
- .replace(DCP_XML_OPEN_TAG_TO_END_RE, "")
335
- .replace(DCP_XML_UNPAIRED_TAG_RE, "");
336
- // Hide a partially streamed markdown reference line before the complete-line
337
- // regex can strip the prefix and strand the `(m123` payload.
338
- cleaned = suppressPendingDcpIdMetadataLine(cleaned).replace(DCP_MARKDOWN_REFERENCE_RE, "");
339
- cleaned = suppressPendingDcpIdMetadataLine(cleaned);
340
- cleaned = stripDcpDisplayMetadataLines(cleaned);
341
- return cleaned.replace(/\n{3,}/g, "\n\n").trimEnd();
342
- }
343
- function stripDcpDisplayMetadataLines(text) {
344
- if (text.length === 0)
345
- return text;
346
- let removed = false;
347
- const keptLines = text.split("\n").filter((line) => {
348
- const normalizedLine = line.replace(/\r$/u, "");
349
- const isMetadataLine = DCP_MARKDOWN_REFERENCE_LINE_RE.test(normalizedLine) || DCP_XML_METADATA_LINE_RE.test(normalizedLine);
350
- if (isMetadataLine)
351
- removed = true;
352
- return !isMetadataLine;
353
- });
354
- return removed ? keptLines.join("\n") : text;
355
- }
356
314
  export function applyOutputFilters(text, filters) {
357
315
  if (filters.length === 0 || text.length === 0)
358
316
  return text;
@@ -374,43 +332,6 @@ export function applyOutputFilters(text, filters) {
374
332
  }
375
333
  return filteredLines.join("\n");
376
334
  }
377
- export function outputFiltersRemoveDcpIdMetadataLine(filters) {
378
- return filters.length > 0 && applyOutputFilters(DCP_ID_METADATA_SAMPLE, filters).length === 0;
379
- }
380
- export function suppressPendingDcpIdMetadataLine(text) {
381
- if (text.length === 0)
382
- return text;
383
- const lineStart = text.lastIndexOf("\n") + 1;
384
- const line = text.slice(lineStart);
385
- if (!isPendingDcpIdMetadataLine(line))
386
- return text;
387
- // Hide the still-streaming metadata line and its line break until it either
388
- // becomes a complete filtered line or diverges from the metadata prefix.
389
- return lineStart > 0 ? text.slice(0, lineStart - 1) : "";
390
- }
391
- function isPendingDcpIdMetadataLine(line) {
392
- const candidate = line.trimStart();
393
- if (candidate.length === 0)
394
- return false;
395
- return isPendingXmlDcpIdMetadataLine(candidate) || isPendingMarkdownDcpMetadataLine(candidate);
396
- }
397
- function isPendingXmlDcpIdMetadataLine(candidate) {
398
- if (DCP_ID_METADATA_PREFIX.startsWith(candidate))
399
- return true;
400
- if (!candidate.startsWith(DCP_ID_METADATA_PREFIX))
401
- return false;
402
- const afterPrefix = candidate.slice(DCP_ID_METADATA_PREFIX.length);
403
- const digits = afterPrefix.match(/^\d*/)?.[0] ?? "";
404
- const afterDigits = afterPrefix.slice(digits.length);
405
- if (afterDigits.length === 0)
406
- return true;
407
- return DCP_ID_METADATA_SUFFIX.startsWith(afterDigits) && afterDigits.length < DCP_ID_METADATA_SUFFIX.length;
408
- }
409
- function isPendingMarkdownDcpMetadataLine(candidate) {
410
- if (DCP_MARKDOWN_REFERENCE_LINE_RE.test(candidate))
411
- return false;
412
- return DCP_MARKDOWN_REFERENCE_PENDING_RE.test(candidate);
413
- }
414
335
  function applyOutputFiltersToLine(line, filters) {
415
336
  let filtered = line;
416
337
  for (const filter of filters) {
@@ -169,10 +169,10 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
169
169
  },
170
170
 
171
171
  // Output filters applied to assistant text in the renderer.
172
- // Supports glob-style * wildcards, or regex literals like "/<dcp-id>m\\d+<\\/dcp-id>/".
172
+ // Supports glob-style * wildcards, or regex literals like "/token=\\w+/".
173
173
  // "outputFilters": {
174
174
  // "patterns": [
175
- // "<dcp-id>m*</dcp-id>"
175
+ // "secret-*"
176
176
  // ]
177
177
  // },
178
178
 
@@ -4,6 +4,7 @@ export function formatMarkdownTables(text, maxWidth) {
4
4
  const lines = text.split("\n");
5
5
  const formatted = [];
6
6
  let fence;
7
+ let skipBlankAfterHiddenReference = false;
7
8
  for (let index = 0; index < lines.length;) {
8
9
  const line = lines[index] ?? "";
9
10
  const nextFence = markdownFence(line);
@@ -16,6 +17,16 @@ export function formatMarkdownTables(text, maxWidth) {
16
17
  index += 1;
17
18
  continue;
18
19
  }
20
+ if (!fence && isMarkdownReferenceDefinition(line)) {
21
+ skipBlankAfterHiddenReference = formatted.length === 0 || (formatted.at(-1) ?? "").trim().length === 0;
22
+ index += 1;
23
+ continue;
24
+ }
25
+ if (!fence && skipBlankAfterHiddenReference && line.trim().length === 0) {
26
+ index += 1;
27
+ continue;
28
+ }
29
+ skipBlankAfterHiddenReference = false;
19
30
  if (!fence) {
20
31
  const table = parseMarkdownTableBlock(lines, index);
21
32
  if (table) {
@@ -61,7 +72,10 @@ export function renderMarkdownLine(text, start = 0) {
61
72
  export function renderMarkdownTextLines(text, width, start = 0) {
62
73
  const lines = [];
63
74
  let fence;
64
- for (const rawLine of formatMarkdownTables(sanitizeMarkdownText(text), width).split("\n")) {
75
+ const formattedText = formatMarkdownTables(sanitizeMarkdownText(text), width);
76
+ if (formattedText.length === 0)
77
+ return [];
78
+ for (const rawLine of formattedText.split("\n")) {
65
79
  const nextFence = markdownFence(rawLine);
66
80
  const closesFence = Boolean(fence && nextFence && fence.marker === nextFence.marker && nextFence.length >= fence.length);
67
81
  const opensFence = !fence && nextFence !== undefined;
@@ -533,6 +547,9 @@ function markdownLineSyntaxHighlight(fence, fenceDelimiterLine, start) {
533
547
  function sanitizeMarkdownText(text) {
534
548
  return expandTabs(text.replace(/\x1b/g, "␛").replace(/\r/g, ""));
535
549
  }
550
+ function isMarkdownReferenceDefinition(line) {
551
+ return /^ {0,3}\[[^\]\n]+\]:[ \t]*\S.*$/u.test(line);
552
+ }
536
553
  function markdownFence(line) {
537
554
  const match = /^\s{0,3}(`{3,}|~{3,})(.*)$/.exec(line);
538
555
  const marker = match?.[1];
@@ -14,12 +14,12 @@ This package keeps the former standalone extensions as ordinary source folders u
14
14
  - `src/model-tools` — model-specific tool aliases such as Claude/GLM-style `Read` / `Edit` / `Write` / `Bash` / `Grep` / `Glob` / `LS`, GPT/Codex-style `shell`, and model-gated `apply_patch`
15
15
  - `src/usage` — `/usage` command and startup hint for read-only AI quota checks across OpenAI, Zhipu AI, Z.ai, and Google Antigravity, including Antigravity quota by model
16
16
  - `src/web-search` — `web_search` and `web_fetch` tools migrated from `@ollama/pi-web-search`; calls the local Ollama experimental web search/fetch APIs, honors `OLLAMA_HOST`, supports request timeouts via `timeout_ms` / `PI_WEB_SEARCH_TIMEOUT_MS`, and reports targeted `ollama signin`, unsupported-endpoint, invalid-response, timeout, DNS, and Ollama-not-running errors
17
- - `src/compress` — Dynamic Context Pruning: explicit `compress` tool with range and message modes, `/dcp` commands (context, stats, sweep, manual, decompress, recompress, compress), same-call overlap validation, recoverable compressed-block rollups, grouped message-mode skip diagnostics, stable raw-message anchors when available, protected user/tool preservation, deduplication, error purging, context nudges, and footer status visualization
17
+ - `src/dcp` — headless Dynamic Context Pruning ported from `opencode-dynamic-context-pruning` for the Pi SDK: explicit `compress` tool with range and message modes, `/dcp` commands (context, stats, sweep, manual, decompress, recompress, compress), same-call overlap validation, recoverable compressed-block rollups, grouped message-mode skip diagnostics, stable raw-message anchors when available, protected user/tool preservation, deduplication, error purging, and context nudges; visualization is left to `compress` tool responses and the renderer-owned context-percent click dialog
18
18
  - `src/prompt-commands` — user slash-command builder: `/prompt-commands` opens a CRUD menu for saved prompt-backed slash commands, stores them under `promptCommands` in `~/.config/pi/pi-tools-suite.jsonc`, reloads after edits, and runs each saved prompt as a normal user message
19
19
 
20
20
  `index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.
21
21
 
22
- Registration order is preserved in `src/index.ts`: ast-grep, async-subagents, terminal-bell, lsp, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, compress, then prompt-commands. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
22
+ Registration order is preserved in `src/index.ts`: ast-grep, async-subagents, terminal-bell, lsp, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, then prompt-commands. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
23
23
 
24
24
  ## Disabling modules
25
25
 
@@ -55,7 +55,7 @@ Saved prompt slash commands are stored under `promptCommands`. Use `/prompt-comm
55
55
  }
56
56
  ```
57
57
 
58
- DCP/compress settings are stored under `dcp` in the same shared config files. Legacy standalone `dcp.jsonc` files are still read for compatibility, but the `dcp` section in `pi-tools-suite.jsonc` wins at the same global/env/project layer.
58
+ DCP settings are stored only under `dcp` in the user shared config file `~/.config/pi/pi-tools-suite.jsonc`. Legacy standalone `dcp.jsonc`, `$PI_CONFIG_DIR`, and project-local `.pi/pi-tools-suite.jsonc` DCP settings are intentionally ignored by the ported headless DCP module.
59
59
 
60
60
  ```jsonc
61
61
  {
@@ -214,7 +214,7 @@ pi-tools-suite/
214
214
  model-tools/
215
215
  usage/
216
216
  web-search/
217
- compress/
217
+ dcp/
218
218
  prompt-commands/
219
219
  docs/
220
220
  licenses/