pi-ui-extend 0.1.56 → 0.1.59

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 (44) hide show
  1. package/dist/app/app.js +2 -0
  2. package/dist/app/commands/command-session-actions.js +3 -2
  3. package/dist/app/extensions/extension-actions-controller.js +1 -3
  4. package/dist/app/rendering/conversation-entry-renderer.d.ts +3 -0
  5. package/dist/app/rendering/conversation-entry-renderer.js +2 -0
  6. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  7. package/dist/app/rendering/conversation-viewport.js +4 -0
  8. package/dist/app/rendering/extension-entry-renderer.d.ts +6 -0
  9. package/dist/app/rendering/extension-entry-renderer.js +100 -0
  10. package/dist/app/rendering/tool-block-renderer.d.ts +5 -0
  11. package/dist/app/rendering/tool-block-renderer.js +1 -1
  12. package/dist/app/screen/mouse-controller.js +1 -1
  13. package/dist/app/session/lazy-session-manager.js +18 -1
  14. package/dist/app/session/pix-system-message.d.ts +1 -0
  15. package/dist/app/session/pix-system-message.js +4 -0
  16. package/dist/app/session/session-event-controller.js +7 -1
  17. package/dist/app/session/session-history.d.ts +3 -0
  18. package/dist/app/session/session-history.js +19 -2
  19. package/dist/app/session/session-search.js +2 -0
  20. package/dist/app/session/session-stats.d.ts +3 -0
  21. package/dist/app/session/session-stats.js +65 -0
  22. package/dist/app/types.d.ts +6 -1
  23. package/dist/bundled-extensions/terminal-bell/index.js +1 -37
  24. package/external/pi-tools-suite/README.md +17 -1
  25. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +102 -0
  26. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
  27. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +0 -33
  28. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
  29. package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +13 -121
  30. package/external/pi-tools-suite/src/dcp/config.ts +27 -0
  31. package/external/pi-tools-suite/src/dcp/debug-log.ts +2 -1
  32. package/external/pi-tools-suite/src/dcp/index.ts +193 -20
  33. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +85 -0
  34. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +254 -0
  35. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -0
  36. package/external/pi-tools-suite/src/dcp/pruner-types.ts +30 -0
  37. package/external/pi-tools-suite/src/dcp/pruner.ts +8 -0
  38. package/external/pi-tools-suite/src/dcp/state.ts +21 -6
  39. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +14 -0
  40. package/external/pi-tools-suite/src/index.ts +4 -2
  41. package/external/pi-tools-suite/src/telegram-mirror/events.ts +9 -4
  42. package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
  43. package/external/pi-tools-suite/src/todo/index.ts +23 -27
  44. package/package.json +1 -1
package/dist/app/app.js CHANGED
@@ -3,6 +3,7 @@ import { InputEditor } from "../input-editor.js";
3
3
  import { compileOutputFilterPatterns, defaultPixConfig, loadPixConfig, resolveToolRule, } from "../config.js";
4
4
  import { AppCommandController } from "./commands/command-controller.js";
5
5
  import { ConversationViewport } from "./rendering/conversation-viewport.js";
6
+ import { renderRegisteredExtensionEntry } from "./rendering/extension-entry-renderer.js";
6
7
  import { EditorLayoutRenderer } from "./rendering/editor-layout-renderer.js";
7
8
  import { AppExtensionActionsController } from "./extensions/extension-actions-controller.js";
8
9
  import { ExtensionUiController } from "./extensions/extension-ui-controller.js";
@@ -462,6 +463,7 @@ export class PiUiExtendApp {
462
463
  hasDynamicConversationBlock: () => this.popupMenus.hasDynamicConversationBlock(),
463
464
  isDynamicConversationBlock: (entry) => this.popupMenus.isDynamicConversationBlock(entry),
464
465
  renderInlineUserMessageMenu: (entry, context) => this.popupMenus.renderInlineUserMessageMenu(entry, context),
466
+ renderExtensionEntry: (entry, width) => renderRegisteredExtensionEntry(entry, width, this.runtime?.session.extensionRunner.getEntryRenderer(entry.sessionEntry.customType), this.theme),
465
467
  });
