pi-ui-extend 0.1.18 → 0.1.20

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 (70) 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 +2 -1
  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 +29 -20
  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 +40 -49
  16. package/dist/app/rendering/render-text.js +2 -2
  17. package/dist/app/rendering/status-line-renderer.d.ts +2 -0
  18. package/dist/app/rendering/status-line-renderer.js +75 -29
  19. package/dist/app/rendering/tab-line-renderer.js +3 -3
  20. package/dist/app/runtime.js +29 -3
  21. package/dist/app/screen/file-link-opener.d.ts +2 -0
  22. package/dist/app/screen/file-link-opener.js +84 -17
  23. package/dist/app/screen/mouse-controller.d.ts +1 -3
  24. package/dist/app/screen/mouse-controller.js +14 -14
  25. package/dist/app/screen/screen-styler.js +1 -1
  26. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  27. package/dist/app/session/lazy-session-manager.js +64 -52
  28. package/dist/app/session/queued-message-controller.d.ts +6 -0
  29. package/dist/app/session/queued-message-controller.js +9 -1
  30. package/dist/app/session/queued-message-entries.d.ts +8 -0
  31. package/dist/app/session/queued-message-entries.js +41 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  33. package/dist/app/session/session-lifecycle-controller.js +45 -11
  34. package/dist/app/session/tabs-controller.d.ts +11 -1
  35. package/dist/app/session/tabs-controller.js +197 -30
  36. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  37. package/dist/app/terminal/terminal-controller.js +7 -5
  38. package/dist/app/types.d.ts +1 -1
  39. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  40. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  41. package/dist/theme.d.ts +11 -0
  42. package/dist/theme.js +26 -4
  43. package/extensions/question/tui.ts +1 -1
  44. package/extensions/session-title/config.ts +3 -3
  45. package/extensions/session-title/index.ts +60 -5
  46. package/external/pi-tools-suite/README.md +3 -2
  47. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  48. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  49. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  50. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  52. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  53. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  54. package/external/pi-tools-suite/src/config.ts +13 -0
  55. package/external/pi-tools-suite/src/dcp/state.ts +9 -4
  56. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
  57. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +683 -0
  58. package/external/pi-tools-suite/src/index.ts +1 -0
  59. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  60. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  61. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  62. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  63. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
  64. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  65. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  66. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  67. package/package.json +3 -14
  68. package/schemas/pi-tools-suite.json +19 -0
  69. package/apps/desktop-tauri/README.md +0 -103
  70. package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
