pi-ui-extend 0.1.70 → 0.1.72

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 (58) hide show
  1. package/dist/app/app.d.ts +2 -0
  2. package/dist/app/app.js +39 -1
  3. package/dist/app/commands/command-host.d.ts +7 -0
  4. package/dist/app/commands/command-host.js +10 -1
  5. package/dist/app/commands/command-model-actions.d.ts +1 -1
  6. package/dist/app/commands/command-model-actions.js +36 -2
  7. package/dist/app/commands/command-navigation-actions.d.ts +1 -1
  8. package/dist/app/commands/command-navigation-actions.js +40 -6
  9. package/dist/app/commands/command-session-actions.d.ts +1 -1
  10. package/dist/app/commands/command-session-actions.js +56 -0
  11. package/dist/app/commands/shell-controller.d.ts +9 -2
  12. package/dist/app/commands/shell-controller.js +32 -21
  13. package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
  14. package/dist/app/extensions/extension-actions-controller.js +25 -7
  15. package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
  16. package/dist/app/extensions/extension-ui-controller.js +104 -64
  17. package/dist/app/icons.d.ts +1 -0
  18. package/dist/app/icons.js +2 -0
  19. package/dist/app/input/input-action-controller.d.ts +4 -1
  20. package/dist/app/input/input-action-controller.js +68 -27
  21. package/dist/app/input/input-controller.d.ts +3 -0
  22. package/dist/app/input/input-controller.js +45 -1
  23. package/dist/app/input/input-paste-handler.d.ts +3 -0
  24. package/dist/app/input/input-paste-handler.js +21 -12
  25. package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
  26. package/dist/app/input/prompt-enhancer-controller.js +11 -5
  27. package/dist/app/input/voice-controller.d.ts +4 -0
  28. package/dist/app/input/voice-controller.js +76 -35
  29. package/dist/app/popup/popup-action-controller.d.ts +3 -0
  30. package/dist/app/popup/popup-action-controller.js +43 -8
  31. package/dist/app/rendering/render-controller.js +1 -0
  32. package/dist/app/rendering/status-line-renderer.d.ts +3 -1
  33. package/dist/app/rendering/status-line-renderer.js +11 -0
  34. package/dist/app/screen/mouse-controller.d.ts +5 -1
  35. package/dist/app/screen/mouse-controller.js +14 -2
  36. package/dist/app/session/queued-message-controller.d.ts +5 -4
  37. package/dist/app/session/queued-message-controller.js +32 -27
  38. package/dist/app/session/request-history.d.ts +2 -0
  39. package/dist/app/session/request-history.js +22 -15
  40. package/dist/app/session/session-event-controller.js +28 -8
  41. package/dist/app/session/session-history.js +8 -2
  42. package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
  43. package/dist/app/session/session-lifecycle-controller.js +91 -19
  44. package/dist/app/session/tabs-controller.d.ts +26 -0
  45. package/dist/app/session/tabs-controller.js +469 -96
  46. package/dist/app/terminal/terminal-controller.d.ts +4 -0
  47. package/dist/app/terminal/terminal-controller.js +38 -11
  48. package/dist/app/types.d.ts +10 -0
  49. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  50. package/dist/app/workspace/workspace-actions-controller.js +37 -9
  51. package/dist/input-editor.d.ts +2 -0
  52. package/dist/input-editor.js +17 -0
  53. package/docs/concurrency.md +76 -0
  54. package/external/pi-tools-suite/package.json +3 -3
  55. package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
  56. package/external/pi-tools-suite/src/todo/index.ts +2 -2
  57. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  58. package/package.json +4 -4
@@ -38,6 +38,7 @@ export class AppPopupActionController {
38
38
  return false;
39
39
  }