466
468
  this.scrollController = new AppScrollController({
467
469
  conversationViewport: () => this.conversationViewport,
@@ -10,6 +10,7 @@ import { copyTextToClipboard } from "../screen/clipboard.js";
10
10
  import { formatAccountUsageReport, queryAccountUsageReport } from "../model/model-usage-status.js";
11
11
  import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsage } from "../cli/update.js";
12
12
  import { createStartupInfoMessage } from "../cli/startup-info.js";
13
+ import { getCompleteSessionStats } from "../session/session-stats.js";
13
14
  import { loadSessionTitleConfig } from "../../bundled-extensions/session-title/config.js";
14
15
  import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitle, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
15
16
  export class SessionCommandActions {
@@ -141,7 +142,7 @@ export class SessionCommandActions {
141
142
  const runtime = getRuntime(this.host, "session");
142
143
  if (!runtime)
143
144
  return;
144
- const stats = runtime.session.getSessionStats();
145
+ const stats = await getCompleteSessionStats(runtime.session);
145
146
  const lines = [
146
147
  createStartupInfoMessage(runtime),
147
148
  "",
@@ -181,7 +182,7 @@ export class SessionCommandActions {
181
182
  this.host.setSessionStatus(runtime.session);
182
183
  return;
183
184
  }
184
- const stats = runtime.session.getSessionStats();
185
+ const stats = await getCompleteSessionStats(runtime.session);
185
186
  const contextUsage = stats.contextUsage ?? runtime.session.getContextUsage();
186
187
  const model = runtime.session.model ? this.host.modelRef(runtime.session.model) : "not selected";
187
188
  const lines = [
@@ -54,9 +54,7 @@ export class AppExtensionActionsController {
54
54
  };
55
55
  }
56
56
  async waitForSessionIdle(runtime) {
57
- while (runtime.session.isStreaming || runtime.session.isCompacting) {
58
- await new Promise((resolve) => setTimeout(resolve, 50));
59
- }
57
+ await runtime.session.waitForIdle();
60
58
  }
61
59
  handleExtensionError(error) {
62
60
  const sourceText = formatExtensionErrorSource(error.extensionPath);
@@ -18,5 +18,8 @@ export type ConversationEntryRenderOptions = {
18
18
  renderInlineUserMessageMenu: (entry: Extract<Entry, {
19
19
  kind: "user";
20
20
  }>, context: InlineUserMessageMenuContext) => RenderedLine[];
21
+ renderExtensionEntry?: (entry: Extract<Entry, {
22
+ kind: "extension-entry";
23
+ }>, width: number) => RenderedLine[];
21
24
  };
22
25
  export declare function renderConversationEntry(entry: Entry, width: number, options: ConversationEntryRenderOptions): RenderedLine[];
@@ -49,6 +49,8 @@ export function renderConversationEntry(entry, width, options) {
49
49
  return renderAssistantLines(entry.text, width, options);
50
50
  case "custom":
51
51
  return renderCustomEntry(entry, width);
52
+ case "extension-entry":
53
+ return options.renderExtensionEntry?.(entry, width) ?? [];
52
54
  case "session-aborted":
53
55
  return wrapTextLines(entry.text, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "error" }));
54
56
  case "shell":
@@ -17,6 +17,9 @@ export type ConversationViewportHost = {
17
17
  renderInlineUserMessageMenu(entry: Extract<Entry, {
18
18
  kind: "user";
19
19
  }>, context: InlineUserMessageMenuContext): RenderedLine[];
20
+ renderExtensionEntry?(entry: Extract<Entry, {
21
+ kind: "extension-entry";
22
+ }>, width: number): RenderedLine[];
20
23
  };
21
24
  export type ConversationEntryBlockPosition = {
22
25
  entry: Entry;
@@ -85,6 +85,7 @@ export class ConversationViewport {
85
85
  allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
86
86
  currentTimeMs: Date.now(),
87
87
  renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
88
+ renderExtensionEntry: (extensionEntry, renderWidth) => this.host.renderExtensionEntry?.(extensionEntry, renderWidth) ?? [],
88
89
  });
89
90
  const block = {
90
91
  version,
@@ -297,6 +298,7 @@ export class ConversationViewport {
297
298
  }
298
299
  isDynamicConversationBlock(entry) {
299
300
  return (entry.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined)
301
+ || entry.kind === "extension-entry"
300
302
  || this.host.isDynamicConversationBlock(entry);
301
303
  }
302
304
  refreshLayoutEntry(layout, width, index, measure) {
@@ -342,6 +344,8 @@ export class ConversationViewport {
342
344
  case "custom":
343
345
  case "session-aborted":
344
346
  return estimateWrappedLineCount(entry.text, width);
347
+ case "extension-entry":
348
+ return 1;
345
349
  case "user": {
346
350
  const { contentWidth } = horizontalPaddingLayout(width);
347
351
  return estimateWrappedLineCount(entry.text, contentWidth);
@@ -0,0 +1,6 @@
1
+ import { type EntryRenderer } from "@earendil-works/pi-coding-agent";
2
+ import type { Theme as PixTheme } from "../../theme.js";
3
+ import type { Entry, RenderedLine } from "../types.js";
4
+ export declare function renderRegisteredExtensionEntry(entry: Extract<Entry, {
5
+ kind: "extension-entry";
6
+ }>, width: number, renderer: EntryRenderer | undefined, theme: PixTheme): RenderedLine[];
@@ -0,0 +1,100 @@
1
+ import { Theme as PiTheme } from "@earendil-works/pi-coding-agent";
2
+ import { ansiStyledLine } from "./tool-block-renderer.js";
3
+ const themes = new WeakMap();
4
+ const components = new WeakMap();
5
+ export function renderRegisteredExtensionEntry(entry, width, renderer, theme) {
6
+ if (!renderer || width <= 0)
7
+ return [];
8
+ try {
9
+ const cached = components.get(entry);
10
+ let component = cached?.component;
11
+ if (!cached || cached.renderer !== renderer || cached.theme !== theme || cached.expanded !== entry.expanded) {
12
+ component = renderer(entry.sessionEntry, { expanded: entry.expanded }, extensionRendererTheme(theme)) ?? undefined;
13
+ if (!component) {
14
+ components.delete(entry);
15
+ return [];
16
+ }
17
+ components.set(entry, { renderer, theme, expanded: entry.expanded, component });
18
+ }
19
+ if (!component)
20
+ return [];
21
+ return component.render(width).map((rawLine) => {
22
+ const line = ansiStyledLine(rawLine.replace(/\r/gu, ""));
23
+ return {
24
+ text: line.text,
25
+ copyText: line.text,
26
+ target: { kind: "tool", id: entry.id },
27
+ ...(line.segments.length > 0 ? { segments: line.segments } : {}),
28
+ };
29
+ });
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ return [{ text: `[${entry.sessionEntry.customType}] renderer failed: ${message}`, variant: "error" }];
34
+ }
35
+ }
36
+ function extensionRendererTheme(theme) {
37
+ const cached = themes.get(theme);
38
+ if (cached)
39
+ return cached;
40
+ const colors = theme.colors;
41
+ const foregrounds = {
42
+ accent: colors.accent,
43
+ border: colors.inputBorder,
44
+ borderAccent: colors.accent,
45
+ borderMuted: colors.muted,
46
+ success: colors.success,
47
+ error: colors.error,
48
+ warning: colors.warning,
49
+ muted: colors.muted,
50
+ dim: colors.muted,
51
+ text: colors.foreground,
52
+ thinkingText: colors.thinkingForeground,
53
+ userMessageText: colors.userForeground,
54
+ customMessageText: colors.foreground,
55
+ customMessageLabel: colors.accent,
56
+ toolTitle: colors.toolTitle,
57
+ toolOutput: colors.foreground,
58
+ mdHeading: colors.heading,
59
+ mdLink: colors.info,
60
+ mdLinkUrl: colors.muted,
61
+ mdCode: colors.info,
62
+ mdCodeBlock: colors.foreground,
63
+ mdCodeBlockBorder: colors.muted,
64
+ mdQuote: colors.foreground,
65
+ mdQuoteBorder: colors.muted,
66
+ mdHr: colors.muted,
67
+ mdListBullet: colors.accent,
68
+ toolDiffAdded: colors.success,
69
+ toolDiffRemoved: colors.error,
70
+ toolDiffContext: colors.muted,
71
+ syntaxComment: colors.muted,
72
+ syntaxKeyword: colors.toolEdit,
73
+ syntaxFunction: colors.toolIndex,
74
+ syntaxVariable: colors.foreground,
75
+ syntaxString: colors.toolRead,
76
+ syntaxNumber: colors.warning,
77
+ syntaxType: colors.toolSearch,
78
+ syntaxOperator: colors.muted,
79
+ syntaxPunctuation: colors.muted,
80
+ thinkingOff: colors.muted,
81
+ thinkingMinimal: colors.muted,
82
+ thinkingLow: colors.info,
83
+ thinkingMedium: colors.accent,
84
+ thinkingHigh: colors.warning,
85
+ thinkingXhigh: colors.thinkingXHigh,
86
+ thinkingMax: colors.thinkingMax,
87
+ bashMode: colors.toolBash,
88
+ };
89
+ const backgrounds = {
90
+ selectedBg: colors.popupSelectedBackground,
91
+ userMessageBg: colors.userMessageBackground || colors.background,
92
+ customMessageBg: colors.assistantMessageBackground || colors.background,
93
+ toolPendingBg: colors.thinkingMessageBackground || colors.background,
94
+ toolSuccessBg: colors.assistantMessageBackground || colors.background,
95
+ toolErrorBg: colors.assistantMessageBackground || colors.background,
96
+ };
97
+ const created = new PiTheme(foregrounds, backgrounds, "truecolor", { name: `pix-${theme.name}` });
98
+ themes.set(theme, created);
99
+ return created;
100
+ }
@@ -29,3 +29,8 @@ export type ToolBlockRenderOptions = {
29
29
  headerColorOverride?: string;
30
30
  };
31
31
  export declare function renderToolBlock(entry: ToolBlockEntry, rule: ResolvedToolRule, width: number, colors: Theme["colors"], options?: ToolBlockRenderOptions): RenderedLine[];
32
+ export type AnsiStyledLine = {
33
+ text: string;
34
+ segments: StyledSegment[];
35
+ };
36
+ export declare function ansiStyledLine(rawLine: string): AnsiStyledLine;
@@ -279,7 +279,7 @@ function sanitizeToolBodyText(text, preserveAnsi) {
279
279
  function stripAnsi(text) {
280
280
  return ansiStyledLine(text).text;
281
281
  }
282
- function ansiStyledLine(rawLine) {
282
+ export function ansiStyledLine(rawLine) {
283
283
  let text = "";
284
284
  let style = {};
285
285
  let segmentStart;
@@ -161,7 +161,7 @@ export class AppMouseController {
161
161
  if (!this.toolTargetContainsEvent(event))
162
162
  return;
163
163
  const entry = this.host.findEntry(target.id);
164
- if (entry?.kind === "tool" || entry?.kind === "thinking" || entry?.kind === "shell") {
164
+ if (entry?.kind === "tool" || entry?.kind === "thinking" || entry?.kind === "shell" || entry?.kind === "extension-entry") {
165
165
  entry.expanded = !entry.expanded;
166
166
  this.host.touchEntry(entry);
167
167
  this.showClickFlashForEvent(event);
@@ -3,7 +3,7 @@ import { appendFileSync, createReadStream, existsSync, mkdirSync, writeFileSync
3
3
  import { mkdir, open as openFile, stat, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createInterface } from "node:readline";
6
- import { buildSessionContext, SessionManager, } from "@earendil-works/pi-coding-agent";
6
+ import { buildContextEntries as buildSdkContextEntries, buildSessionContext, SessionManager, } from "@earendil-works/pi-coding-agent";
7
7
  import { isRecord } from "../guards.js";
8
8
  const CURRENT_SESSION_VERSION = 3;
9
9
  const DEFAULT_TAIL_ENTRY_COUNT = 180;
@@ -39,6 +39,11 @@ class LazySessionManager {
39
39
  async initialize(cwdOverride) {
40
40
  this.header = await this.loadHeaderAsync(cwdOverride);
41
41
  this.cwdPath = resolve(cwdOverride ?? this.header.cwd ?? process.cwd());
42
+ if ((this.header.version ?? 1) < CURRENT_SESSION_VERSION) {
43
+ this.hydrated = SessionManager.open(this.sessionFilePath, this.sessionDirPath, this.cwdPath);
44
+ this.header = this.hydrated.getHeader() ?? this.header;
45
+ return;
46
+ }
42
47
  await this.loadTailEntriesAsync();
43
48
  }
44
49
  setSessionFile(sessionFile) {
@@ -140,6 +145,18 @@ class LazySessionManager {
140
145
  const entries = await readAllSessionEntries(this.sessionFilePath);
141
146
  return branchEntries(entries, this.leafId ?? entries.at(-1)?.id);
142
147
  }
148
+ async readFullSessionEntries() {
149
+ if (this.hydrated)
150
+ return this.hydrated.getEntries();
151
+ return readAllSessionEntries(this.sessionFilePath);
152
+ }
153
+ buildContextEntries() {
154
+ if (this.hydrated)
155
+ return this.hydrated.buildContextEntries();
156
+ const entries = this.contextEntries();
157
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
158
+ return buildSdkContextEntries(entries, entries.at(-1)?.id ?? null, byId);
159
+ }
143
160
  buildSessionContext() {
144
161
  if (this.hydrated)
145
162
  return this.hydrated.buildSessionContext();
@@ -1,6 +1,7 @@
1
1
  import type { AgentSession, SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
  export declare const PIX_SYSTEM_MESSAGE_CUSTOM_TYPE = "pix-system";
3
3
  export declare const PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE = "pix:system_message";
4
+ export declare const PIX_EXTENSION_ENTRY_ROLE = "pix:extension_entry";
4
5
  export declare const PIX_SESSION_ENTRY_ID_FIELD = "__pixSessionEntryId";
5
6
  export declare const PIX_THINKING_LEVEL_FIELD = "__pixThinkingLevel";
6
7
  export declare function appendPixSystemDisplayEntry(session: AgentSession, text: string): void;
@@ -1,6 +1,7 @@
1
1
  import { isRecord } from "../guards.js";
2
2
  export const PIX_SYSTEM_MESSAGE_CUSTOM_TYPE = "pix-system";
3
3
  export const PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE = "pix:system_message";
4
+ export const PIX_EXTENSION_ENTRY_ROLE = "pix:extension_entry";
4
5
  export const PIX_SESSION_ENTRY_ID_FIELD = "__pixSessionEntryId";
5
6
  export const PIX_THINKING_LEVEL_FIELD = "__pixThinkingLevel";
6
7
  export function appendPixSystemDisplayEntry(session, text) {
@@ -50,6 +51,9 @@ export function sessionHistoryDisplayMessagesFromEntries(branch) {
50
51
  display: true,
51
52
  });
52
53
  }
54
+ else if (entry.type === "custom") {
55
+ messages.push({ role: PIX_EXTENSION_ENTRY_ROLE, entry });
56
+ }
53
57
  }
54
58
  return messages;
55
59
  }
@@ -1,6 +1,6 @@
1
1
  import { createId } from "../id.js";
2
2
  import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
3
- import { customMessageEntry, loadSessionHistoryEntries, loadSessionHistoryEntriesAsync } from "./session-history.js";
3
+ import { customMessageEntry, extensionSessionEntry, loadSessionHistoryEntries, loadSessionHistoryEntriesAsync } from "./session-history.js";
4
4
  import { sessionHistoryDisplayMessages, sessionHistoryDisplayMessagesFromEntries, sessionHistoryFullBranchEntries } from "./pix-system-message.js";
5
5
  import { THINKING_TOOL_NAME } from "../constants.js";
6
6
  import { isRecord } from "../guards.js";
@@ -212,6 +212,12 @@ export class AppSessionEventController {
212
212
  case "message_end":
213
213
  this.handleMessageEnd(event.message);
214
214
  break;
215
+ case "entry_appended": {
216
+ const entry = extensionSessionEntry(event.entry);
217
+ if (entry)
218
+ this.addEntry(entry);
219
+ break;
220
+ }
215
221
  case "agent_start":
216
222
  this.assistantMessageClosed = false;
217
223
  this.host.setSessionActivity("running");
@@ -38,4 +38,7 @@ export type OlderSessionHistoryMessagesReader = {
38
38
  export declare function loadSessionHistoryEntries(options: LoadSessionHistoryOptions): void;
39
39
  export declare function loadSessionHistoryEntriesAsync(options: LoadSessionHistoryAsyncOptions): Promise<boolean>;
40
40
  export declare function customMessageEntry(message: Record<string, unknown>): Entry | undefined;
41
+ export declare function extensionSessionEntry(value: unknown): Extract<Entry, {
42
+ kind: "extension-entry";
43
+ }> | undefined;
41
44
  export {};
@@ -3,7 +3,7 @@ import { createId } from "../id.js";
3
3
  import { isOnlyHiddenMetadata } from "../../markdown-format.js";
4
4
  import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
5
5
  import { THINKING_TOOL_NAME } from "../constants.js";
6
- import { PIX_SESSION_ENTRY_ID_FIELD, PIX_SYSTEM_MESSAGE_CUSTOM_TYPE, PIX_THINKING_LEVEL_FIELD } from "./pix-system-message.js";
6
+ import { PIX_EXTENSION_ENTRY_ROLE, PIX_SESSION_ENTRY_ID_FIELD, PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE, PIX_SYSTEM_MESSAGE_CUSTOM_TYPE, PIX_THINKING_LEVEL_FIELD } from "./pix-system-message.js";
7
7
  const HISTORICAL_SUBAGENTS_OBSERVATION = { showSnapshot: false };
8
8
  const DEFAULT_HISTORY_CHUNK_SIZE = 50;
9
9
  const DEFAULT_HISTORY_TAIL_MESSAGE_COUNT = 80;
@@ -142,7 +142,12 @@ function addSessionHistoryRangeEntries(messages, start, end, toolResults, addEnt
142
142
  const message = messages[index];
143
143
  if (!isRecord(message))
144
144
  continue;
145
- if (message.role === "custom") {
145
+ if (message.role === PIX_EXTENSION_ENTRY_ROLE) {
146
+ const entry = extensionSessionEntry(message.entry);
147
+ if (entry)
148
+ addEntry(entry);
149
+ }
150
+ else if (message.role === "custom") {
146
151
  const entry = customMessageEntry(message);
147
152
  if (entry)
148
153
  addEntry(entry);
@@ -175,6 +180,18 @@ export function customMessageEntry(message) {
175
180
  return { id: createId("system"), kind: "system", text };
176
181
  return { id: createId("custom"), kind: "custom", customType, text };
177
182
  }
183
+ export function extensionSessionEntry(value) {
184
+ if (!isRecord(value) || value.type !== "custom" || typeof value.id !== "string" || typeof value.customType !== "string")
185
+ return undefined;
186
+ if (value.customType === PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE)
187
+ return undefined;
188
+ return {
189
+ id: `extension-entry-${value.id}`,
190
+ kind: "extension-entry",
191
+ sessionEntry: value,
192
+ expanded: false,
193
+ };
194
+ }
178
195
  function renderAssistantHistoryMessage(message, toolResults, options) {
179
196
  const content = message.content;
180
197
  if (!Array.isArray(content))
@@ -145,6 +145,8 @@ function entrySearchText(entry) {
145
145
  return `${entry.toolName}\n${entry.argsText}\n${entry.output}`;
146
146
  case "shell":
147
147
  return `${entry.command}\n${entry.output}`;
148
+ case "extension-entry":
149
+ return entry.sessionEntry.customType;
148
150
  case "user":
149
151
  case "assistant":
150
152
  case "custom":
@@ -0,0 +1,3 @@
1
+ import type { AgentSession, SessionEntry, SessionStats } from "@earendil-works/pi-coding-agent";
2
+ export declare function getCompleteSessionStats(session: AgentSession): Promise<SessionStats>;
3
+ export declare function aggregateSessionStats(entries: readonly SessionEntry[], base: SessionStats): SessionStats;
@@ -0,0 +1,65 @@
1
+ export async function getCompleteSessionStats(session) {
2
+ const base = session.getSessionStats();
3
+ const manager = session.sessionManager;
4
+ if (typeof manager.readFullSessionEntries !== "function")
5
+ return base;
6
+ const entries = await manager.readFullSessionEntries();
7
+ return aggregateSessionStats(entries, base);
8
+ }
9
+ export function aggregateSessionStats(entries, base) {
10
+ let userMessages = 0;
11
+ let assistantMessages = 0;
12
+ let toolResults = 0;
13
+ let totalMessages = 0;
14
+ let toolCalls = 0;
15
+ let input = 0;
16
+ let output = 0;
17
+ let cacheRead = 0;
18
+ let cacheWrite = 0;
19
+ let cost = 0;
20
+ for (const entry of entries) {
21
+ if (entry.type !== "message")
22
+ continue;
23
+ totalMessages += 1;
24
+ const message = entry.message;
25
+ if (message.role === "user") {
26
+ userMessages += 1;
27
+ continue;
28
+ }
29
+ if (message.role === "toolResult") {
30
+ toolResults += 1;
31
+ continue;
32
+ }
33
+ if (message.role !== "assistant")
34
+ continue;
35
+ assistantMessages += 1;
36
+ if (Array.isArray(message.content)) {
37
+ toolCalls += message.content.filter((content) => content.type === "toolCall").length;
38
+ }
39
+ const usage = message.usage;
40
+ input += finiteNumber(usage?.input);
41
+ output += finiteNumber(usage?.output);
42
+ cacheRead += finiteNumber(usage?.cacheRead);
43
+ cacheWrite += finiteNumber(usage?.cacheWrite);
44
+ cost += finiteNumber(usage?.cost?.total);
45
+ }
46
+ return {
47
+ ...base,
48
+ userMessages,
49
+ assistantMessages,
50
+ toolCalls,
51
+ toolResults,
52
+ totalMessages,
53
+ tokens: {
54
+ input,
55
+ output,
56
+ cacheRead,
57
+ cacheWrite,
58
+ total: input + output + cacheRead + cacheWrite,
59
+ },
60
+ cost,
61
+ };
62
+ }
63
+ function finiteNumber(value) {
64
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
65
+ }
@@ -1,4 +1,4 @@
1
- import type { AgentSession, ExtensionUIContext, SessionManager, SessionInfo, SlashCommandSource, SourceInfo } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentSession, ExtensionUIContext, CustomEntry, SessionManager, SessionInfo, SlashCommandSource, SourceInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { ImageContent } from "../input-editor.js";
3
3
  import type { SyntaxLineHighlight } from "../syntax-highlight.js";
4
4
  import type { ThemeName } from "../theme.js";
@@ -53,6 +53,11 @@ export type Entry = {
53
53
  kind: "custom";
54
54
  customType: string;
55
55
  text: string;
56
+ } | {
57
+ id: string;
58
+ kind: "extension-entry";
59
+ sessionEntry: CustomEntry;
60
+ expanded: boolean;
56
61
  } | {
57
62
  id: string;
58
63
  kind: "session-aborted";
@@ -9,12 +9,6 @@ const IDLE_RETRY_DELAY_MS = 100;
9
9
  const MAX_IDLE_RETRIES = 40;
10
10
  const SUBAGENTS_LIVE_COUNT_EVENT = "pi-tools-suite:async-subagents:live-count";
11
11
  const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
12
- /**
13
- * Renderer-relayed signal that the session is in an auto-retry cycle.
14
- * Payload: `{ active: boolean }`. The SDK does not forward retry state to
15
- * extensions, so the renderer emits this on the extension event bus.
16
- */
17
- const RETRY_ACTIVE_EVENT = "pix:retry-active";
18
12
  /**
19
13
  * Renderer-relayed signal that the user interrupted the session (Esc/Ctrl-C).
20
14
  * Payload: `{ aborted: boolean }`. Aborting the SDK stream during tool
@@ -224,9 +218,6 @@ function retryFailureMessageTemplate() {
224
218
  function notificationTitleTemplate(defaultTitle) {
225
219
  return process.env.PI_TERMINAL_BELL_NOTIFY_TITLE ?? defaultTitle;
226
220
  }
227
- function willRetryAfterAgentEnd(event) {
228
- return event.willRetry === true;
229
- }
230
221
  function isAbortedMessageUpdate(event) {
231
222
  return event.type === "error" && event.reason === "aborted";
232
223
  }
@@ -405,11 +396,6 @@ export default function terminalBell(pi) {
405
396
  // attention bell should never ring for a user-initiated abort, so this flag
406
397
  // suppresses any queued/pending bell until the next agent_start resets it.
407
398
  let userAborted = false;
408
- // True while the session is in an auto-retry cycle (relayed via the
409
- // extension event bus). Suppresses the failure bell on intermediate retry
410
- // attempts; the final exhausted failure still rings because no retry-start
411
- // signal precedes it.
412
- let retryActive = false;
413
399
  const activeSubagentWaitToolCallIds = new Set();
414
400
  const notifiedAskUserToolCallIds = new Set();
415
401
  const idleDelayMs = parseDelayMs(process.env.PI_TERMINAL_BELL_DELAY_MS);
@@ -440,10 +426,6 @@ export default function terminalBell(pi) {
440
426
  function attemptBell(pending, attempt) {
441
427
  timer = undefined;
442
428
  const { ctx, notification, message } = pending;
443
- // Safety net: if a retry-start signal arrives between the agent_end that
444
- // queued this bell and the timer firing, suppress the bell entirely.
445
- if (retryActive)
446
- return;
447
429
  // Safety net: if the user aborted after the bell was queued (e.g. an
448
430
  // aborted agent_end with no aborted message_update), suppress it.
449
431
  if (userAborted) {
@@ -527,18 +509,6 @@ export default function terminalBell(pi) {
527
509
  scheduleBell(pendingBell.ctx, idleDelayMs, 0, pendingBell.message, pendingBell.notification);
528
510
  }
529
511
  });
530
- pi.events.on(RETRY_ACTIVE_EVENT, (data) => {
531
- retryActive = data != null && typeof data === "object" && data.active === true;
532
- if (retryActive) {
533
- // A retry is starting right after an intermediate agent_end: cancel
534
- // any bell queued from that attempt so we don't chime on every
535
- // failed retry attempt. The final exhausted failure rings normally
536
- // because it is not followed by a retry-start signal.
537
- clearTimer();
538
- pendingBell = undefined;
539
- deferredUntilSubagentsFinish = false;
540
- }
541
- });
542
512
  pi.events.on(SESSION_ABORTED_EVENT, (data) => {
543
513
  const aborted = data != null && typeof data === "object" && data.aborted === true;
544
514
  if (!aborted)
@@ -557,7 +527,6 @@ export default function terminalBell(pi) {
557
527
  deferredUntilSubagentsFinish = false;
558
528
  lastFailureReason = undefined;
559
529
  userAborted = false;
560
- retryActive = false;
561
530
  activeSubagentWaitToolCallIds.clear();
562
531
  notifiedAskUserToolCallIds.clear();
563
532
  });
@@ -598,11 +567,7 @@ export default function terminalBell(pi) {
598
567
  activeSubagentWaitToolCallIds.delete(event.toolCallId);
599
568
  notifiedAskUserToolCallIds.delete(event.toolCallId);
600
569
  });
601
- pi.on("agent_end", async (event, ctx) => {
602
- if (willRetryAfterAgentEnd(event)) {
603
- clearTimer();
604
- return;
605
- }
570
+ pi.on("agent_settled", async (_event, ctx) => {
606
571
  if (userAborted) {
607
572
  clearTimer();
608
573
  return;
@@ -623,7 +588,6 @@ export default function terminalBell(pi) {
623
588
  liveSubagentCount = 0;
624
589
  lastFailureReason = undefined;
625
590
  userAborted = false;
626
- retryActive = false;
627
591
  activeSubagentWaitToolCallIds.clear();
628
592
  notifiedAskUserToolCallIds.clear();
629
593
  });
@@ -73,6 +73,18 @@ DCP settings are stored only under `dcp` in the user shared config file `~/.conf
73
73
  "nudgeForce": "strong",
74
74
  "protectedTools": ["compress", "write", "edit", "subagents"]
75
75
  },
76
+ "strategies": {
77
+ "emergencyCurrentTurnPruning": {
78
+ "enabled": true,
79
+ "hardContextPercent": 0.82,
80
+ "targetContextPercent": 0.70,
81
+ "patience": 2,
82
+ "keepRecentToolPairs": 8,
83
+ "minOutputTokens": 500,
84
+ "maxSuggestions": 8,
85
+ "protectedTools": []
86
+ }
87
+ },
76
88
  "modelOverrides": {
77
89
  "openai-codex/gpt-5*": {
78
90
  "compress": {
@@ -127,7 +139,11 @@ DCP settings are stored only under `dcp` in the user shared config file `~/.conf
127
139
  }
128
140
  ```
129
141
 
130
- `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. `modelOverrides` and the `modelMin*` / `modelMax*` maps support exact model keys plus `*` / `?` wildcard patterns; matching is applied from generic to specific so exact bare-model matches override bare wildcards, and exact `provider/model` matches override provider wildcards. Array fields are union-merged, so model-specific `protectedTools` extend the defaults instead of replacing them. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available. Set `dcp.debug: true` to write a JSONL debug log of DCP context/prune/compress events to `~/.pi/agent/dcp-debug.jsonl` (override the path with `PI_DCP_DEBUG_LOG`, or enable without config via `PI_DCP_DEBUG=1`); off by default. The log is size-limited and rotated: once it reaches `dcp.debugLog.maxBytes` (default `5242880` = 5 MB) it is renamed to `.1`, older backups shift down (`.1`→`.2`, …) and the oldest beyond `dcp.debugLog.maxBackups` (default `3`, minimum `1`) is dropped; override either with `PI_DCP_DEBUG_MAX_BYTES` / `PI_DCP_DEBUG_MAX_BACKUPS`.
142
+ `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. `modelOverrides` and the `modelMin*` / `modelMax*` maps support exact model keys plus `*` / `?` wildcard patterns; matching is applied from generic to specific so exact bare-model matches override bare wildcards, and exact `provider/model` matches override provider wildcards. Array fields are union-merged, so model-specific `protectedTools` extend the defaults instead of replacing them. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.
143
+
144
+ `strategies.emergencyCurrentTurnPruning` is the default-enabled lossy safety floor for a single unfinished turn that has no normal compression candidate. DCP first emits emergency reminders and offers only safe old same-turn tool-result candidates. After `patience` ignored reminders, or at the model-independent `hardContextPercent`, it replaces eligible oldest result bodies until the estimated provider context reaches `targetContextPercent` or a margin below the model emergency threshold. User messages, configured/protected data, the newest `keepRecentToolPairs`, and results not present in an accepted provider request are never selected. The raw session transcript is unchanged. Setting `enabled` to `false` disables same-turn candidates and lossy pruning, but keeps the non-destructive emergency reminder.
145
+
146
+ Set `dcp.debug: true` to write a JSONL debug log of DCP context/prune/compress events to `~/.pi/agent/dcp-debug.jsonl` (override the path with `PI_DCP_DEBUG_LOG`, or enable without config via `PI_DCP_DEBUG=1`); off by default. The log is size-limited and rotated: once it reaches `dcp.debugLog.maxBytes` (default `5242880` = 5 MB) it is renamed to `.1`, older backups shift down (`.1`→`.2`, …) and the oldest beyond `dcp.debugLog.maxBackups` (default `3`, minimum `1`) is dropped; override either with `PI_DCP_DEBUG_MAX_BYTES` / `PI_DCP_DEBUG_MAX_BACKUPS`.
131
147
 
132
148
  ## LSP setup
133
149