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.
- package/dist/app/app.js +2 -0
- package/dist/app/commands/command-session-actions.js +3 -2
- package/dist/app/extensions/extension-actions-controller.js +1 -3
- package/dist/app/rendering/conversation-entry-renderer.d.ts +3 -0
- package/dist/app/rendering/conversation-entry-renderer.js +2 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +4 -0
- package/dist/app/rendering/extension-entry-renderer.d.ts +6 -0
- package/dist/app/rendering/extension-entry-renderer.js +100 -0
- package/dist/app/rendering/tool-block-renderer.d.ts +5 -0
- package/dist/app/rendering/tool-block-renderer.js +1 -1
- package/dist/app/screen/mouse-controller.js +1 -1
- package/dist/app/session/lazy-session-manager.js +18 -1
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +4 -0
- package/dist/app/session/session-event-controller.js +7 -1
- package/dist/app/session/session-history.d.ts +3 -0
- package/dist/app/session/session-history.js +19 -2
- package/dist/app/session/session-search.js +2 -0
- package/dist/app/session/session-stats.d.ts +3 -0
- package/dist/app/session/session-stats.js +65 -0
- package/dist/app/types.d.ts +6 -1
- package/dist/bundled-extensions/terminal-bell/index.js +1 -37
- package/external/pi-tools-suite/README.md +17 -1
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +102 -0
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +0 -33
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
- package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +13 -121
- package/external/pi-tools-suite/src/dcp/config.ts +27 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +2 -1
- package/external/pi-tools-suite/src/dcp/index.ts +193 -20
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +85 -0
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +254 -0
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -0
- package/external/pi-tools-suite/src/dcp/pruner-types.ts +30 -0
- package/external/pi-tools-suite/src/dcp/pruner.ts +8 -0
- package/external/pi-tools-suite/src/dcp/state.ts +21 -6
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +14 -0
- package/external/pi-tools-suite/src/index.ts +4 -2
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +9 -4
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +23 -27
- 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
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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
|
+
}
|
package/dist/app/types.d.ts
CHANGED
|
@@ -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("
|
|
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.
|
|
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
|
|