@@ -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) {
@@ -784,14 +784,15 @@ export class AppMouseController {
784
784
  const topOffset = editorLayoutTopOffset(tabPanelRows);
785
785
  const toScreenRow = (layoutRow) => Math.max(1, Math.min(terminalRows, topOffset + layoutRow));
786
786
  const toScreenRowExclusive = (layoutRow) => Math.max(1, Math.min(terminalRows + 1, topOffset + layoutRow));
787
- return {
787
+ const inputFrame = {
788
788
  inputStartRow: toScreenRow(layout.inputStartRow),
789
789
  inputEndRow: toScreenRowExclusive(layout.inputStartRow + layout.renderedInput.lines.length),
790
790
  inputSeparatorRow: toScreenRow(layout.inputSeparatorRow),
791
- inputBottomSeparatorRow: toScreenRow(layout.inputBottomSeparatorRow),
792
- contentStartColumn: 2,
793
- contentEndColumn: columns,
794
791
  };
792
+ if (layout.inputBottomSeparatorRow !== undefined) {
793
+ inputFrame.inputBottomSeparatorRow = toScreenRow(layout.inputBottomSeparatorRow);
794
+ }
795
+ return inputFrame;
795
796
  }
796
797
  getSelectedConversationText(anchor, current) {
797
798
  const range = orderedConversationSelection(anchor, current);
@@ -800,11 +801,13 @@ export class AppMouseController {
800
801
  const renderedLines = this.host.conversationViewport().slice(width, range.start.line, count);
801
802
  const lines = [];
802
803
  for (let index = 0; index < count; index += 1) {
803
- const text = renderedLines[index]?.text ?? "";
804
+ const rendered = renderedLines[index];
805
+ const text = rendered?.text ?? "";
804
806
  const line = range.start.line + index;
805
807
  const startColumn = line === range.start.line ? range.start.x : 1;
806
808
  const endColumn = line === range.end.line ? range.end.x : text.length + 1;
807
- lines.push(sliceByDisplayColumns(text, startColumn, endColumn).trimEnd());
809
+ const lineText = sliceByDisplayColumns(text, startColumn, endColumn);
810
+ lines.push(lineText.trimEnd());
808
811
  }
809
812
  return lines.join("\n").replace(/\s+$/u, "");
810
813
  }
@@ -935,16 +938,13 @@ function orderedConversationSelection(anchor, current) {
935
938
  return anchor.x <= current.x ? { start: anchor, end: current } : { start: current, end: anchor };
936
939
  }
937
940
  export function screenSelectionLineText(row, text, startColumn, endColumn, inputFrame) {
938
- if (inputFrame && (row === inputFrame.inputSeparatorRow || row === inputFrame.inputBottomSeparatorRow)) {
941
+ if (inputFrame && row === inputFrame.inputSeparatorRow) {
939
942
  return undefined;
940
943
  }
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);
944
+ if (inputFrame?.inputBottomSeparatorRow !== undefined && row === inputFrame.inputBottomSeparatorRow) {
945
+ return undefined;
946
946
  }
947
- return sliceByDisplayColumns(text, copyStartColumn, copyEndColumn);
947
+ return sliceByDisplayColumns(text, startColumn, endColumn);
948
948
  }
949
949
  function sameConversationPoint(left, right) {
950
950
  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.userForeground };
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
  }
@@ -5,6 +5,9 @@ import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-
5
5
  export class AppSessionLifecycleController {
6
6
  host;
7
7
  unsubscribe;
8
+ extensionBindPromise;
9
+ extensionBindRuntime;
10
+ extensionBindSession;
8
11
  constructor(host) {
9
12
  this.host = host;
10
13
  }
@@ -25,9 +28,9 @@ export class AppSessionLifecycleController {
25
28
  }
26
29
  this.host.setRuntime(runtime);
27
30
  runtime.setRebindSession(async () => {
28
- await this.bindCurrentSession();
31
+ await this.bindCurrentSession({ awaitExtensions: false });
29
32
  });
30
- await this.bindCurrentSession();
33
+ await this.bindCurrentSession({ awaitExtensions: false });
31
34
  if (isEmptyStartupSession(runtime)) {
32
35
  this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(runtime) });
33
36
  }
@@ -64,21 +67,28 @@ export class AppSessionLifecycleController {
64
67
  this.host.render();
65
68
  }
66
69
  }
