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.
Files changed (67) hide show
  1. package/dist/app/app.js +8 -6
  2. package/dist/app/constants.d.ts +1 -0
  3. package/dist/app/constants.js +1 -0
  4. package/dist/app/input/voice-controller.js +16 -12
  5. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  6. package/dist/app/popup/popup-menu-controller.js +7 -8
  7. package/dist/app/process.js +7 -0
  8. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  9. package/dist/app/rendering/conversation-viewport.js +4 -35
  10. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  11. package/dist/app/rendering/editor-layout-renderer.js +25 -16
  12. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  13. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  14. package/dist/app/rendering/render-controller.d.ts +2 -0
  15. package/dist/app/rendering/render-controller.js +26 -25
  16. package/dist/app/rendering/render-text.js +2 -2
  17. package/dist/app/rendering/status-line-renderer.js +1 -1
  18. package/dist/app/rendering/tab-line-renderer.js +3 -3
  19. package/dist/app/runtime.js +29 -3
  20. package/dist/app/screen/file-link-opener.d.ts +2 -0
  21. package/dist/app/screen/file-link-opener.js +84 -17
  22. package/dist/app/screen/mouse-controller.d.ts +0 -2
  23. package/dist/app/screen/mouse-controller.js +6 -12
  24. package/dist/app/screen/screen-styler.js +1 -1
  25. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  26. package/dist/app/session/lazy-session-manager.js +64 -52
  27. package/dist/app/session/queued-message-controller.d.ts +6 -0
  28. package/dist/app/session/queued-message-controller.js +9 -1
  29. package/dist/app/session/queued-message-entries.d.ts +8 -0
  30. package/dist/app/session/queued-message-entries.js +41 -0
  31. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  32. package/dist/app/session/session-lifecycle-controller.js +45 -11
  33. package/dist/app/session/tabs-controller.d.ts +11 -1
  34. package/dist/app/session/tabs-controller.js +197 -30
  35. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  36. package/dist/app/terminal/terminal-controller.js +7 -5
  37. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  38. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  39. package/dist/theme.d.ts +3 -0
  40. package/dist/theme.js +8 -2
  41. package/extensions/session-title/config.ts +3 -3
  42. package/extensions/session-title/index.ts +60 -5
  43. package/external/pi-tools-suite/README.md +3 -2
  44. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  45. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  46. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  47. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  49. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  50. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  51. package/external/pi-tools-suite/src/config.ts +13 -0
  52. package/external/pi-tools-suite/src/dcp/state.ts +9 -4
  53. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
  54. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
  55. package/external/pi-tools-suite/src/index.ts +1 -0
  56. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  57. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  58. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  59. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  60. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
  61. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  62. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  63. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  64. package/package.json +3 -14
  65. package/schemas/pi-tools-suite.json +19 -0
  66. package/apps/desktop-tauri/README.md +0 -103
  67. 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.inputBorder,
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.inputBorder,
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.inputBorder,
122
+ foreground: this.host.theme.colors.tabBorder,
123
123
  });
124
124
  }
125
125
  bottomText(layout, width) {
@@ -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 ensureBundledSkillsInstalled();
180
- await ensurePiToolsSuiteExtensionInstalled({ agentDir });
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 { delimiter, join } from "node:path";
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 target = zedTarget(filePath, link.line, link.column);
18
- const candidates = zedCommandCandidates();
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
- if (process.platform === "darwin")
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 = [process.env.ZED_CLI, "zed", "zeditor"];
42
- if (process.platform === "darwin")
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 (command.includes("/") && !deps.existsSync(command))
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 = process.env.PATH?.split(delimiter) ?? [];
59
- const extensions = process.platform === "win32"
60
- ? (process.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT", ".COM"])
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((extension) => deps.existsSync(join(entry, `${command}${extension}`))));
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. Install the Zed CLI or set ZED_CLI.", "warning");
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 text = renderedLines[index]?.text ?? "";
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
- lines.push(sliceByDisplayColumns(text, startColumn, endColumn).trimEnd());
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
- let copyStartColumn = startColumn;
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.inputForeground };
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, closeSync, createReadStream, existsSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "node:fs";
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 new LazySessionManager(sessionPath, options);
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.header = this.loadHeader(options.cwdOverride);
33
- this.cwdPath = resolve(options.cwdOverride ?? this.header.cwd ?? process.cwd());
34
- this.loadTailEntries();
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
- this.header = this.loadHeader(this.cwdPath);
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
- const firstEntryOffset = readFirstSessionEntryOffset(this.sessionFilePath);
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
- if (cursorOffset <= firstEntryOffset)
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 = firstEntryOffset;
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
- loadHeader(cwdOverride) {
260
- if (!existsSync(this.sessionFilePath)) {
261
- mkdirSync(dirname(this.sessionFilePath), { recursive: true });
262
- const header = createSessionHeader(resolve(cwdOverride ?? process.cwd()));
263
- writeFileSync(this.sessionFilePath, `${JSON.stringify(header)}\n`, "utf8");
264
- return header;
265
- }
266
- const header = readSessionHeaderFast(this.sessionFilePath);
267
- return header ?? createSessionHeader(resolve(cwdOverride ?? process.cwd()));
268
- }
269
- loadTailEntries() {
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 fd;
371
+ async function readFirstLine(filePath, maxBytes) {
372
+ let file;
358
373
  try {
359
- fd = openSync(filePath, "r");
374
+ file = await openFile(filePath, "r");
360
375
  const buffer = Buffer.alloc(maxBytes);
361
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
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
- if (fd !== undefined)
370
- closeSync(fd);
384
+ await file?.close();
371
385
  }
372
386
  }
373
- function readFirstSessionEntryOffset(filePath) {
374
- let fd;
387
+ async function readFirstSessionEntryOffset(filePath) {
388
+ let file;
375
389
  try {
376
- fd = openSync(filePath, "r");
390
+ file = await openFile(filePath, "r");
377
391
  const buffer = Buffer.alloc(64 * 1024);
378
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
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
- if (fd !== undefined)
387
- closeSync(fd);
400
+ await file?.close();
388
401
  }
389
402
  }
390
- function readTailSessionEntries(filePath, limit) {
391
- if (!existsSync(filePath))
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 fd;
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
- fd = openSync(filePath, "r");
413
- readSync(fd, buffer, 0, buffer.length, start);
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
- if (fd !== undefined)
426
- closeSync(fd);
435
+ await file?.close();
427
436
  }
428
437
  }
429
438
  async function readSessionEntriesBeforeOffset(filePath, endOffset, limit) {
430
- if (!existsSync(filePath) || endOffset <= 0)
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.host.visibleEntries().find((candidate) => candidate.id === entryId);
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
  }