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
@@ -4,9 +4,15 @@ import { collectStartupAvailabilityIssues } from "../cli/startup-checks.js";
4
4
  export class AppSessionLifecycleController {
5
5
  host;
6
6
  unsubscribe;
7
+ ownershipRuntime;
8
+ ownershipSession;
9
+ ownershipGeneration = 0;
10
+ subscriptionGeneration = 0;
11
+ replacementHistoryGeneration = 0;
7
12
  extensionBindPromise;
8
13
  extensionBindRuntime;
9
14
  extensionBindSession;
15
+ extensionBindOwnershipGeneration;
10
16
  constructor(host) {
11
17
  this.host = host;
12
18
  }
@@ -33,6 +39,8 @@ export class AppSessionLifecycleController {
33
39
  }
34
40
  this.host.setRuntime(runtime);
35
41
  runtime.setRebindSession(async () => {
42
+ if (!this.host.isRunning() || this.host.runtime() !== runtime)
43
+ return;
36
44
  await this.bindCurrentSession({ awaitExtensions: false });
37
45
  });
38
46
  await this.bindCurrentSession({ awaitExtensions: false });
@@ -65,17 +73,27 @@ export class AppSessionLifecycleController {
65
73
  async bindCurrentSession(options = {}) {
66
74
  const runtime = this.requireRuntime();
67
75
  const session = runtime.session;
76
+ const ownershipGeneration = this.advanceOwnership(runtime, session);
77
+ const subscriptionGeneration = ++this.subscriptionGeneration;
78
+ this.replacementHistoryGeneration += 1;
68
79
  this.unsubscribe?.();
69
80
  this.unsubscribe = session.subscribe((event) => {
81
+ if (!this.isCurrentRuntimeSession(runtime, session, ownershipGeneration)
82
+ || subscriptionGeneration !== this.subscriptionGeneration)
83
+ return;
70
84
  this.host.handleSessionEvent(event);
71
85
  });
72
86
  this.host.closeSdkMenuForBind();
73
87
  const extensionUiScope = this.extensionUiScope(session);
74
88
  this.host.clearExtensionWidgets(extensionUiScope, { cancelCustomUi: false });
75
- const bindPromise = this.bindSessionExtensions(runtime, session, extensionUiScope);
89
+ const bindPromise = this.bindSessionExtensions(runtime, session, extensionUiScope, {
90
+ deferStart: options.awaitExtensions === false,
91
+ ownershipGeneration,
92
+ });
76
93
  if (options.awaitExtensions === false) {
77
94
  void bindPromise.catch((error) => {
78
- if (!this.isCurrentRuntimeSession(runtime, session))
95
+ if (!this.isCurrentRuntimeSession(runtime, session, ownershipGeneration)
96
+ || subscriptionGeneration !== this.subscriptionGeneration)
79
97
  return;
80
98
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Extension bind failed: ${stringifyUnknown(error)}` });
81
99
  this.host.showToast("Extension initialization failed", "error");
@@ -90,28 +108,53 @@ export class AppSessionLifecycleController {
90
108
  return;
91
109
  if (this.extensionBindRuntime !== runtime || this.extensionBindSession !== runtime.session)
92
110
  return;
111
+ if (this.extensionBindOwnershipGeneration !== this.ownershipGeneration)
112
+ return;
93
113
  await this.extensionBindPromise;
94
114
  }
95
115
  unsubscribeSession() {
96
116
  this.unsubscribe?.();
117
+ this.unsubscribe = undefined;
118
+ this.ownershipRuntime = undefined;
119
+ this.ownershipSession = undefined;
120
+ this.ownershipGeneration += 1;
121
+ this.subscriptionGeneration += 1;
122
+ this.replacementHistoryGeneration += 1;
97
123
  }
98
124
  afterSessionReplacement(message) {
125
+ const runtime = this.host.runtime();
126
+ if (!runtime) {
127
+ this.resetSessionView();
128
+ this.host.render();
129
+ return;
130
+ }
131
+ const session = runtime.session;
132
+ const ownershipGeneration = this.advanceOwnership(runtime, session);
133
+ const historyGeneration = ++this.replacementHistoryGeneration;
99
134
  this.resetSessionView();
100
- void this.loadReplacementHistory(message);
135
+ void this.loadReplacementHistory(runtime, session, ownershipGeneration, historyGeneration, message);
101
136
  this.host.render();
102
137
  }
103
- async loadReplacementHistory(message) {
104
- await this.host.loadSessionHistoryEntriesAsync({
105
- isCancelled: () => !this.host.isRunning(),
106
- render: () => this.host.render(),
138
+ async loadReplacementHistory(runtime, session, ownershipGeneration, historyGeneration, message) {
139
+ const isCancelled = () => historyGeneration !== this.replacementHistoryGeneration
140
+ || !this.isCurrentRuntimeSession(runtime, session, ownershipGeneration);
141
+ const completed = await this.host.loadSessionHistoryEntriesAsync({
142
+ isCancelled,
143
+ render: () => {
144
+ if (!isCancelled())
145
+ this.host.render();
146
+ },
107
147
  lazyOlderHistory: true,
108
148
  });
149
+ if (!completed || isCancelled())
150
+ return;
109
151
  this.host.syncUserSessionEntryMetadata();
152
+ if (isCancelled())
153
+ return;
110
154
  if (message)
111
155
  this.host.addEntry({ id: createId("system"), kind: "system", text: message });
112
- const session = this.host.runtime()?.session;
113
156
  this.host.setSessionStatus(session);
114
- this.host.setSessionActivity(session?.isStreaming ? "running" : "idle");
157
+ this.host.setSessionActivity(session.isStreaming ? "running" : "idle");
115
158
  this.host.render();
116
159
  }
117
160
  resetSessionView() {
@@ -135,25 +178,41 @@ export class AppSessionLifecycleController {
135
178
  throw new Error("Runtime is not initialized");
136
179
  return runtime;
137
180
  }
138
- bindSessionExtensions(runtime, session, scopeKey) {
139
- if (this.extensionBindPromise && this.extensionBindRuntime === runtime && this.extensionBindSession === session) {
181
+ bindSessionExtensions(runtime, session, scopeKey, options) {
182
+ if (this.extensionBindPromise
183
+ && this.extensionBindRuntime === runtime
184
+ && this.extensionBindSession === session
185
+ && this.extensionBindOwnershipGeneration === options.ownershipGeneration) {
140
186
  return this.extensionBindPromise;
141
187
  }
142
- const promise = session.bindExtensions({
143
- uiContext: this.host.createExtensionUIContext(scopeKey),
144
- commandContextActions: this.host.createExtensionCommandContextActions(runtime),
145
- shutdownHandler: this.host.extensionShutdownHandler(),
146
- onError: (error) => this.host.handleExtensionError(error),
147
- }).finally(() => {
188
+ const startBind = () => {
189
+ if (!this.isCurrentRuntimeSession(runtime, session, options.ownershipGeneration))
190
+ return Promise.resolve();
191
+ return session.bindExtensions({
192
+ uiContext: this.host.createExtensionUIContext(scopeKey),
193
+ commandContextActions: this.host.createExtensionCommandContextActions(runtime),
194
+ shutdownHandler: this.host.extensionShutdownHandler(),
195
+ onError: (error) => {
196
+ if (this.isCurrentRuntimeSession(runtime, session, options.ownershipGeneration))
197
+ this.host.handleExtensionError(error);
198
+ },
199
+ });
200
+ };
201
+ const bindPromise = options.deferStart
202
+ ? new Promise((resolve) => setTimeout(resolve, 0)).then(startBind)
203
+ : startBind();
204
+ const promise = bindPromise.finally(() => {
148
205
  if (this.extensionBindPromise !== promise)
149
206
  return;
150
207
  this.extensionBindPromise = undefined;
151
208
  this.extensionBindRuntime = undefined;
152
209
  this.extensionBindSession = undefined;
210
+ this.extensionBindOwnershipGeneration = undefined;
153
211
  });
154
212
  this.extensionBindPromise = promise;
155
213
  this.extensionBindRuntime = runtime;
156
214
  this.extensionBindSession = session;
215
+ this.extensionBindOwnershipGeneration = options.ownershipGeneration;
157
216
  return promise;
158
217
  }
159
218
  async collectAvailabilityIssues(runtime) {
@@ -184,8 +243,21 @@ export class AppSessionLifecycleController {
184
243
  this.host.render();
185
244
  }
186
245
  }
187
- isCurrentRuntimeSession(runtime, session) {
188
- return this.host.isRunning() && this.host.runtime() === runtime && runtime.session === session;
246
+ advanceOwnership(runtime, session) {
247
+ if (this.ownershipRuntime !== runtime || this.ownershipSession !== session) {
248
+ this.ownershipRuntime = runtime;
249
+ this.ownershipSession = session;
250
+ this.ownershipGeneration += 1;
251
+ }
252
+ return this.ownershipGeneration;
253
+ }
254
+ isCurrentRuntimeSession(runtime, session, ownershipGeneration) {
255
+ return this.host.isRunning()
256
+ && this.host.runtime() === runtime
257
+ && runtime.session === session
258
+ && this.ownershipRuntime === runtime
259
+ && this.ownershipSession === session
260
+ && this.ownershipGeneration === ownershipGeneration;
189
261
  }
190
262
  extensionUiScope(session) {
191
263
  return session.sessionFile ?? session.sessionId;
@@ -61,15 +61,27 @@ export declare class AppTabsController {
61
61
  private readonly sessionViewsByTabId;
62
62
  private readonly scrollStatesByTabId;
63
63
  private readonly tabIdsNeedingHistoryReload;
64
+ private readonly historyInvalidationGenerationByTabId;
65
+ private readonly runtimeOwnershipGenerationByTabId;
66
+ private readonly orphanRuntimeDisposals;
67
+ private readonly lifecycleMutationQueue;
64
68
  private activeTabId;
65
69
  private pendingActiveTabId;
66
70
  private historyLoadGeneration;
71
+ private historyInvalidationGeneration;
72
+ private runtimeOwnershipGeneration;
73
+ private lifecycleGeneration;
74
+ private lifecycleMutationRunning;
75
+ private saveTabsWriteTail;
67
76
  private restored;
68
77
  private retentionCleanupRunning;
69
78
  private retentionCleanupScheduled;
70
79
  private prewarmScheduled;
71
80
  private prewarmRunning;
72
81
  constructor(host: AppTabsControllerHost);
82
+ private runLifecycleMutation;
83
+ private isLifecycleOwner;
84
+ cancelPendingLifecycleWork(): void;
73
85
  tabs(): readonly SessionTab[];
74
86
  isSwitching(): boolean;
75
87
  markTerminalBellAttention(sessionPath: string | undefined): void;
@@ -80,16 +92,24 @@ export declare class AppTabsController {
80
92
  disposeInactiveRuntimes(disposeRuntime?: (runtime: AgentSessionRuntime) => Promise<void>): Promise<void>;
81
93
  saveInputStateForQuit(): Promise<void>;
82
94
  persistActiveDeferredUserMessages(): void;
95
+ requeueAutoUserMessageForSession(session: AgentSession, message: SubmittedUserMessage): void;
96
+ inputTabOwnedByRuntime(tabId: string | undefined, runtime: AgentSessionRuntime, session: AgentSession): boolean;
83
97
  syncActiveTabFromRuntime(options?: {
84
98
  save?: boolean;
85
99
  force?: boolean;
86
100
  }): void;
87
101
  restoreAfterStartup(): Promise<void>;
102
+ private restoreAfterStartupMutation;
88
103
  openNewTab(): Promise<void>;
104
+ private openNewTabMutation;
89
105
  openSessionInNewTab(sessionPath: string): Promise<boolean>;
106
+ private openSessionInNewTabMutation;
90
107
  forkSessionEntryInNewTab(entryId: string): Promise<boolean>;
108
+ private forkSessionEntryInNewTabMutation;
91
109
  switchToTab(tabId: string): Promise<void>;
110
+ private switchToTabMutation;
92
111
  closeTab(tabId: string): Promise<void>;
112
+ private closeTabMutation;
93
113
  private replaceLastTabWithNewSession;
94
114
  private loadActiveSessionHistory;
95
115
  private cancelHistoryLoad;
@@ -103,8 +123,13 @@ export declare class AppTabsController {
103
123
  private restoreStoredScrollState;
104
124
  private setRuntimeForTab;
105
125
  private deleteRuntimeForTab;
126
+ private releaseRuntimeOwnership;
106
127
  private clearRuntimeSubscriptions;
107
128
  private observeRuntimeForTab;
129
+ private isRuntimeOwnedByTab;
130
+ private queueOrphanRuntimeDisposal;
131
+ private disposeRuntimeIfOrphan;
132
+ private adoptCurrentRuntimeAfterFailedRollback;
108
133
  private shouldSyncTabFromRuntimeEvent;
109
134
  private shouldScheduleDelayedSyncForRuntimeEvent;
110
135
  private shouldInvalidateCachedViewForRuntimeEvent;
@@ -151,6 +176,7 @@ export declare class AppTabsController {
151
176
  private parsePersistedScrollState;
152
177
  private parsePersistedSubmittedUserMessages;
153
178
  private saveTabs;
179
+ private writeTabsSnapshot;
154
180
  private filePath;
155
181
  private sessionDir;
156
182
  private scheduleProjectSessionRetention;