67
- async bindCurrentSession() {
70
+ async bindCurrentSession(options = {}) {
68
71
  const runtime = this.requireRuntime();
72
+ const session = runtime.session;
69
73
  this.unsubscribe?.();
70
- this.unsubscribe = runtime.session.subscribe((event) => {
74
+ this.unsubscribe = session.subscribe((event) => {
71
75
  this.host.handleSessionEvent(event);
72
76
  });
73
77
  this.host.closeSdkMenuForBind();
74
- const extensionUiScope = this.extensionUiScope(runtime.session);
78
+ const extensionUiScope = this.extensionUiScope(session);
75
79
  this.host.clearExtensionWidgets(extensionUiScope, { cancelCustomUi: false });
76
- await runtime.session.bindExtensions({
77
- uiContext: this.host.createExtensionUIContext(extensionUiScope),
78
- commandContextActions: this.host.createExtensionCommandContextActions(runtime),
79
- shutdownHandler: this.host.extensionShutdownHandler(),
80
- onError: (error) => this.host.handleExtensionError(error),
81
- });
80
+ const bindPromise = this.bindSessionExtensions(runtime, session, extensionUiScope);
81
+ if (options.awaitExtensions === false) {
82
+ void bindPromise.catch((error) => {
83
+ if (!this.isCurrentRuntimeSession(runtime, session))
84
+ return;
85
+ this.host.addEntry({ id: createId("error"), kind: "error", text: `Extension bind failed: ${stringifyUnknown(error)}` });
86
+ this.host.showToast("Extension initialization failed", "error");
87
+ this.host.render();
88
+ });
89
+ return;
90
+ }
91
+ await bindPromise;
82
92
  }
83
93
  unsubscribeSession() {
84
94
  this.unsubscribe?.();
@@ -123,6 +133,30 @@ export class AppSessionLifecycleController {
123
133
  throw new Error("Runtime is not initialized");
124
134
  return runtime;
125
135
  }
136
+ bindSessionExtensions(runtime, session, scopeKey) {
137
+ if (this.extensionBindPromise && this.extensionBindRuntime === runtime && this.extensionBindSession === session) {
138
+ return this.extensionBindPromise;
139
+ }
140
+ const promise = session.bindExtensions({
141
+ uiContext: this.host.createExtensionUIContext(scopeKey),
142
+ commandContextActions: this.host.createExtensionCommandContextActions(runtime),
143
+ shutdownHandler: this.host.extensionShutdownHandler(),
144
+ onError: (error) => this.host.handleExtensionError(error),
145
+ }).finally(() => {
146
+ if (this.extensionBindPromise !== promise)
147
+ return;
148
+ this.extensionBindPromise = undefined;
149
+ this.extensionBindRuntime = undefined;
150
+ this.extensionBindSession = undefined;
151
+ });
152
+ this.extensionBindPromise = promise;
153
+ this.extensionBindRuntime = runtime;
154
+ this.extensionBindSession = session;
155
+ return promise;
156
+ }
157
+ isCurrentRuntimeSession(runtime, session) {
158
+ return this.host.isRunning() && this.host.runtime() === runtime && runtime.session === session;
159
+ }
126
160
  extensionUiScope(session) {
127
161
  return session.sessionFile ?? session.sessionId;
128
162
  }
@@ -1,4 +1,5 @@
1
1
  import { type AgentSession, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
+ import type { BindCurrentSessionOptions } from "./session-lifecycle-controller.js";
2
3
  import type { InputEditorDraftState } from "../../input-editor.js";
3
4
  import type { AppBlinkController } from "../screen/blink-controller.js";
4
5
  import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessage } from "../types.js";
@@ -10,7 +11,7 @@ export type AppTabsControllerHost = {
10
11
  runtime(): AgentSessionRuntime | undefined;
11
12
  createRuntimeForNewSession(): Promise<AgentSessionRuntime>;
12
13
  createRuntimeForSession(sessionPath: string): Promise<AgentSessionRuntime>;
13
- activateRuntime(runtime: AgentSessionRuntime): Promise<void>;
14
+ activateRuntime(runtime: AgentSessionRuntime, options?: BindCurrentSessionOptions): Promise<void>;
14
15
  disposeRuntime(runtime: AgentSessionRuntime): Promise<void>;
15
16
  isRunning(): boolean;
16
17
  setStatus(status: string): void;
@@ -37,7 +38,9 @@ export declare class AppTabsController {
37
38
  private readonly host;
38
39
  private readonly tabItems;
39
40
  private readonly runtimesByTabId;
41
+ private readonly runtimeLoadsByTabId;
40
42
  private readonly runtimeSubscriptionsByTabId;
43
+ private readonly runtimeRefreshTimersByTabId;
41
44
  private readonly inputStatesByTabId;
42
45
  private readonly deferredUserMessagesByTabId;
43
46
  private activeTabId;
@@ -46,6 +49,8 @@ export declare class AppTabsController {
46
49
  private restored;
47
50
  private retentionCleanupRunning;
48
51
  private retentionCleanupScheduled;
52
+ private prewarmScheduled;
53
+ private prewarmRunning;
49
54
  constructor(host: AppTabsControllerHost);
50
55
  tabs(): readonly SessionTab[];
51
56
  isSwitching(): boolean;
@@ -81,6 +86,9 @@ export declare class AppTabsController {
81
86
  private clearRuntimeSubscriptions;
82
87
  private observeRuntimeForTab;
83
88
  private shouldSyncTabFromRuntimeEvent;
89
+ private shouldScheduleDelayedSyncForRuntimeEvent;
90
+ private scheduleDelayedRuntimeSync;
91
+ private clearRuntimeRefreshTimers;
84
92
  private syncTabFromObservedRuntime;
85
93
  private storeActiveInputState;
86
94
  private storeActiveDeferredUserMessages;
@@ -115,6 +123,8 @@ export declare class AppTabsController {
115
123
  private filePath;
116
124
  private sessionDir;
117
125
  private scheduleProjectSessionRetention;
126
+ private scheduleTabPrewarm;
127
+ private prewarmTabs;
118
128
  private cleanupOldProjectSessions;
119
129
  private preservedSessionPaths;
120
130
  private maxProjectSessions;