40
40
  async submitSlashCommand(text) {
41
+ const scope = this.captureScope();
41
42
  const parsed = this.menuItems.parseSlashInput(text);
42
43
  if (!parsed)
43
44
  return;
@@ -67,7 +68,7 @@ export class AppPopupActionController {
67
68
  this.host.render();
68
69
  try {
69
70
  if (command.kind === "resource") {
70
- await this.executeResourceSlashCommand(command, parsed.arguments);
71
+ await this.executeResourceSlashCommand(command, parsed.arguments, scope.session);
71
72
  }
72
73
  else {
73
74
  if (!command.run)
@@ -76,11 +77,13 @@ export class AppPopupActionController {
76
77
  }
77
78
  }
78
79
  catch (error) {
80
+ if (!this.isScopeActive(scope))
81
+ return;
79
82
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
80
83
  this.host.showToast(`/${command.name} failed`, "error");
81
84
  this.host.setSessionStatus(this.host.runtime()?.session);
82
85
  }
83
- if (this.host.isRunning())
86
+ if (this.isScopeActive(scope))
84
87
  this.host.render();
85
88
  }
86
89
  async submitSelectedSlashCommand() {
@@ -97,6 +100,7 @@ export class AppPopupActionController {
97
100
  return true;
98
101
  }
99
102
  async submitSelectedModel() {
103
+ const scope = this.captureScope();
100
104
  const selected = this.popupMenus.selectedModel();
101
105
  if (!selected)
102
106
  return false;
@@ -110,15 +114,18 @@ export class AppPopupActionController {
110
114
  await this.commandController.runModelCommand(selected.value.model);
111
115
  }
112
116
  catch (error) {
117
+ if (!this.isScopeActive(scope))
118
+ return true;
113
119
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
114
120
  this.host.showToast("/model failed", "error");
115
121
  this.host.setSessionStatus(this.host.runtime()?.session);
116
122
  }
117
- if (this.host.isRunning())
123
+ if (this.isScopeActive(scope))
118
124
  this.host.render();
119
125
  return true;
120
126
  }
121
127
  async submitSelectedThinking() {
128
+ const scope = this.captureScope();
122
129
  const selected = this.popupMenus.selectedThinking();
123
130
  if (!selected)
124
131
  return false;
@@ -132,15 +139,18 @@ export class AppPopupActionController {
132
139
  await this.commandController.runThinkingCommand(selected.value.level);
133
140
  }
134
141
  catch (error) {
142
+ if (!this.isScopeActive(scope))
143
+ return true;
135
144
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
136
145
  this.host.showToast("/thinking failed", "error");
137
146
  this.host.setSessionStatus(this.host.runtime()?.session);
138
147
  }
139
- if (this.host.isRunning())
148
+ if (this.isScopeActive(scope))
140
149
  this.host.render();
141
150
  return true;
142
151
  }
143
152
  async submitSelectedUserMessageAction() {
153
+ const scope = this.captureScope();
144
154
  const selected = this.popupMenus.selectedUserMessageAction();
145
155
  if (!selected)
146
156
  return false;
@@ -163,6 +173,8 @@ export class AppPopupActionController {
163
173
  return true;
164
174
  }
165
175
  catch (error) {
176
+ if (!this.isScopeActive(scope))
177
+ return true;
166
178
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
167
179
  this.host.showToast(`${selected.label} failed`, "error");
168
180
  this.host.setSessionStatus(this.host.runtime()?.session);
@@ -170,11 +182,15 @@ export class AppPopupActionController {
170
182
  }
171
183
  }
172
184
  async submitSelectedUserMessageJump() {
185
+ const scope = this.captureScope();
173
186
  const selected = this.popupMenus.selectedUserMessageJump();
174
187
  if (!selected)
175
188
  return false;
176
189
  this.popupMenus.closeUserMessageJumpMenu();
177
- if (!await this.host.scrollToUserMessageJumpTarget(selected)) {
190
+ const found = await this.host.scrollToUserMessageJumpTarget(selected);
191
+ if (!this.isScopeActive(scope))
192
+ return true;
193
+ if (!found) {
178
194
  this.host.showToast("User message not found", "error");
179
195
  this.host.setSessionStatus(this.host.runtime()?.session);
180
196
  return true;
@@ -184,6 +200,7 @@ export class AppPopupActionController {
184
200
  return true;
185
201
  }
186
202
  async submitSelectedQueueMessageAction() {
203
+ const scope = this.captureScope();
187
204
  const selected = this.popupMenus.selectedQueueMessageAction();
188
205
  if (!selected)
189
206
  return false;
@@ -202,6 +219,8 @@ export class AppPopupActionController {
202
219
  return true;
203
220
  }
204
221
  catch (error) {
222
+ if (!this.isScopeActive(scope))
223
+ return true;
205
224
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
206
225
  this.host.showToast(`${selected.label} failed`, "error");
207
226
  this.host.setSessionStatus(this.host.runtime()?.session);
@@ -229,7 +248,11 @@ export class AppPopupActionController {
229
248
  this.host.render();
230
249
  try {
231
250
  await this.host.awaitCurrentSessionExtensions(runtime);
251
+ if (!this.isRuntimeActive(runtime))
252
+ return true;
232
253
  const result = await runtime.switchSession(session.path);
254
+ if (!this.isRuntimeActive(runtime))
255
+ return true;
233
256
  if (result.cancelled) {
234
257
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
235
258
  this.host.setSessionStatus(runtime.session);
@@ -240,21 +263,33 @@ export class AppPopupActionController {
240
263
  this.host.afterSessionReplacement(`Resumed session "${name}"`);
241
264
  }
242
265
  catch (error) {
266
+ if (!this.isRuntimeActive(runtime))
267
+ return true;
243
268
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Resume failed: ${error instanceof Error ? error.message : String(error)}` });
244
269
  this.host.showToast("Failed to resume session", "error");
245
270
  this.host.setSessionStatus(runtime.session);
246
271
  }
247
- if (this.host.isRunning())
272
+ if (this.isRuntimeActive(runtime))
248
273
  this.host.render();
249
274
  return true;
250
275
  }
251
276
  formatSlashCommandLine(name, argumentsText) {
252
277
  return `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
253
278
  }
254
- async executeResourceSlashCommand(command, argumentsText) {
279
+ async executeResourceSlashCommand(command, argumentsText, session) {
255
280
  const promptText = this.formatSlashCommandLine(command.name, argumentsText);
256
281
  this.host.setStatus(`running /${command.name}`);
257
- await this.queuedMessages.submitUserMessage(this.queuedMessages.createSubmittedUserMessage(promptText, promptText, []));
282
+ await this.queuedMessages.submitUserMessage(this.queuedMessages.createSubmittedUserMessage(promptText, promptText, []), session);
283
+ }
284
+ captureScope() {
285
+ const runtime = this.host.runtime();
286
+ return { runtime, session: runtime?.session };
287
+ }
288
+ isScopeActive(scope) {
289
+ return this.host.isRunning() && this.host.runtime() === scope.runtime && scope.runtime?.session === scope.session;
290
+ }
291
+ isRuntimeActive(runtime) {
292
+ return this.host.isRunning() && this.host.runtime() === runtime;
258
293
  }
259
294
  getRuntime(commandName) {
260
295
  const runtime = this.host.runtime();
@@ -268,6 +268,7 @@ export class AppRenderController {
268
268
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
269
269
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
270
270
  this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(statusLayout, statusRow);
271
+ this.deps.mouseController.statusInternalClipboardTarget = this.deps.statusLineRenderer.internalClipboardTarget?.(statusLayout, statusRow);
271
272
  this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
272
273
  this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
273
274
  this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
@@ -1,6 +1,6 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import type { Theme } from "../../theme.js";
3
- import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
3
+ import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
4
4
  import type { ScreenStyler } from "../screen/screen-styler.js";
5
5
  import { type ModelColorsConfig } from "../../config.js";
6
6
  export type StatusLineRendererHost = {
@@ -31,6 +31,7 @@ export type StatusLineRendererHost = {
31
31
  down: boolean;
32
32
  };
33
33
  queueableInputActive?(): boolean;
34
+ internalClipboardActive?(): boolean;
34
35
  userMessageJumpMenuActive?(): boolean;
35
36
  allThinkingExpandedActive?(): boolean;
36
37
  superCompactToolsActive?(): boolean;
@@ -52,6 +53,7 @@ export declare class StatusLineRenderer {
52
53
  voiceLanguageTarget(layout: StatusLineLayout, row: number): StatusVoiceLanguageTarget | undefined;
53
54
  userJumpTarget(layout: StatusLineLayout, row: number): StatusUserJumpTarget | undefined;
54
55
  draftQueueTarget(layout: StatusLineLayout, row: number): StatusDraftQueueTarget | undefined;
56
+ internalClipboardTarget(layout: StatusLineLayout, row: number): StatusInternalClipboardTarget | undefined;
55
57
  thinkingExpandTarget(layout: StatusLineLayout, row: number): StatusThinkingExpandTarget | undefined;
56
58
  compactToolsTarget(layout: StatusLineLayout, row: number): StatusCompactToolsTarget | undefined;
57
59
  quickScrollUpTarget(layout: StatusLineLayout, row: number): StatusQuickScrollTarget | undefined;
@@ -65,6 +65,9 @@ export class StatusLineRenderer {
65
65
  appendWidget(draftQueueButton ? this.iconButtonText(draftQueueButton) : "", (column, text) => {
66
66
  layout.draftQueueWidget = this.widgetLayout(column, text);
67
67
  });
68
+ appendWidget(this.iconButtonText(APP_ICONS.clipboardPaste), (column, text) => {
69
+ layout.internalClipboardWidget = this.widgetLayout(column, text);
70
+ });
68
71
  const promptEnhancerWidgetText = this.host.promptEnhancerStatusWidgetText();
69
72
  appendWidget(promptEnhancerWidgetText ? this.iconButtonText(promptEnhancerWidgetText) : "", (column, text) => {
70
73
  layout.promptEnhancerWidget = this.widgetLayout(column, text);
@@ -141,6 +144,7 @@ export class StatusLineRenderer {
141
144
  });
142
145
  };
143
146
  pushWidgetSegment(layout.draftQueueWidget, colors.info);
147
+ pushWidgetSegment(layout.internalClipboardWidget, this.host.internalClipboardActive?.() ? colors.info : colors.muted);
144
148
  pushWidgetSegment(layout.promptEnhancerWidget, this.host.promptEnhancerStatusWidgetActive()
145
149
  ? colors.warning
146
150
  : this.host.promptEnhancerStatusWidgetEnabled()
@@ -251,6 +255,12 @@ export class StatusLineRenderer {
251
255
  return undefined;
252
256
  return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
253
257
  }
258
+ internalClipboardTarget(layout, row) {
259
+ const widget = layout.internalClipboardWidget;
260
+ if (!widget)
261
+ return undefined;
262
+ return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
263
+ }
254
264
  thinkingExpandTarget(layout, row) {
255
265
  const widget = layout.thinkingExpandWidget;
256
266
  if (!widget)
@@ -339,6 +349,7 @@ export class StatusLineRenderer {
339
349
  const colors = this.host.theme.colors;
340
350
  const widgets = [
341
351
  { widget: layout.draftQueueWidget, foreground: colors.info },
352
+ { widget: layout.internalClipboardWidget, foreground: this.host.internalClipboardActive?.() ? colors.info : colors.muted },
342
353
  { widget: layout.promptEnhancerWidget, foreground: this.host.promptEnhancerStatusWidgetActive()
343
354
  ? colors.warning
344
355
  : this.host.promptEnhancerStatusWidgetEnabled()
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../../ui.js";
6
6
  import type { AppPopupActionController } from "../popup/popup-action-controller.js";
7
7
  import type { AppPopupMenuController } from "../popup/popup-menu-controller.js";
8
8
  import type { AppScrollController } from "./scroll-controller.js";
9
- import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
9
+ import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
10
10
  import { type RenderedLink } from "./file-links.js";
11
11
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
12
  type ClickFlash = {
@@ -56,6 +56,8 @@ export type AppMouseControllerHost = {
56
56
  refreshModelUsageStatus(): void | Promise<void>;
57
57
  refreshUserMessageJumpMenuItems?(): Promise<void>;
58
58
  queueInputFromStatus?(): void | Promise<void>;
59
+ pasteInternalClipboard?(): void;
60
+ setInternalClipboardText?(text: string): void;
59
61
  scrollConversationQuick(direction: "up" | "down"): void | Promise<void>;
60
62
  toggleAllThinkingExpanded?(): void;
61
63
  toggleSuperCompactTools?(): void;
@@ -102,6 +104,7 @@ export declare class AppMouseController {
102
104
  statusModelUsageTarget: StatusModelUsageTarget | undefined;
103
105
  statusUserJumpTarget: StatusUserJumpTarget | undefined;
104
106
  statusDraftQueueTarget: StatusDraftQueueTarget | undefined;
107
+ statusInternalClipboardTarget: StatusInternalClipboardTarget | undefined;
105
108
  statusThinkingExpandTarget: StatusThinkingExpandTarget | undefined;
106
109
  statusCompactToolsTarget: StatusCompactToolsTarget | undefined;
107
110
  statusQuickScrollUpTarget: StatusQuickScrollTarget | undefined;
@@ -161,6 +164,7 @@ export declare class AppMouseController {
161
164
  private handleInputBorderStatusClick;
162
165
  private openStatusUserJumpMenu;
163
166
  private handleStatusDraftQueueClick;
167
+ private handleStatusInternalClipboardClick;
164
168
  private handleStatusThinkingExpandClick;
165
169
  private handleStatusCompactToolsClick;
166
170
  private handleStatusTerminalBellSoundClick;
@@ -26,6 +26,7 @@ export class AppMouseController {
26
26
  statusModelUsageTarget;
27
27
  statusUserJumpTarget;
28
28
  statusDraftQueueTarget;
29
+ statusInternalClipboardTarget;
29
30
  statusThinkingExpandTarget;
30
31
  statusCompactToolsTarget;
31
32
  statusQuickScrollUpTarget;
@@ -317,6 +318,7 @@ export class AppMouseController {
317
318
  this.statusContextTarget,
318
319
  this.statusModelUsageTarget,
319
320
  this.statusDraftQueueTarget,
321
+ this.statusInternalClipboardTarget,
320
322
  this.statusUserJumpTarget,
321
323
  this.statusThinkingExpandTarget,
322
324
  this.statusCompactToolsTarget,
@@ -496,6 +498,7 @@ export class AppMouseController {
496
498
  }
497
499
  handleInputBorderStatusClick(event) {
498
500
  return this.handleStatusDraftQueueClick(event)
501
+ || this.handleStatusInternalClipboardClick(event)
499
502
  || this.handleStatusUserJumpClick(event)
500
503
  || this.handleStatusThinkingExpandClick(event)
501
504
  || this.handleStatusCompactToolsClick(event)
@@ -525,6 +528,12 @@ export class AppMouseController {
525
528
  void this.host.queueInputFromStatus?.();
526
529
  return true;
527
530
  }
531
+ handleStatusInternalClipboardClick(event) {
532
+ if (!this.statusTargetContains(this.statusInternalClipboardTarget, event))
533
+ return false;
534
+ this.host.pasteInternalClipboard?.();
535
+ return true;
536
+ }
528
537
  handleStatusThinkingExpandClick(event) {
529
538
  if (!this.statusTargetContains(this.statusThinkingExpandTarget, event))
530
539
  return false;
@@ -682,9 +691,12 @@ export class AppMouseController {
682
691
  return false;
683
692
  }
684
693
  const selectedText = this.getSelectedText(selection);
685
- this.host.render();
686
- if (selectedText.trim().length === 0)
694
+ if (selectedText.trim().length === 0) {
695
+ this.host.render();
687
696
  return true;
697
+ }
698
+ this.host.setInternalClipboardText?.(selectedText);
699
+ this.host.render();
688
700
  try {
689
701
  this.copyTextToClipboard(selectedText);
690
702
  this.host.showToast("Copied to clipboard", "success");
@@ -19,13 +19,14 @@ export type AppQueuedMessageControllerHost = {
19
19
  setInput(value: string): void;
20
20
  insertInput(value: string): void;
21
21
  attachImage(data: string, mimeType: string): void;
22
+ requeueAutoUserMessageForSession?(session: AgentSession, message: SubmittedUserMessage): void;
22
23
  onDeferredUserMessagesChanged?(): void;
23
24
  };
24
25
  export declare class AppQueuedMessageController {
25
26
  private readonly host;
26
27
  readonly autoUserMessages: SubmittedUserMessage[];
27
28
  readonly deferredUserMessages: SubmittedUserMessage[];
28
- private promptSubmissionInFlightSession;
29
+ private readonly promptSubmissionsInFlight;
29
30
  private immediateSendInProgress;
30
31
  constructor(host: AppQueuedMessageControllerHost);
31
32
  reset(): void;
@@ -34,11 +35,11 @@ export declare class AppQueuedMessageController {
34
35
  captureDeferredUserMessages(): SubmittedUserMessage[];
35
36
  restoreDeferredUserMessages(messages: readonly SubmittedUserMessage[]): void;
36
37
  createSubmittedUserMessage(promptText: string, displayText: string, images: ImageContent[]): SubmittedUserMessage;
37
- submitUserMessage(message: SubmittedUserMessage): Promise<void>;
38
+ submitUserMessage(message: SubmittedUserMessage, session?: AgentSession): Promise<void>;
38
39
  sendUserMessageToSession(message: SubmittedUserMessage, options?: {
39
40
  streamingBehavior?: "steer" | "followUp";
40
- }): Promise<void>;
41
- flushAutoUserMessages(): Promise<void>;
41
+ }, session?: AgentSession): Promise<void>;
42
+ flushAutoUserMessages(expectedSession?: AgentSession): Promise<void>;
42
43
  queuedMessageCounts(): {
43
44
  steering: number;
44
45
  followUp: number;
@@ -5,7 +5,7 @@ export class AppQueuedMessageController {
5
5
  host;
6
6
  autoUserMessages = [];
7
7
  deferredUserMessages = [];
8
- promptSubmissionInFlightSession;
8
+ promptSubmissionsInFlight = new Set();
9
9
  immediateSendInProgress = false;
10
10
  constructor(host) {
11
11
  this.host = host;
@@ -13,7 +13,7 @@ export class AppQueuedMessageController {
13
13
  reset() {
14
14
  this.autoUserMessages.length = 0;
15
15
  this.deferredUserMessages.length = 0;
16
- this.promptSubmissionInFlightSession = undefined;
16
+ this.promptSubmissionsInFlight.clear();
17
17
  }
18
18
  captureAutoUserMessages() {
19
19
  return this.autoUserMessages.map(cloneSubmittedUserMessage);
@@ -40,51 +40,52 @@ export class AppQueuedMessageController {
40
40
  images,
41
41
  };
42
42
  }
43
- async submitUserMessage(message) {
44
- const session = this.host.requireRuntime().session;
45
- if (session.isStreaming) {
46
- await this.sendUserMessageToSession(message, { streamingBehavior: "steer" });
43
+ async submitUserMessage(message, session) {
44
+ const targetSession = session ?? this.host.requireRuntime().session;
45
+ if (targetSession.isStreaming) {
46
+ await this.sendUserMessageToSession(message, { streamingBehavior: "steer" }, targetSession);
47
47
  return;
48
48
  }
49
- if (this.shouldQueueUserMessageAsSteering(session)) {
49
+ if (this.shouldQueueUserMessageAsSteering(targetSession)) {
50
50
  await this.queueUserMessageAsSteering(message);
51
51
  return;
52
52
  }
53
- await this.sendUserMessageToSession(message);
53
+ await this.sendUserMessageToSession(message, {}, targetSession);
54
54
  }
55
- async sendUserMessageToSession(message, options = {}) {
56
- const session = this.host.requireRuntime().session;
57
- const markInFlight = !session.isStreaming;
55
+ async sendUserMessageToSession(message, options = {}, session) {
56
+ const targetSession = session ?? this.host.requireRuntime().session;
57
+ const markInFlight = !targetSession.isStreaming;
58
58
  if (markInFlight)
59
- this.promptSubmissionInFlightSession = session;
59
+ this.promptSubmissionsInFlight.add(targetSession);
60
60
  this.host.setSessionActivity("running");
61
61
  try {
62
62
  const opts = {};
63
- if (session.isStreaming)
63
+ if (targetSession.isStreaming)
64
64
  opts.streamingBehavior = options.streamingBehavior ?? "steer";
65
65
  if (message.images.length > 0)
66
66
  opts.images = message.images;
67
- await session.prompt(message.promptText, Object.keys(opts).length > 0 ? opts : undefined);
67
+ await targetSession.prompt(message.promptText, Object.keys(opts).length > 0 ? opts : undefined);
68
68
  }
69
69
  finally {
70
- if (markInFlight && this.promptSubmissionInFlightSession === session)
71
- this.promptSubmissionInFlightSession = undefined;
70
+ if (markInFlight)
71
+ this.promptSubmissionsInFlight.delete(targetSession);
72
72
  const runtime = this.host.runtime();
73
- if (runtime) {
74
- const activeSession = runtime.session;
75
- this.host.setSessionStatus(activeSession);
76
- this.host.setSessionActivity(activeSession.isStreaming || activeSession.isCompacting ? "running" : "idle");
77
- }
73
+ if (runtime?.session !== targetSession)
74
+ return;
75
+ this.host.setSessionStatus(targetSession);
76
+ this.host.setSessionActivity(targetSession.isStreaming || targetSession.isCompacting ? "running" : "idle");
78
77
  if (this.totalQueuedMessageCount() > 0)
79
78
  this.updateQueuedMessageStatus();
80
- void this.flushAutoUserMessages();
79
+ void this.flushAutoUserMessages(targetSession);
81
80
  }
82
81
  }
83
- async flushAutoUserMessages() {
82
+ async flushAutoUserMessages(expectedSession) {
84
83
  if (this.immediateSendInProgress || this.autoUserMessages.length === 0)
85
84
  return;
86
85
  const session = this.host.runtime()?.session;
87
- if (!session || session.isStreaming || session.isCompacting || this.promptSubmissionInFlightSession === session)
86
+ if (expectedSession && session !== expectedSession)
87
+ return;
88
+ if (!session || session.isStreaming || session.isCompacting || this.promptSubmissionsInFlight.has(session))
88
89
  return;
89
90
  const message = this.autoUserMessages.shift();
90
91
  if (!message)
@@ -94,9 +95,13 @@ export class AppQueuedMessageController {
94
95
  if (this.host.isRunning())
95
96
  this.host.render();
96
97
  try {
97
- await this.sendUserMessageToSession(message);
98
+ await this.sendUserMessageToSession(message, {}, session);
98
99
  }
99
100
  catch (error) {
101
+ if (this.host.runtime()?.session !== session) {
102
+ this.host.requeueAutoUserMessageForSession?.(session, message);
103
+ return;
104
+ }
100
105
  this.autoUserMessages.unshift(message);
101
106
  this.notifyAutoUserMessagesChanged();
102
107
  this.updateQueuedMessageStatus();
@@ -201,7 +206,7 @@ export class AppQueuedMessageController {
201
206
  const message = typeof taken.removed === "string"
202
207
  ? this.createSubmittedUserMessage(taken.removed, taken.removed, [])
203
208
  : taken.removed;
204
- await this.sendUserMessageToSession(message, { streamingBehavior: "steer" });
209
+ await this.sendUserMessageToSession(message, { streamingBehavior: "steer" }, session);
205
210
  this.host.setSessionStatus(this.host.runtime()?.session);
206
211
  this.host.showToast("Queued message sent", "success");
207
212
  }
@@ -237,7 +242,7 @@ export class AppQueuedMessageController {
237
242
  return deferredQueuedMessageEntries(this.deferredUserMessages);
238
243
  }
239
244
  shouldQueueUserMessageAsSteering(session) {
240
- return session.isCompacting || this.promptSubmissionInFlightSession === session;
245
+ return session.isCompacting || this.promptSubmissionsInFlight.has(session);
241
246
  }
242
247
  async queueUserMessageAsSteering(message) {
243
248
  this.autoUserMessages.push(message);
@@ -12,6 +12,8 @@ export declare class AppRequestHistory {
12
12
  private entries;
13
13
  private cursor;
14
14
  private draft;
15
+ private saveChain;
16
+ private saveSequence;
15
17
  constructor(host: RequestHistoryHost);
16
18
  load(): Promise<void>;
17
19
  add(text: string): void;
@@ -9,12 +9,14 @@ export class AppRequestHistory {
9
9
  entries = [];
10
10
  cursor;
11
11
  draft = "";
12
+ saveChain = Promise.resolve();
13
+ saveSequence = 0;
12
14
  constructor(host) {
13
15
  this.host = host;
14
16
  }
15
17
  async load() {
16
18
  if (this.host.noSession)
17
- return;
19
+ return Promise.resolve();
18
20
  try {
19
21
  const raw = await readFile(this.filePath(), "utf8");
20
22
  const parsed = JSON.parse(raw);
@@ -93,21 +95,26 @@ export class AppRequestHistory {
93
95
  this.replaceInput(entry);
94
96
  return true;
95
97
  }
96
- async save() {
98
+ save() {
97
99
  if (this.host.noSession)
98
- return;
99
- try {
100
- this.entries = this.limited(this.entries);
101
- const filePath = this.filePath();
102
- const payload = this.payload(this.entries);
103
- await mkdir(dirname(filePath), { recursive: true });
104
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
105
- await writeFile(tempPath, payload, "utf8");
106
- await rename(tempPath, filePath);
107
- }
108
- catch {
109
- // Request history is a convenience feature; never interrupt the UI on disk errors.
110
- }
100
+ return Promise.resolve();
101
+ this.entries = this.limited(this.entries);
102
+ const payload = this.payload(this.entries);
103
+ const sequence = ++this.saveSequence;
104
+ const writeSnapshot = async () => {
105
+ try {
106
+ const filePath = this.filePath();
107
+ await mkdir(dirname(filePath), { recursive: true });
108
+ const tempPath = `${filePath}.${process.pid}.${sequence}.tmp`;
109
+ await writeFile(tempPath, payload, "utf8");
110
+ await rename(tempPath, filePath);
111
+ }
112
+ catch {
113
+ // Request history is a convenience feature; never interrupt the UI on disk errors.
114
+ }
115
+ };
116
+ this.saveChain = this.saveChain.then(writeSnapshot, writeSnapshot);
117
+ return this.saveChain;
111
118
  }
112
119
  replaceInput(value) {
113
120
  if (value !== this.host.getInput()) {
@@ -1,7 +1,7 @@
1
1
  import { createId } from "../id.js";
2
2
  import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
3
3
  import { customMessageEntry, extensionSessionEntry, loadSessionHistoryEntries, loadSessionHistoryEntriesAsync } from "./session-history.js";
4
- import { sessionHistoryDisplayMessages, sessionHistoryDisplayMessagesFromEntries, sessionHistoryFullBranchEntries } from "./pix-system-message.js";
4
+ import { sessionHistoryDisplayMessages, sessionHistoryDisplayMessagesFromEntries, sessionHistoryFullBranchEntries, sessionHistoryOlderMessagesReader } from "./pix-system-message.js";
5
5
  import { THINKING_TOOL_NAME } from "../constants.js";
6
6
  import { isRecord } from "../guards.js";
7
7
  /**
@@ -132,24 +132,44 @@ export class AppSessionEventController {
132
132
  this.olderHistoryLoader = undefined;
133
133
  this.historyEntries = [];
134
134
  this.historyWindowStart = 0;
135
- const branchEntries = await sessionHistoryFullBranchEntries(runtime.session);
135
+ const lazyOlderHistory = options.lazyOlderHistory === true;
136
+ const historyEntries = lazyOlderHistory ? undefined : [];
137
+ const messages = lazyOlderHistory
138
+ ? sessionHistoryDisplayMessages(runtime.session)
139
+ : sessionHistoryDisplayMessagesFromEntries(await sessionHistoryFullBranchEntries(runtime.session));
136
140
  if (options.isCancelled())
137
141
  return false;
138
- const historyEntries = [];
139
142
  const loaded = await loadSessionHistoryEntriesAsync({
140
- messages: sessionHistoryDisplayMessagesFromEntries(branchEntries),
141
- addEntry: (entry) => historyEntries.push(entry),
142
- prependEntries: (entries) => historyEntries.unshift(...entries),
143
+ messages,
144
+ addEntry: (entry) => {
145
+ if (historyEntries)
146
+ historyEntries.push(entry);
147
+ else
148
+ this.addEntry(entry);
149
+ },
150
+ prependEntries: (entries) => {
151
+ if (historyEntries)
152
+ historyEntries.unshift(...entries);
153
+ else
154
+ this.prependEntries(entries);
155
+ },
143
156
  setToolEntryId: (toolCallId, entryId) => this.toolEntryIdsByCallId.set(toolCallId, entryId),
144
157
  toolDefaultExpanded: (toolName) => this.host.toolDefaultExpanded(toolName),
145
158
  observeSubagentsToolResult: (toolName, details, options) => this.host.observeSubagentsToolResult(toolName, details, options),
146
159
  observeTodoToolResult: (toolName, details, isError) => this.host.observeTodoToolResult(toolName, details, isError),
147
160
  isCancelled: options.isCancelled,
148
- render: () => { },
149
- lazyOlderHistory: false,
161
+ render: lazyOlderHistory ? options.render : () => { },
162
+ lazyOlderHistory,
163
+ olderMessagesReader: lazyOlderHistory ? sessionHistoryOlderMessagesReader(runtime.session) : undefined,
164
+ onOlderLoaderReady: (loader) => {
165
+ if (!options.isCancelled())
166
+ this.olderHistoryLoader = loader;
167
+ },
150
168
  });
151
169
  if (!loaded)
152
170
  return false;
171
+ if (!historyEntries)
172
+ return !options.isCancelled();
153
173
  this.historyEntries = historyEntries;
154
174
  this.setHistoryWindowStart(this.maxHistoryWindowStart());
155
175
  options.render();
@@ -112,11 +112,17 @@ async function externalOlderMessagesBatch(olderMessagesReader, chunkSize) {
112
112
  }
113
113
  function expandedTailStart(messages, initialStart) {
114
114
  let start = initialStart;
115
+ const firstMessage = messages[start];
116
+ if (!isRecord(firstMessage) || (firstMessage.role !== "assistant" && firstMessage.role !== "toolResult"))
117
+ return start;
118
+ // A fixed-size tail can otherwise begin inside a long assistant/tool turn,
119
+ // hiding the user prompt that owns the first visible response. Keep that turn
120
+ // atomic so reaching the top never stops at an orphaned assistant block.
115
121
  while (start > 0) {
122
+ start -= 1;
116
123
  const message = messages[start];
117
- if (!isRecord(message) || message.role !== "toolResult")
124
+ if (isRecord(message) && message.role === "user")
118
125
  break;
119
- start -= 1;
120
126
  }
121
127
  return start;
122
128
  }
@@ -55,9 +55,15 @@ export type BindCurrentSessionOptions = {
55
55
  export declare class AppSessionLifecycleController {
56
56
  private readonly host;
57
57
  private unsubscribe;
58
+ private ownershipRuntime;
59
+ private ownershipSession;
60
+ private ownershipGeneration;
61
+ private subscriptionGeneration;
62
+ private replacementHistoryGeneration;
58
63
  private extensionBindPromise;
59
64
  private extensionBindRuntime;
60
65
  private extensionBindSession;
66
+ private extensionBindOwnershipGeneration;
61
67
  constructor(host: AppSessionLifecycleHost);
62
68
  start(): Promise<void>;
63
69
  bindCurrentSession(options?: BindCurrentSessionOptions): Promise<void>;
@@ -70,6 +76,7 @@ export declare class AppSessionLifecycleController {
70
76
  requireRuntime(): AgentSessionRuntime;
71
77
  private bindSessionExtensions;
72
78
  private collectAvailabilityIssues;
79
+ private advanceOwnership;
73
80
  private isCurrentRuntimeSession;
74
81
  private extensionUiScope;
75
82
  }