pi-ui-extend 0.1.57 → 0.1.60
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/model/model-usage-status.js +26 -10
- 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/package.json +3 -3
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +7 -7
- 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 +7 -114
- 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 +182 -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 +22 -8
- 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/package.json +4 -4
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);
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import { formatCompactProgressBar } from "../../context-progress-bar.js";
|
|
6
7
|
import { APP_ICONS } from "../icons.js";
|
|
7
8
|
const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -168,26 +169,25 @@ export function openAIUsageStatusFromResponse(data, modelKey, now = Date.now())
|
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
171
|
async function queryOpenAIModelUsage(modelKey) {
|
|
171
|
-
|
|
172
|
+
let authData = await readOpenAIAuth();
|
|
172
173
|
if (!authData || authData.type !== "oauth" || !authData.access)
|
|
173
174
|
return undefined;
|
|
174
175
|
if (isExpired(authData))
|
|
175
|
-
|
|
176
|
+
authData = await refreshOpenAICodexAuth();
|
|
177
|
+
if (!authData.access)
|
|
178
|
+
throw new Error("OpenAI Codex OAuth refresh returned no access token");
|
|
176
179
|
const usage = await fetchOpenAIUsage(authData.access);
|
|
177
180
|
return openAIUsageStatusFromResponse(usage, modelKey);
|
|
178
181
|
}
|
|
179
182
|
async function queryOpenAIAccountUsage(now) {
|
|
180
|
-
|
|
183
|
+
let authData = await readOpenAIAuth();
|
|
181
184
|
if (!authData || authData.type !== "oauth" || !authData.access)
|
|
182
185
|
return undefined;
|
|
183
|
-
if (isExpired(authData))
|
|
184
|
-
return {
|
|
185
|
-
account: accountLabelFromOpenAIAuth(authData),
|
|
186
|
-
windows: [],
|
|
187
|
-
limitReached: false,
|
|
188
|
-
error: "OAuth token is expired",
|
|
189
|
-
};
|
|
190
186
|
try {
|
|
187
|
+
if (isExpired(authData))
|
|
188
|
+
authData = await refreshOpenAICodexAuth();
|
|
189
|
+
if (!authData.access)
|
|
190
|
+
throw new Error("OpenAI Codex OAuth refresh returned no access token");
|
|
191
191
|
const usage = await fetchOpenAIUsage(authData.access);
|
|
192
192
|
const windows = [usage.rate_limit?.primary_window, usage.rate_limit?.secondary_window]
|
|
193
193
|
.filter(isRateLimitWindow)
|
|
@@ -219,6 +219,22 @@ async function queryOpenAIAccountUsage(now) {
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
async function refreshOpenAICodexAuth() {
|
|
223
|
+
// Delegate to pi core so refresh-token rotation is persisted under the same
|
|
224
|
+
// cross-process auth.json lock used by model requests.
|
|
225
|
+
const authStorage = AuthStorage.create(getPiAuthPath());
|
|
226
|
+
const access = await authStorage.getApiKey("openai-codex", { includeFallback: false });
|
|
227
|
+
const credential = authStorage.get("openai-codex");
|
|
228
|
+
if (!access || credential?.type !== "oauth" || !credential.access || isExpired(credential)) {
|
|
229
|
+
throw new Error("OpenAI Codex OAuth token refresh failed");
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
type: "oauth",
|
|
233
|
+
access: credential.access,
|
|
234
|
+
refresh: credential.refresh,
|
|
235
|
+
expires: credential.expires,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
222
238
|
async function readOpenAIAuth() {
|
|
223
239
|
const authData = await readOpenCodeAuth();
|
|
224
240
|
const piAuth = await readPiAuth();
|
|
@@ -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
|
});
|