pi-ui-extend 0.1.18 → 0.1.19
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 +8 -6
- package/dist/app/constants.d.ts +1 -0
- package/dist/app/constants.js +1 -0
- package/dist/app/input/voice-controller.js +16 -12
- package/dist/app/popup/popup-menu-controller.d.ts +1 -5
- package/dist/app/popup/popup-menu-controller.js +7 -8
- package/dist/app/process.js +7 -0
- package/dist/app/rendering/conversation-entry-renderer.js +17 -16
- package/dist/app/rendering/conversation-viewport.js +4 -35
- package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
- package/dist/app/rendering/editor-layout-renderer.js +25 -16
- package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
- package/dist/app/rendering/popup-menu-renderer.js +24 -34
- package/dist/app/rendering/render-controller.d.ts +2 -0
- package/dist/app/rendering/render-controller.js +26 -25
- package/dist/app/rendering/render-text.js +2 -2
- package/dist/app/rendering/status-line-renderer.js +1 -1
- package/dist/app/rendering/tab-line-renderer.js +3 -3
- package/dist/app/runtime.js +29 -3
- package/dist/app/screen/file-link-opener.d.ts +2 -0
- package/dist/app/screen/file-link-opener.js +84 -17
- package/dist/app/screen/mouse-controller.d.ts +0 -2
- package/dist/app/screen/mouse-controller.js +6 -12
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/app/session/lazy-session-manager.d.ts +1 -1
- package/dist/app/session/lazy-session-manager.js +64 -52
- package/dist/app/session/queued-message-controller.d.ts +6 -0
- package/dist/app/session/queued-message-controller.js +9 -1
- package/dist/app/session/queued-message-entries.d.ts +8 -0
- package/dist/app/session/queued-message-entries.js +41 -0
- package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
- package/dist/app/session/session-lifecycle-controller.js +45 -11
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +197 -30
- package/dist/app/terminal/terminal-controller.d.ts +2 -0
- package/dist/app/terminal/terminal-controller.js +7 -5
- package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
- package/dist/schemas/pi-tools-suite-schema.js +3 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +8 -2
- package/extensions/session-title/config.ts +3 -3
- package/extensions/session-title/index.ts +60 -5
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
- package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
- package/external/pi-tools-suite/src/config.ts +13 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
- package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
- package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
- package/external/pi-tools-suite/src/todo/index.ts +81 -4
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +3 -14
- package/schemas/pi-tools-suite.json +19 -0
- package/apps/desktop-tauri/README.md +0 -103
- package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
|
@@ -50,7 +50,7 @@ export class TabLineRenderer {
|
|
|
50
50
|
segments.push({
|
|
51
51
|
start: separatorOffset + 1,
|
|
52
52
|
end: separatorOffset + 2,
|
|
53
|
-
foreground: this.host.theme.colors.
|
|
53
|
+
foreground: this.host.theme.colors.tabBorder,
|
|
54
54
|
});
|
|
55
55
|
displayColumn += separatorWidth;
|
|
56
56
|
}
|
|
@@ -89,7 +89,7 @@ export class TabLineRenderer {
|
|
|
89
89
|
segments.push({
|
|
90
90
|
start: newTabDividerOffset,
|
|
91
91
|
end: newTabDividerOffset + 1,
|
|
92
|
-
foreground: this.host.theme.colors.
|
|
92
|
+
foreground: this.host.theme.colors.tabBorder,
|
|
93
93
|
});
|
|
94
94
|
segments.push({
|
|
95
95
|
start: lineText.length - APP_ICONS.plus.length,
|
|
@@ -119,7 +119,7 @@ export class TabLineRenderer {
|
|
|
119
119
|
}
|
|
120
120
|
renderBottom(row, layout, width) {
|
|
121
121
|
return this.host.screenStyler.styleLine(row, this.bottomText(layout, width), width, {
|
|
122
|
-
foreground: this.host.theme.colors.
|
|
122
|
+
foreground: this.host.theme.colors.tabBorder,
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
bottomText(layout, width) {
|
package/dist/app/runtime.js
CHANGED
|
@@ -133,6 +133,32 @@ function isBundledQuestionConflict(error, bundledExtensionPaths) {
|
|
|
133
133
|
}
|
|
134
134
|
return false;
|
|
135
135
|
}
|
|
136
|
+
const bundledSkillsInstallPromises = new Map();
|
|
137
|
+
const piToolsSuiteInstallPromises = new Map();
|
|
138
|
+
async function ensureBundledSkillsInstalledOnce(options = {}) {
|
|
139
|
+
const targetPath = resolve(options.targetPath ?? bundledSkillsInstallPath(options.homeDir));
|
|
140
|
+
const existing = bundledSkillsInstallPromises.get(targetPath);
|
|
141
|
+
if (existing)
|
|
142
|
+
return await existing;
|
|
143
|
+
const pending = ensureBundledSkillsInstalled(options).catch((error) => {
|
|
144
|
+
bundledSkillsInstallPromises.delete(targetPath);
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
147
|
+
bundledSkillsInstallPromises.set(targetPath, pending);
|
|
148
|
+
return await pending;
|
|
149
|
+
}
|
|
150
|
+
async function ensurePiToolsSuiteExtensionInstalledOnce(options = {}) {
|
|
151
|
+
const targetPath = resolve(options.targetPath ?? piToolsSuiteExtensionInstallPath(options.agentDir));
|
|
152
|
+
const existing = piToolsSuiteInstallPromises.get(targetPath);
|
|
153
|
+
if (existing)
|
|
154
|
+
return await existing;
|
|
155
|
+
const pending = ensurePiToolsSuiteExtensionInstalled(options).catch((error) => {
|
|
156
|
+
piToolsSuiteInstallPromises.delete(targetPath);
|
|
157
|
+
throw error;
|
|
158
|
+
});
|
|
159
|
+
piToolsSuiteInstallPromises.set(targetPath, pending);
|
|
160
|
+
return await pending;
|
|
161
|
+
}
|
|
136
162
|
export function resolvePixRuntimeModelRef(options, sessionManager, config = loadPixConfig()) {
|
|
137
163
|
if (options.modelRef)
|
|
138
164
|
return options.modelRef;
|
|
@@ -176,8 +202,8 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
176
202
|
const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
|
|
177
203
|
const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
|
|
178
204
|
const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
|
|
179
|
-
await
|
|
180
|
-
await
|
|
205
|
+
await ensureBundledSkillsInstalledOnce();
|
|
206
|
+
await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
|
|
181
207
|
const bundledExtensionPaths = getBundledExtensionPaths();
|
|
182
208
|
const services = await createAgentSessionServices({
|
|
183
209
|
cwd,
|
|
@@ -232,7 +258,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
232
258
|
sessionManager: options.noSession
|
|
233
259
|
? SessionManager.inMemory(options.cwd)
|
|
234
260
|
: options.sessionPath
|
|
235
|
-
? openLazySessionManager(options.sessionPath, { cwdOverride: options.cwd })
|
|
261
|
+
? await openLazySessionManager(options.sessionPath, { cwdOverride: options.cwd })
|
|
236
262
|
: SessionManager.create(options.cwd),
|
|
237
263
|
});
|
|
238
264
|
}
|
|
@@ -3,6 +3,8 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import type { RenderedLink } from "./file-links.js";
|
|
4
4
|
type FileLinkOpenerDeps = {
|
|
5
5
|
existsSync: typeof existsSync;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
platform: NodeJS.Platform;
|
|
6
8
|
spawn: typeof spawn;
|
|
7
9
|
};
|
|
8
10
|
export declare function setFileLinkOpenerTestDeps(overrides: Partial<FileLinkOpenerDeps>): () => void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { isAbsolute, posix, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
let deps = { existsSync, spawn };
|
|
5
|
+
let deps = { existsSync, env: process.env, platform: process.platform, spawn };
|
|
6
6
|
export function setFileLinkOpenerTestDeps(overrides) {
|
|
7
7
|
const previous = deps;
|
|
8
8
|
deps = { ...deps, ...overrides };
|
|
@@ -14,13 +14,10 @@ export function openFileLink(link) {
|
|
|
14
14
|
const filePath = link.filePath ?? filePathFromUrl(link.url);
|
|
15
15
|
if (!filePath)
|
|
16
16
|
return false;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (trySpawnCandidates(candidates, [target]))
|
|
17
|
+
const editorLaunch = preferredEditorLaunch(filePath, link.line, link.column);
|
|
18
|
+
if (editorLaunch && trySpawnCandidates(editorLaunch.candidates, editorLaunch.args))
|
|
20
19
|
return true;
|
|
21
|
-
|
|
22
|
-
return spawnDetached("open", ["-a", "Zed", filePath]);
|
|
23
|
-
return false;
|
|
20
|
+
return openPathWithSystemViewer(filePath);
|
|
24
21
|
}
|
|
25
22
|
function filePathFromUrl(url) {
|
|
26
23
|
if (!url.startsWith("file://"))
|
|
@@ -37,29 +34,99 @@ function zedTarget(filePath, line, column) {
|
|
|
37
34
|
return filePath;
|
|
38
35
|
return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
|
|
39
36
|
}
|
|
37
|
+
function gotoTarget(filePath, line, column) {
|
|
38
|
+
if (line === undefined)
|
|
39
|
+
return filePath;
|
|
40
|
+
return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
|
|
41
|
+
}
|
|
42
|
+
function preferredEditorLaunch(filePath, line, column) {
|
|
43
|
+
switch (detectEditor(deps.env)) {
|
|
44
|
+
case "cursor":
|
|
45
|
+
return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.CURSOR_CLI, "cursor") };
|
|
46
|
+
case "jetbrains":
|
|
47
|
+
return {
|
|
48
|
+
args: jetbrainsTargetArgs(filePath, line),
|
|
49
|
+
candidates: commandCandidates(deps.env.JETBRAINS_IDE_CLI, "idea", "idea64", "webstorm", "webstorm64", "pycharm", "pycharm64", "goland", "goland64", "clion", "clion64", "phpstorm", "phpstorm64", "rubymine", "rubymine64", "rider", "rider64"),
|
|
50
|
+
};
|
|
51
|
+
case "vscode":
|
|
52
|
+
return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.VSCODE_CLI, "code", "code-insiders") };
|
|
53
|
+
case "windsurf":
|
|
54
|
+
return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.WINDSURF_CLI, "windsurf") };
|
|
55
|
+
case "zed":
|
|
56
|
+
return { args: [zedTarget(filePath, line, column)], candidates: zedCommandCandidates() };
|
|
57
|
+
default:
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function detectEditor(env) {
|
|
62
|
+
const termProgram = env.TERM_PROGRAM?.trim().toLowerCase();
|
|
63
|
+
const terminalEmulator = env.TERMINAL_EMULATOR?.trim().toLowerCase();
|
|
64
|
+
const terminalProvider = env.TERMINAL_PROVIDER?.trim().toLowerCase();
|
|
65
|
+
if (termProgram === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_TRACE)
|
|
66
|
+
return "cursor";
|
|
67
|
+
if (termProgram === "windsurf")
|
|
68
|
+
return "windsurf";
|
|
69
|
+
if (termProgram === "zed" || env.ZED_CLI)
|
|
70
|
+
return "zed";
|
|
71
|
+
if (termProgram === "vscode" || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE)
|
|
72
|
+
return "vscode";
|
|
73
|
+
if (terminalEmulator?.includes("jetbrains") || terminalProvider === "jetbrains")
|
|
74
|
+
return "jetbrains";
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
40
77
|
function zedCommandCandidates() {
|
|
41
|
-
const candidates = [
|
|
42
|
-
if (
|
|
78
|
+
const candidates = [deps.env.ZED_CLI, "zed", "zeditor"];
|
|
79
|
+
if (deps.platform === "darwin")
|
|
43
80
|
candidates.push("/opt/homebrew/bin/zed", "/usr/local/bin/zed");
|
|
44
81
|
return candidates.filter((candidate) => Boolean(candidate));
|
|
45
82
|
}
|
|
83
|
+
function commandCandidates(primary, ...rest) {
|
|
84
|
+
return [primary, ...rest].filter((candidate) => Boolean(candidate));
|
|
85
|
+
}
|
|
86
|
+
function jetbrainsTargetArgs(filePath, line) {
|
|
87
|
+
if (line === undefined)
|
|
88
|
+
return [filePath];
|
|
89
|
+
return ["--line", `${line}`, filePath];
|
|
90
|
+
}
|
|
46
91
|
function trySpawnCandidates(candidates, args) {
|
|
47
92
|
for (const command of candidates) {
|
|
48
|
-
if (
|
|
49
|
-
continue;
|
|
50
|
-
if (!command.includes("/") && !commandOnPath(command))
|
|
93
|
+
if (!canRunCommand(command))
|
|
51
94
|
continue;
|
|
52
95
|
if (spawnDetached(command, args))
|
|
53
96
|
return true;
|
|
54
97
|
}
|
|
55
98
|
return false;
|
|
56
99
|
}
|
|
100
|
+
function canRunCommand(command) {
|
|
101
|
+
if (hasPathSeparator(command) || isAbsolute(command))
|
|
102
|
+
return deps.existsSync(command);
|
|
103
|
+
return commandOnPath(command);
|
|
104
|
+
}
|
|
105
|
+
function hasPathSeparator(command) {
|
|
106
|
+
return command.includes("/") || command.includes("\\");
|
|
107
|
+
}
|
|
57
108
|
function commandOnPath(command) {
|
|
58
|
-
const pathEntries =
|
|
59
|
-
const extensions =
|
|
60
|
-
? (
|
|
109
|
+
const pathEntries = deps.env.PATH?.split(pathDelimiter()) ?? [];
|
|
110
|
+
const extensions = deps.platform === "win32"
|
|
111
|
+
? (deps.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT", ".COM"])
|
|
61
112
|
: [""];
|
|
62
|
-
return pathEntries.some((entry) => extensions.some((
|
|
113
|
+
return pathEntries.some((entry) => pathCommandCandidates(entry, command, extensions).some((candidate) => deps.existsSync(candidate)));
|
|
114
|
+
}
|
|
115
|
+
function pathDelimiter() {
|
|
116
|
+
return deps.platform === "win32" ? ";" : ":";
|
|
117
|
+
}
|
|
118
|
+
function pathCommandCandidates(entry, command, extensions) {
|
|
119
|
+
const pathApi = deps.platform === "win32" ? win32 : posix;
|
|
120
|
+
if (deps.platform !== "win32" || pathApi.extname(command))
|
|
121
|
+
return [pathApi.join(entry, command)];
|
|
122
|
+
return [pathApi.join(entry, command), ...extensions.map((extension) => pathApi.join(entry, `${command}${extension}`))];
|
|
123
|
+
}
|
|
124
|
+
function openPathWithSystemViewer(filePath) {
|
|
125
|
+
if (deps.platform === "darwin")
|
|
126
|
+
return spawnDetached("open", [filePath]);
|
|
127
|
+
if (deps.platform === "win32")
|
|
128
|
+
return spawnDetached("cmd", ["/c", "start", "", filePath]);
|
|
129
|
+
return spawnDetached("xdg-open", [filePath]);
|
|
63
130
|
}
|
|
64
131
|
function spawnDetached(command, args) {
|
|
65
132
|
try {
|
|
@@ -21,8 +21,6 @@ export type InputFrameCopyRows = {
|
|
|
21
21
|
inputEndRow: number;
|
|
22
22
|
inputSeparatorRow: number;
|
|
23
23
|
inputBottomSeparatorRow: number;
|
|
24
|
-
contentStartColumn: number;
|
|
25
|
-
contentEndColumn: number;
|
|
26
24
|
};
|
|
27
25
|
export type AppMouseControllerHost = {
|
|
28
26
|
terminalColumns(): number;
|
|
@@ -320,7 +320,7 @@ export class AppMouseController {
|
|
|
320
320
|
return false;
|
|
321
321
|
const opened = this.host.openFileLink?.(link) ?? openDetectedFileLink(link);
|
|
322
322
|
if (!opened)
|
|
323
|
-
this.host.showToast("Could not open file link
|
|
323
|
+
this.host.showToast("Could not open file link in the detected editor or system viewer.", "warning");
|
|
324
324
|
return true;
|
|
325
325
|
}
|
|
326
326
|
handleInputScrollBar(event) {
|
|
@@ -789,8 +789,6 @@ export class AppMouseController {
|
|
|
789
789
|
inputEndRow: toScreenRowExclusive(layout.inputStartRow + layout.renderedInput.lines.length),
|
|
790
790
|
inputSeparatorRow: toScreenRow(layout.inputSeparatorRow),
|
|
791
791
|
inputBottomSeparatorRow: toScreenRow(layout.inputBottomSeparatorRow),
|
|
792
|
-
contentStartColumn: 2,
|
|
793
|
-
contentEndColumn: columns,
|
|
794
792
|
};
|
|
795
793
|
}
|
|
796
794
|
getSelectedConversationText(anchor, current) {
|
|
@@ -800,11 +798,13 @@ export class AppMouseController {
|
|
|
800
798
|
const renderedLines = this.host.conversationViewport().slice(width, range.start.line, count);
|
|
801
799
|
const lines = [];
|
|
802
800
|
for (let index = 0; index < count; index += 1) {
|
|
803
|
-
const
|
|
801
|
+
const rendered = renderedLines[index];
|
|
802
|
+
const text = rendered?.text ?? "";
|
|
804
803
|
const line = range.start.line + index;
|
|
805
804
|
const startColumn = line === range.start.line ? range.start.x : 1;
|
|
806
805
|
const endColumn = line === range.end.line ? range.end.x : text.length + 1;
|
|
807
|
-
|
|
806
|
+
const lineText = sliceByDisplayColumns(text, startColumn, endColumn);
|
|
807
|
+
lines.push(lineText.trimEnd());
|
|
808
808
|
}
|
|
809
809
|
return lines.join("\n").replace(/\s+$/u, "");
|
|
810
810
|
}
|
|
@@ -938,13 +938,7 @@ export function screenSelectionLineText(row, text, startColumn, endColumn, input
|
|
|
938
938
|
if (inputFrame && (row === inputFrame.inputSeparatorRow || row === inputFrame.inputBottomSeparatorRow)) {
|
|
939
939
|
return undefined;
|
|
940
940
|
}
|
|
941
|
-
|
|
942
|
-
let copyEndColumn = endColumn;
|
|
943
|
-
if (inputFrame && row >= inputFrame.inputStartRow && row < inputFrame.inputEndRow) {
|
|
944
|
-
copyStartColumn = Math.max(copyStartColumn, inputFrame.contentStartColumn);
|
|
945
|
-
copyEndColumn = Math.min(copyEndColumn, inputFrame.contentEndColumn);
|
|
946
|
-
}
|
|
947
|
-
return sliceByDisplayColumns(text, copyStartColumn, copyEndColumn);
|
|
941
|
+
return sliceByDisplayColumns(text, startColumn, endColumn);
|
|
948
942
|
}
|
|
949
943
|
function sameConversationPoint(left, right) {
|
|
950
944
|
return !!left && left.line === right.line && left.x === right.x;
|
|
@@ -75,7 +75,7 @@ export class ScreenStyler {
|
|
|
75
75
|
}
|
|
76
76
|
styleInputLine(row, text, tagSpans, suggestionSpans, width, tagColor, suggestionColor, frameColor) {
|
|
77
77
|
const colors = this.host.theme.colors;
|
|
78
|
-
const baseOptions = { foreground: colors.
|
|
78
|
+
const baseOptions = { foreground: colors.warning };
|
|
79
79
|
if (this.selectionRangeForRow(row, width, text))
|
|
80
80
|
return this.styleLine(row, text, width, baseOptions);
|
|
81
81
|
const plain = padOrTrimPlain(text, width);
|
|
@@ -8,4 +8,4 @@ export type LazySessionHistoryReader = {
|
|
|
8
8
|
hasOlder(): boolean;
|
|
9
9
|
readOlder(limit: number): Promise<SessionEntry[]>;
|
|
10
10
|
};
|
|
11
|
-
export declare function openLazySessionManager(sessionPath: string, options?: LazySessionManagerOptions): SessionManager
|
|
11
|
+
export declare function openLazySessionManager(sessionPath: string, options?: LazySessionManagerOptions): Promise<SessionManager>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFileSync,
|
|
3
|
-
import { open as openFile } from "node:fs/promises";
|
|
2
|
+
import { appendFileSync, createReadStream, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
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
6
|
import { buildSessionContext, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
@@ -9,8 +9,8 @@ const CURRENT_SESSION_VERSION = 3;
|
|
|
9
9
|
const DEFAULT_TAIL_ENTRY_COUNT = 180;
|
|
10
10
|
const INITIAL_TAIL_BYTES = 256 * 1024;
|
|
11
11
|
const MAX_TAIL_BYTES = 16 * 1024 * 1024;
|
|
12
|
-
export function openLazySessionManager(sessionPath, options = {}) {
|
|
13
|
-
return
|
|
12
|
+
export async function openLazySessionManager(sessionPath, options = {}) {
|
|
13
|
+
return await LazySessionManager.open(sessionPath, options);
|
|
14
14
|
}
|
|
15
15
|
class LazySessionManager {
|
|
16
16
|
sessionFilePath;
|
|
@@ -29,9 +29,18 @@ class LazySessionManager {
|
|
|
29
29
|
this.sessionFilePath = resolve(sessionPath);
|
|
30
30
|
this.sessionDirPath = resolve(options.sessionDir ?? dirname(this.sessionFilePath));
|
|
31
31
|
this.tailEntryCount = Math.max(1, Math.floor(options.tailEntryCount ?? DEFAULT_TAIL_ENTRY_COUNT));
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
34
|
-
|
|
32
|
+
this.cwdPath = resolve(options.cwdOverride ?? process.cwd());
|
|
33
|
+
this.header = createSessionHeader(this.cwdPath);
|
|
34
|
+
}
|
|
35
|
+
static async open(sessionPath, options = {}) {
|
|
36
|
+
const manager = new LazySessionManager(sessionPath, options);
|
|
37
|
+
await manager.initialize(options.cwdOverride);
|
|
38
|
+
return manager;
|
|
39
|
+
}
|
|
40
|
+
async initialize(cwdOverride) {
|
|
41
|
+
this.header = await this.loadHeaderAsync(cwdOverride);
|
|
42
|
+
this.cwdPath = resolve(cwdOverride ?? this.header.cwd ?? process.cwd());
|
|
43
|
+
await this.loadTailEntriesAsync();
|
|
35
44
|
}
|
|
36
45
|
setSessionFile(sessionFile) {
|
|
37
46
|
if (this.hydrated) {
|
|
@@ -40,9 +49,7 @@ class LazySessionManager {
|
|
|
40
49
|
}
|
|
41
50
|
this.sessionFilePath = resolve(sessionFile);
|
|
42
51
|
this.sessionDirPath = dirname(this.sessionFilePath);
|
|
43
|
-
|
|
44
|
-
this.cwdPath = resolve(this.header.cwd || this.cwdPath);
|
|
45
|
-
this.loadTailEntries();
|
|
52
|
+
throw new Error("LazySessionManager.setSessionFile() before hydration is unsupported");
|
|
46
53
|
}
|
|
47
54
|
newSession(options) {
|
|
48
55
|
if (this.hydrated)
|
|
@@ -106,16 +113,25 @@ class LazySessionManager {
|
|
|
106
113
|
if (this.hydrated || this.tailStartOffset <= 0)
|
|
107
114
|
return undefined;
|
|
108
115
|
let cursorOffset = this.tailStartOffset;
|
|
109
|
-
|
|
116
|
+
let firstEntryOffset = 0;
|
|
117
|
+
let firstEntryOffsetPromise;
|
|
118
|
+
const loadFirstEntryOffset = async () => {
|
|
119
|
+
firstEntryOffsetPromise ??= readFirstSessionEntryOffset(this.sessionFilePath).then((offset) => {
|
|
120
|
+
firstEntryOffset = offset;
|
|
121
|
+
return offset;
|
|
122
|
+
});
|
|
123
|
+
return await firstEntryOffsetPromise;
|
|
124
|
+
};
|
|
110
125
|
return {
|
|
111
126
|
hasOlder: () => cursorOffset > firstEntryOffset,
|
|
112
127
|
readOlder: async (limit) => {
|
|
113
|
-
|
|
128
|
+
const resolvedFirstEntryOffset = await loadFirstEntryOffset();
|
|
129
|
+
if (cursorOffset <= resolvedFirstEntryOffset)
|
|
114
130
|
return [];
|
|
115
131
|
const result = await readSessionEntriesBeforeOffset(this.sessionFilePath, cursorOffset, Math.max(1, Math.floor(limit)));
|
|
116
132
|
cursorOffset = result.startOffset;
|
|
117
133
|
if (result.entries.length === 0)
|
|
118
|
-
cursorOffset =
|
|
134
|
+
cursorOffset = resolvedFirstEntryOffset;
|
|
119
135
|
return result.entries;
|
|
120
136
|
},
|
|
121
137
|
};
|
|
@@ -256,18 +272,17 @@ class LazySessionManager {
|
|
|
256
272
|
}
|
|
257
273
|
return this.hydrated;
|
|
258
274
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const result = readTailSessionEntries(this.sessionFilePath, this.tailEntryCount);
|
|
275
|
+
async loadHeaderAsync(cwdOverride) {
|
|
276
|
+
const existingHeader = await readSessionHeaderFast(this.sessionFilePath);
|
|
277
|
+
if (existingHeader)
|
|
278
|
+
return existingHeader;
|
|
279
|
+
await mkdir(dirname(this.sessionFilePath), { recursive: true });
|
|
280
|
+
const header = createSessionHeader(resolve(cwdOverride ?? process.cwd()));
|
|
281
|
+
await writeFile(this.sessionFilePath, `${JSON.stringify(header)}\n`, "utf8");
|
|
282
|
+
return header;
|
|
283
|
+
}
|
|
284
|
+
async loadTailEntriesAsync() {
|
|
285
|
+
const result = await readTailSessionEntries(this.sessionFilePath, this.tailEntryCount);
|
|
271
286
|
this.entries = result.entries;
|
|
272
287
|
this.tailStartOffset = result.startOffset;
|
|
273
288
|
this.rebuildIndexes();
|
|
@@ -338,8 +353,8 @@ function createSessionHeader(cwd) {
|
|
|
338
353
|
function createSessionId() {
|
|
339
354
|
return randomUUID();
|
|
340
355
|
}
|
|
341
|
-
function readSessionHeaderFast(filePath) {
|
|
342
|
-
const line = readFirstLine(filePath, 64 * 1024);
|
|
356
|
+
async function readSessionHeaderFast(filePath) {
|
|
357
|
+
const line = await readFirstLine(filePath, 64 * 1024);
|
|
343
358
|
if (!line)
|
|
344
359
|
return undefined;
|
|
345
360
|
try {
|
|
@@ -353,12 +368,12 @@ function readSessionHeaderFast(filePath) {
|
|
|
353
368
|
return undefined;
|
|
354
369
|
}
|
|
355
370
|
}
|
|
356
|
-
function readFirstLine(filePath, maxBytes) {
|
|
357
|
-
let
|
|
371
|
+
async function readFirstLine(filePath, maxBytes) {
|
|
372
|
+
let file;
|
|
358
373
|
try {
|
|
359
|
-
|
|
374
|
+
file = await openFile(filePath, "r");
|
|
360
375
|
const buffer = Buffer.alloc(maxBytes);
|
|
361
|
-
const bytesRead =
|
|
376
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
362
377
|
const text = buffer.toString("utf8", 0, bytesRead);
|
|
363
378
|
return text.split("\n")[0];
|
|
364
379
|
}
|
|
@@ -366,16 +381,15 @@ function readFirstLine(filePath, maxBytes) {
|
|
|
366
381
|
return undefined;
|
|
367
382
|
}
|
|
368
383
|
finally {
|
|
369
|
-
|
|
370
|
-
closeSync(fd);
|
|
384
|
+
await file?.close();
|
|
371
385
|
}
|
|
372
386
|
}
|
|
373
|
-
function readFirstSessionEntryOffset(filePath) {
|
|
374
|
-
let
|
|
387
|
+
async function readFirstSessionEntryOffset(filePath) {
|
|
388
|
+
let file;
|
|
375
389
|
try {
|
|
376
|
-
|
|
390
|
+
file = await openFile(filePath, "r");
|
|
377
391
|
const buffer = Buffer.alloc(64 * 1024);
|
|
378
|
-
const bytesRead =
|
|
392
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
379
393
|
const entries = parseSessionEntryBufferLines(buffer.subarray(0, bytesRead), 0);
|
|
380
394
|
return entries[0]?.offset ?? 0;
|
|
381
395
|
}
|
|
@@ -383,34 +397,30 @@ function readFirstSessionEntryOffset(filePath) {
|
|
|
383
397
|
return 0;
|
|
384
398
|
}
|
|
385
399
|
finally {
|
|
386
|
-
|
|
387
|
-
closeSync(fd);
|
|
400
|
+
await file?.close();
|
|
388
401
|
}
|
|
389
402
|
}
|
|
390
|
-
function readTailSessionEntries(filePath, limit) {
|
|
391
|
-
|
|
392
|
-
return { entries: [], startOffset: 0 };
|
|
393
|
-
const size = statSync(filePath).size;
|
|
403
|
+
async function readTailSessionEntries(filePath, limit) {
|
|
404
|
+
const size = await stat(filePath).then((result) => result.size).catch(() => 0);
|
|
394
405
|
if (size <= 0)
|
|
395
406
|
return { entries: [], startOffset: 0 };
|
|
396
407
|
let byteCount = Math.min(size, INITIAL_TAIL_BYTES);
|
|
397
408
|
const maxBytes = Math.min(size, MAX_TAIL_BYTES);
|
|
398
409
|
while (byteCount <= maxBytes) {
|
|
399
|
-
const result = readTailSessionEntriesWithByteCount(filePath, byteCount, limit);
|
|
410
|
+
const result = await readTailSessionEntriesWithByteCount(filePath, byteCount, limit, size);
|
|
400
411
|
if (result.entries.length >= limit || byteCount >= maxBytes || byteCount >= size)
|
|
401
412
|
return result;
|
|
402
413
|
byteCount = Math.min(size, Math.max(byteCount + 1, byteCount * 2));
|
|
403
414
|
}
|
|
404
415
|
return { entries: [], startOffset: 0 };
|
|
405
416
|
}
|
|
406
|
-
function readTailSessionEntriesWithByteCount(filePath, byteCount, limit) {
|
|
407
|
-
let
|
|
417
|
+
async function readTailSessionEntriesWithByteCount(filePath, byteCount, limit, size) {
|
|
418
|
+
let file;
|
|
408
419
|
try {
|
|
409
|
-
const size = statSync(filePath).size;
|
|
410
420
|
const start = Math.max(0, size - byteCount);
|
|
411
421
|
const buffer = Buffer.alloc(size - start);
|
|
412
|
-
|
|
413
|
-
|
|
422
|
+
file = await openFile(filePath, "r");
|
|
423
|
+
await file.read(buffer, 0, buffer.length, start);
|
|
414
424
|
let parseStart = 0;
|
|
415
425
|
if (start > 0) {
|
|
416
426
|
const firstNewline = buffer.indexOf(10);
|
|
@@ -422,12 +432,14 @@ function readTailSessionEntriesWithByteCount(filePath, byteCount, limit) {
|
|
|
422
432
|
return { entries: [], startOffset: 0 };
|
|
423
433
|
}
|
|
424
434
|
finally {
|
|
425
|
-
|
|
426
|
-
closeSync(fd);
|
|
435
|
+
await file?.close();
|
|
427
436
|
}
|
|
428
437
|
}
|
|
429
438
|
async function readSessionEntriesBeforeOffset(filePath, endOffset, limit) {
|
|
430
|
-
if (
|
|
439
|
+
if (endOffset <= 0)
|
|
440
|
+
return { entries: [], startOffset: 0 };
|
|
441
|
+
const exists = await stat(filePath).then(() => true).catch(() => false);
|
|
442
|
+
if (!exists)
|
|
431
443
|
return { entries: [], startOffset: 0 };
|
|
432
444
|
let byteCount = Math.min(endOffset, INITIAL_TAIL_BYTES);
|
|
433
445
|
const maxBytes = Math.min(endOffset, MAX_TAIL_BYTES);
|
|
@@ -51,6 +51,12 @@ export declare class AppQueuedMessageController {
|
|
|
51
51
|
findQueuedEntry(entryId: string): Extract<Entry, {
|
|
52
52
|
kind: "queued";
|
|
53
53
|
}> | undefined;
|
|
54
|
+
queuedEntries(): Extract<Entry, {
|
|
55
|
+
kind: "queued";
|
|
56
|
+
}>[];
|
|
57
|
+
deferredQueuedEntries(): Extract<Entry, {
|
|
58
|
+
kind: "queued";
|
|
59
|
+
}>[];
|
|
54
60
|
private shouldDeferUserMessage;
|
|
55
61
|
deferUserMessage(message: SubmittedUserMessage): void;
|
|
56
62
|
private rewriteSdkQueuedMessages;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createId } from "../id.js";
|
|
2
2
|
import { stringifyUnknown, submittedUserDisplayText } from "../rendering/message-content.js";
|
|
3
|
+
import { deferredQueuedMessageEntries, queuedMessageEntries } from "./queued-message-entries.js";
|
|
3
4
|
export class AppQueuedMessageController {
|
|
4
5
|
host;
|
|
5
6
|
deferredUserMessages = [];
|
|
@@ -228,9 +229,16 @@ export class AppQueuedMessageController {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
findQueuedEntry(entryId) {
|
|
231
|
-
const entry = this.
|
|
232
|
+
const entry = this.queuedEntries().find((candidate) => candidate.id === entryId)
|
|
233
|
+
?? this.host.visibleEntries().find((candidate) => candidate.id === entryId);
|
|
232
234
|
return entry?.kind === "queued" ? entry : undefined;
|
|
233
235
|
}
|
|
236
|
+
queuedEntries() {
|
|
237
|
+
return queuedMessageEntries(this.host.runtime()?.session, this.deferredUserMessages);
|
|
238
|
+
}
|
|
239
|
+
deferredQueuedEntries() {
|
|
240
|
+
return deferredQueuedMessageEntries(this.deferredUserMessages);
|
|
241
|
+
}
|
|
234
242
|
shouldDeferUserMessage(session) {
|
|
235
243
|
return session.isCompacting || this.promptSubmissionInFlight;
|
|
236
244
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Entry, SubmittedUserMessage } from "../types.js";
|
|
3
|
+
export type QueuedEntry = Extract<Entry, {
|
|
4
|
+
kind: "queued";
|
|
5
|
+
}>;
|
|
6
|
+
export declare function sdkQueuedMessageEntries(session: AgentSession | undefined): QueuedEntry[];
|
|
7
|
+
export declare function deferredQueuedMessageEntries(messages: readonly SubmittedUserMessage[]): QueuedEntry[];
|
|
8
|
+
export declare function queuedMessageEntries(session: AgentSession | undefined, deferredUserMessages: readonly SubmittedUserMessage[]): QueuedEntry[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { shortHash } from "../rendering/render-text.js";
|
|
2
|
+
export function sdkQueuedMessageEntries(session) {
|
|
3
|
+
const entries = [];
|
|
4
|
+
for (const [index, text] of (session?.getSteeringMessages() ?? []).entries()) {
|
|
5
|
+
entries.push({
|
|
6
|
+
id: `queued-sdk-steering-${index}-${shortHash(text)}`,
|
|
7
|
+
kind: "queued",
|
|
8
|
+
mode: "steering",
|
|
9
|
+
text,
|
|
10
|
+
queueSource: "sdk-steering",
|
|
11
|
+
queueIndex: index,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
for (const [index, text] of (session?.getFollowUpMessages() ?? []).entries()) {
|
|
15
|
+
entries.push({
|
|
16
|
+
id: `queued-sdk-follow-up-${index}-${shortHash(text)}`,
|
|
17
|
+
kind: "queued",
|
|
18
|
+
mode: "follow-up",
|
|
19
|
+
text,
|
|
20
|
+
queueSource: "sdk-follow-up",
|
|
21
|
+
queueIndex: index,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
export function deferredQueuedMessageEntries(messages) {
|
|
27
|
+
return messages.map((message, index) => ({
|
|
28
|
+
id: `${message.id}-${index}`,
|
|
29
|
+
kind: "queued",
|
|
30
|
+
mode: "steering",
|
|
31
|
+
text: message.displayText,
|
|
32
|
+
queueSource: "deferred",
|
|
33
|
+
queueIndex: index,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
export function queuedMessageEntries(session, deferredUserMessages) {
|
|
37
|
+
return [
|
|
38
|
+
...sdkQueuedMessageEntries(session),
|
|
39
|
+
...deferredQueuedMessageEntries(deferredUserMessages),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
@@ -48,17 +48,25 @@ export type AppSessionLifecycleHost = {
|
|
|
48
48
|
restoreTabsAfterStartup(): Promise<void>;
|
|
49
49
|
render(): void;
|
|
50
50
|
};
|
|
51
|
+
export type BindCurrentSessionOptions = {
|
|
52
|
+
awaitExtensions?: boolean;
|
|
53
|
+
};
|
|
51
54
|
export declare class AppSessionLifecycleController {
|
|
52
55
|
private readonly host;
|
|
53
56
|
private unsubscribe;
|
|
57
|
+
private extensionBindPromise;
|
|
58
|
+
private extensionBindRuntime;
|
|
59
|
+
private extensionBindSession;
|
|
54
60
|
constructor(host: AppSessionLifecycleHost);
|
|
55
61
|
start(): Promise<void>;
|
|
56
|
-
bindCurrentSession(): Promise<void>;
|
|
62
|
+
bindCurrentSession(options?: BindCurrentSessionOptions): Promise<void>;
|
|
57
63
|
unsubscribeSession(): void;
|
|
58
64
|
afterSessionReplacement(message?: string): void;
|
|
59
65
|
private loadReplacementHistory;
|
|
60
66
|
resetSessionView(): void;
|
|
61
67
|
loadSessionHistory(): void;
|
|
62
68
|
requireRuntime(): AgentSessionRuntime;
|
|
69
|
+
private bindSessionExtensions;
|
|
70
|
+
private isCurrentRuntimeSession;
|
|
63
71
|
private extensionUiScope;
|
|
64
72
|
}
|