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
@@ -3,6 +3,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
+ import { captureCommandScope, isCommandRuntimeActive, isCommandScopeActive, } from "./command-host.js";
6
7
  import { getIdleRuntime, getRuntime, parsePathArgument } from "./command-runtime.js";
7
8
  import { createId } from "../id.js";
8
9
  import { runProcess } from "../process.js";
@@ -22,6 +23,7 @@ export class SessionCommandActions {
22
23
  const runtime = getIdleRuntime(this.host, "export");
23
24
  if (!runtime)
24
25
  return;
26
+ const scope = captureCommandScope(this.host);
25
27
  const outputPath = parsePathArgument(argumentsText);
26
28
  const resolvedOutputPath = outputPath ? resolve(runtime.cwd, outputPath) : undefined;
27
29
  this.host.setStatus("exporting session");
@@ -29,6 +31,8 @@ export class SessionCommandActions {
29
31
  const filePath = resolvedOutputPath?.endsWith(".jsonl")
30
32
  ? runtime.session.exportToJsonl(resolvedOutputPath)
31
33
  : await runtime.session.exportToHtml(resolvedOutputPath);
34
+ if (!isCommandScopeActive(this.host, scope))
35
+ return;
32
36
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Session exported to: ${filePath}` });
33
37
  this.host.setSessionStatus(runtime.session);
34
38
  this.host.toast.success("Session exported");
@@ -44,6 +48,8 @@ export class SessionCommandActions {
44
48
  this.host.setStatus("importing session");
45
49
  this.host.render();
46
50
  const result = await runtime.importFromJsonl(resolvedInputPath);
51
+ if (!isCommandRuntimeActive(this.host, runtime))
52
+ return;
47
53
  if (result.cancelled) {
48
54
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Import cancelled." });
49
55
  this.host.setSessionStatus(runtime.session);
@@ -55,17 +61,26 @@ export class SessionCommandActions {
55
61
  const runtime = getIdleRuntime(this.host, "share");
56
62
  if (!runtime)
57
63
  return;
64
+ const scope = captureCommandScope(this.host);
58
65
  const authResult = await runProcess("gh", ["auth", "status"], { maxBufferBytes: 32 * 1024 });
66
+ if (!isCommandScopeActive(this.host, scope))
67
+ return;
59
68
  if (authResult.status !== 0)
60
69
  throw new Error("GitHub CLI is not installed or is not logged in. Run `gh auth login` first.");
61
70
  const shareDir = join(getAgentDir(), "pix");
62
71
  await mkdir(shareDir, { recursive: true });
72
+ if (!isCommandScopeActive(this.host, scope))
73
+ return;
63
74
  const tmpFile = join(shareDir, `session-share-${randomUUID()}.html`);
64
75
  try {
65
76
  this.host.setStatus("creating share gist");
66
77
  this.host.render();
67
78
  await runtime.session.exportToHtml(tmpFile);
79
+ if (!isCommandScopeActive(this.host, scope))
80
+ return;
68
81
  const gistResult = await runProcess("gh", ["gist", "create", "--public=false", tmpFile], { maxBufferBytes: 64 * 1024 });
82
+ if (!isCommandScopeActive(this.host, scope))
83
+ return;
69
84
  if (gistResult.status !== 0)
70
85
  throw new Error(gistResult.stderr?.trim() || "Failed to create gist");
71
86
  const gistUrl = gistResult.stdout.trim();
@@ -81,10 +96,13 @@ export class SessionCommandActions {
81
96
  const runtime = getRuntime(this.host, "copy");
82
97
  if (!runtime)
83
98
  return;
99
+ const scope = captureCommandScope(this.host);
84
100
  const text = runtime.session.getLastAssistantText();
85
101
  if (!text)
86
102
  throw new Error("No agent messages to copy yet");
87
103
  await copyTextToClipboard(text);
104
+ if (!isCommandScopeActive(this.host, scope))
105
+ return;
88
106
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Copied last agent message to clipboard." });
89
107
  this.host.setSessionStatus(runtime.session);
90
108
  this.host.toast.success("Copied last agent message");
@@ -103,6 +121,7 @@ export class SessionCommandActions {
103
121
  return;
104
122
  const name = argumentsText.trim();
105
123
  if (!name) {
124
+ const scope = captureCommandScope(this.host);
106
125
  const branch = runtime.session.sessionManager.getBranch();
107
126
  const firstPrompt = firstUserMessageText(branch);
108
127
  if (!firstPrompt)
@@ -117,6 +136,8 @@ export class SessionCommandActions {
117
136
  for (const modelRef of modelRefs) {
118
137
  for (let attempt = 0; attempt < config.generationAttempts; attempt++) {
119
138
  generatedName = await generateSessionTitleWithRuntime(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRuntime, config, modelRef, AbortSignal.timeout(config.timeoutMs));
139
+ if (!isCommandScopeActive(this.host, scope))
140
+ return;
120
141
  if (generatedName)
121
142
  break;
122
143
  }
@@ -142,7 +163,10 @@ export class SessionCommandActions {
142
163
  const runtime = getRuntime(this.host, "session");
143
164
  if (!runtime)
144
165
  return;
166
+ const scope = captureCommandScope(this.host);
145
167
  const stats = await getCompleteSessionStats(runtime.session);
168
+ if (!isCommandScopeActive(this.host, scope))
169
+ return;
146
170
  const lines = [
147
171
  createStartupInfoMessage(runtime),
148
172
  "",
@@ -173,9 +197,12 @@ export class SessionCommandActions {
173
197
  const runtime = getRuntime(this.host, "usage");
174
198
  if (!runtime)
175
199
  return;
200
+ const scope = captureCommandScope(this.host);
176
201
  this.host.setStatus("loading usage");
177
202
  this.host.render();
178
203
  const accountReport = await queryAccountUsageReport();
204
+ if (!isCommandScopeActive(this.host, scope))
205
+ return;
179
206
  const accountUsage = formatAccountUsageReport(accountReport);
180
207
  if (accountUsage) {
181
208
  this.host.addEntry({ id: createId("system"), kind: "system", text: accountUsage });
@@ -183,6 +210,8 @@ export class SessionCommandActions {
183
210
  return;
184
211
  }
185
212
  const stats = await getCompleteSessionStats(runtime.session);
213
+ if (!isCommandScopeActive(this.host, scope))
214
+ return;
186
215
  const contextUsage = stats.contextUsage ?? runtime.session.getContextUsage();
187
216
  const model = runtime.session.model ? this.host.modelRef(runtime.session.model) : "not selected";
188
217
  const lines = [
@@ -204,8 +233,11 @@ export class SessionCommandActions {
204
233
  this.host.setSessionStatus(runtime.session);
205
234
  }
206
235
  async runChangelogCommand() {
236
+ const scope = captureCommandScope(this.host);
207
237
  const changelogPath = join(this.piPackageRoot(), "CHANGELOG.md");
208
238
  const raw = await readFile(changelogPath, "utf8");
239
+ if (!isCommandScopeActive(this.host, scope))
240
+ return;
209
241
  const lines = raw.trim().split(/\r?\n/).slice(0, 140);
210
242
  this.host.addEntry({ id: createId("system"), kind: "system", text: lines.join("\n") });
211
243
  this.host.setSessionStatus(this.host.runtime()?.session);
@@ -214,6 +246,7 @@ export class SessionCommandActions {
214
246
  const runtime = getRuntime(this.host, "update");
215
247
  if (!runtime)
216
248
  return;
249
+ const scope = captureCommandScope(this.host);
217
250
  const options = parsePixUpdateArgs(splitUpdateArguments(argumentsText));
218
251
  if (options.help) {
219
252
  this.host.addEntry({ id: createId("system"), kind: "system", text: pixUpdateUsage() });
@@ -223,6 +256,8 @@ export class SessionCommandActions {
223
256
  this.host.setStatus("checking updates");
224
257
  this.host.render();
225
258
  const result = await checkPixUpdate();
259
+ if (!isCommandScopeActive(this.host, scope))
260
+ return;
226
261
  const globalPiResult = checkGlobalPiInstall(result.packageRoot);
227
262
  const forceHint = options.force ? "\n\n/update is check-only. To force a reinstall, run `pix update --force` in your shell and restart Pix." : "";
228
263
  this.host.addEntry({
@@ -263,16 +298,23 @@ export class SessionCommandActions {
263
298
  const runtime = getIdleRuntime(this.host, "reload");
264
299
  if (!runtime)
265
300
  return;
301
+ const scope = captureCommandScope(this.host);
266
302
  this.host.setStatus("reloading");
267
303
  this.host.render();
268
304
  try {
269
305
  await this.host.awaitCurrentSessionExtensions(runtime);
306
+ if (!isCommandScopeActive(this.host, scope))
307
+ return;
270
308
  await runtime.session.reload();
309
+ if (!isCommandScopeActive(this.host, scope))
310
+ return;
271
311
  this.host.setSessionStatus(runtime.session);
272
312
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
273
313
  this.host.toast.success("Reloaded resources");
274
314
  }
275
315
  catch (error) {
316
+ if (!isCommandScopeActive(this.host, scope))
317
+ return;
276
318
  this.host.setSessionStatus(runtime.session);
277
319
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Reload failed: ${error instanceof Error ? error.message : String(error)}` });
278
320
  this.host.toast.error("Reload failed");
@@ -282,6 +324,7 @@ export class SessionCommandActions {
282
324
  const runtime = getIdleRuntime(this.host, "delete");
283
325
  if (!runtime)
284
326
  return;
327
+ const scope = captureCommandScope(this.host);
285
328
  const sessionManager = runtime.session.sessionManager;
286
329
  const currentSessionFile = sessionManager.getSessionFile();
287
330
  const currentSessionId = sessionManager.getSessionId();
@@ -304,6 +347,8 @@ export class SessionCommandActions {
304
347
  },
305
348
  { value: false, label: "Cancel" },
306
349
  ], { title: "Delete session?", searchable: false, preserveStatus: true });
350
+ if (!isCommandScopeActive(this.host, scope))
351
+ return;
307
352
  if (confirm !== true) {
308
353
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete cancelled." });
309
354
  this.host.setSessionStatus(runtime.session);
@@ -317,6 +362,8 @@ export class SessionCommandActions {
317
362
  if (deleteCurrent) {
318
363
  await this.host.awaitCurrentSessionExtensions(runtime);
319
364
  const result = await runtime.newSession();
365
+ if (!isCommandRuntimeActive(this.host, runtime))
366
+ return;
320
367
  if (result.cancelled) {
321
368
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete succeeded, but new session was cancelled." });
322
369
  this.host.setSessionStatus(runtime.session);
@@ -334,6 +381,8 @@ export class SessionCommandActions {
334
381
  this.host.setSessionStatus(runtime.session);
335
382
  }
336
383
  else {
384
+ if (!isCommandScopeActive(this.host, scope))
385
+ return;
337
386
  this.host.addEntry({
338
387
  id: createId("system"),
339
388
  kind: "system",
@@ -358,7 +407,11 @@ export class SessionCommandActions {
358
407
  this.host.setStatus("starting new session");
359
408
  this.host.render();
360
409
  await this.host.awaitCurrentSessionExtensions(runtime);
410
+ if (!isCommandRuntimeActive(this.host, runtime))
411
+ return;
361
412
  const result = await runtime.newSession();
413
+ if (!isCommandRuntimeActive(this.host, runtime))
414
+ return;
362
415
  if (result.cancelled) {
363
416
  this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
364
417
  this.host.setSessionStatus(runtime.session);
@@ -378,6 +431,7 @@ export class SessionCommandActions {
378
431
  const runtime = getRuntime(this.host, "compact");
379
432
  if (!runtime)
380
433
  return;
434
+ const scope = captureCommandScope(this.host);
381
435
  if (runtime.session.isCompacting) {
382
436
  this.host.toast.warning("Compaction already running");
383
437
  return;
@@ -385,6 +439,8 @@ export class SessionCommandActions {
385
439
  this.host.setStatus(customInstructions ? "compacting with instructions" : "compacting");
386
440
  this.host.render();
387
441
  const result = await runtime.session.compact(customInstructions);
442
+ if (!isCommandScopeActive(this.host, scope))
443
+ return;
388
444
  this.host.addEntry({
389
445
  id: createId("system"),
390
446
  kind: "system",
@@ -1,8 +1,12 @@
1
- import { type InteractiveShellCommandResult } from "./shell-command.js";
1
+ import { type ChatShellCommandHandlers, type InteractiveShellCommandResult, type RunningChatShellCommand } from "./shell-command.js";
2
2
  import type { Entry, SessionActivity } from "../types.js";
3
+ type AppShellControllerDeps = {
4
+ runChatShellCommand(command: string, cwd: string, handlers: ChatShellCommandHandlers): RunningChatShellCommand;
5
+ };
3
6
  export type AppShellControllerHost = {
4
7
  readonly cwd: string;
5
8
  isRunning(): boolean;
9
+ activeScopeKey(): string | undefined;
6
10
  addEntry(entry: Entry): void;
7
11
  touchEntry(entry: Entry): void;
8
12
  setStatus(status: string): void;
@@ -13,9 +17,10 @@ export type AppShellControllerHost = {
13
17
  };
14
18
  export declare class AppShellController {
15
19
  private readonly host;
20
+ private readonly deps;
16
21
  private activeRun;
17
22
  private renderTimer;
18
- constructor(host: AppShellControllerHost);
23
+ constructor(host: AppShellControllerHost, deps?: AppShellControllerDeps);
19
24
  isRunning(): boolean;
20
25
  run(command: string): Promise<InteractiveShellCommandResult>;
21
26
  sendInput(text: string): boolean;
@@ -26,4 +31,6 @@ export declare class AppShellController {
26
31
  private finishEntry;
27
32
  private touchAndScheduleRender;
28
33
  private flushRender;
34
+ private isScopeActive;
29
35
  }
36
+ export {};
@@ -1,19 +1,23 @@
1
1
  import { createId } from "../id.js";
2
- import { runChatShellCommand } from "./shell-command.js";
2
+ import { runChatShellCommand, } from "./shell-command.js";
3
3
  const SHELL_RENDER_THROTTLE_MS = 33;
4
+ const defaultDeps = { runChatShellCommand };
4
5
  export class AppShellController {
5
6
  host;
7
+ deps;
6
8
  activeRun;
7
9
  renderTimer;
8
- constructor(host) {
10
+ constructor(host, deps = defaultDeps) {
9
11
  this.host = host;
12
+ this.deps = deps;
10
13
  }
11
14
  isRunning() {
12
- return this.activeRun !== undefined;
15
+ return this.activeRun !== undefined && this.isScopeActive(this.activeRun.scopeKey);
13
16
  }
14
17
  async run(command) {
15
18
  if (this.activeRun)
16
19
  throw new Error("A shell command is already running");
20
+ const scopeKey = this.host.activeScopeKey();
17
21
  const entry = {
18
22
  id: createId("shell"),
19
23
  kind: "shell",
@@ -26,11 +30,11 @@ export class AppShellController {
26
30
  this.host.setStatus(`shell: ${command}`);
27
31
  this.host.setSessionActivity("running");
28
32
  this.host.render();
29
- const runningCommand = runChatShellCommand(command, this.host.cwd, {
30
- onOutput: (chunk) => this.appendOutput(entry, chunk),
31
- onSettled: (result) => this.finishEntry(entry, result),
33
+ const runningCommand = this.deps.runChatShellCommand(command, this.host.cwd, {
34
+ onOutput: (chunk) => this.appendOutput(entry, chunk, scopeKey),
35
+ onSettled: (result) => this.finishEntry(entry, result, scopeKey),
32
36
  });
33
- this.activeRun = { entry, command: runningCommand };
37
+ this.activeRun = { entry, command: runningCommand, scopeKey };
34
38
  try {
35
39
  return await runningCommand.done;
36
40
  }
@@ -38,24 +42,26 @@ export class AppShellController {
38
42
  if (this.activeRun?.entry === entry)
39
43
  this.activeRun = undefined;
40
44
  this.flushRender();
41
- this.host.restoreSessionStatus();
42
- if (this.host.isRunning())
43
- this.host.render();
45
+ if (this.isScopeActive(scopeKey)) {
46
+ this.host.restoreSessionStatus();
47
+ if (this.host.isRunning())
48
+ this.host.render();
49
+ }
44
50
  }
45
51
  }
46
52
  sendInput(text) {
47
53
  const activeRun = this.activeRun;
48
- if (!activeRun)
54
+ if (!activeRun || !this.isScopeActive(activeRun.scopeKey))
49
55
  return false;
50
56
  const input = text.endsWith("\n") ? text : `${text}\n`;
51
- this.appendInputEcho(activeRun.entry, input);
57
+ this.appendInputEcho(activeRun.entry, input, activeRun.scopeKey);
52
58
  return activeRun.command.writeInput(input);
53
59
  }
54
60
  interrupt() {
55
61
  const activeRun = this.activeRun;
56
- if (!activeRun)
62
+ if (!activeRun || !this.isScopeActive(activeRun.scopeKey))
57
63
  return false;
58
- this.appendOutput(activeRun.entry, "\n^C\n");
64
+ this.appendOutput(activeRun.entry, "\n^C\n", activeRun.scopeKey);
59
65
  return activeRun.command.interrupt();
60
66
  }
61
67
  dispose() {
@@ -66,23 +72,25 @@ export class AppShellController {
66
72
  this.activeRun = undefined;
67
73
  activeRun?.command.kill("SIGTERM");
68
74
  }
69
- appendOutput(entry, chunk) {
75
+ appendOutput(entry, chunk, scopeKey) {
70
76
  entry.output = appendTerminalChunk(entry.output, chunk);
71
- this.touchAndScheduleRender(entry);
77
+ this.touchAndScheduleRender(entry, scopeKey);
72
78
  }
73
- appendInputEcho(entry, input) {
79
+ appendInputEcho(entry, input, scopeKey) {
74
80
  entry.output = appendTerminalChunk(entry.output, formatInputEcho(input));
75
- this.touchAndScheduleRender(entry);
81
+ this.touchAndScheduleRender(entry, scopeKey);
76
82
  }
77
- finishEntry(entry, result) {
83
+ finishEntry(entry, result, scopeKey) {
78
84
  entry.status = "done";
79
85
  entry.exitCode = result.exitCode;
80
86
  entry.signal = result.signal;
81
87
  if (result.error)
82
88
  entry.error = result.error;
83
- this.touchAndScheduleRender(entry);
89
+ this.touchAndScheduleRender(entry, scopeKey);
84
90
  }
85
- touchAndScheduleRender(entry) {
91
+ touchAndScheduleRender(entry, scopeKey) {
92
+ if (!this.isScopeActive(scopeKey))
93
+ return;
86
94
  this.host.touchEntry(entry);
87
95
  if (!this.host.isRunning() || this.renderTimer)
88
96
  return;
@@ -99,6 +107,9 @@ export class AppShellController {
99
107
  clearTimeout(this.renderTimer);
100
108
  this.renderTimer = undefined;
101
109
  }
110
+ isScopeActive(scopeKey) {
111
+ return this.host.activeScopeKey() === scopeKey;
112
+ }
102
113
  }
103
114
  function appendTerminalChunk(current, chunk) {
104
115
  let next = current;
@@ -3,6 +3,7 @@ import { type PixLogDetails, type PixLogLevel } from "../logger.js";
3
3
  import type { Entry } from "../types.js";
4
4
  export type AppExtensionActionsHost = {
5
5
  isRunning(): boolean;
6
+ runtime(): AgentSessionRuntime | undefined;
6
7
  getInput(): string;
7
8
  setInput(value: string): void;
8
9
  awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
@@ -22,5 +23,7 @@ export declare class AppExtensionActionsController {
22
23
  constructor(host: AppExtensionActionsHost, logExtensionError?: ExtensionErrorLogger);
23
24
  createCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
24
25
  waitForSessionIdle(runtime: AgentSessionRuntime): Promise<void>;
26
+ private isRuntimeActive;
27
+ private isSessionActive;
25
28
  handleExtensionError(error: ExtensionError): void;
26
29
  }
@@ -13,21 +13,26 @@ export class AppExtensionActionsController {
13
13
  waitForIdle: () => this.waitForSessionIdle(runtime),
14
14
  newSession: async (options) => {
15
15
  await this.host.awaitCurrentSessionExtensions(runtime);
16
+ if (!this.isRuntimeActive(runtime))
17
+ return { cancelled: true };
16
18
  const result = await runtime.newSession(options);
17
- if (!result.cancelled)
19
+ if (!result.cancelled && this.isRuntimeActive(runtime))
18
20
  this.host.afterSessionReplacement("Started a new session.");
19
21
  return result;
20
22
  },
21
23
  fork: async (entryId, options) => {
22
24
  await this.host.awaitCurrentSessionExtensions(runtime);
25
+ if (!this.isRuntimeActive(runtime))
26
+ return { cancelled: true };
23
27
  const result = await runtime.fork(entryId, options);
24
- if (!result.cancelled)
28
+ if (!result.cancelled && this.isRuntimeActive(runtime))
25
29
  this.host.afterSessionReplacement("Forked to a new session.");
26
30
  return result;
27
31
  },
28
32
  navigateTree: async (targetId, options) => {
29
- const result = await runtime.session.navigateTree(targetId, options);
30
- if (!result.cancelled && !result.aborted) {
33
+ const session = runtime.session;
34
+ const result = await session.navigateTree(targetId, options);
35
+ if (!result.cancelled && !result.aborted && this.isSessionActive(runtime, session)) {
31
36
  this.host.resetSessionView();
32
37
  this.host.loadSessionHistory();
33
38
  if (result.editorText && !this.host.getInput().trim())
@@ -39,15 +44,22 @@ export class AppExtensionActionsController {
39
44
  },
40
45
  switchSession: async (sessionPath, options) => {
41
46
  await this.host.awaitCurrentSessionExtensions(runtime);
47
+ if (!this.isRuntimeActive(runtime))
48
+ return { cancelled: true };
42
49
  const result = await runtime.switchSession(sessionPath, options);
43
- if (!result.cancelled)
50
+ if (!result.cancelled && this.isRuntimeActive(runtime))
44
51
  this.host.afterSessionReplacement(`Switched session: ${sessionPath}`);
45
52
  return result;
46
53
  },
47
54
  reload: async () => {
55
+ const session = runtime.session;
48
56
  await this.host.awaitCurrentSessionExtensions(runtime);
49
- await runtime.session.reload();
50
- this.host.setSessionStatus(runtime.session);
57
+ if (!this.isSessionActive(runtime, session))
58
+ return;
59
+ await session.reload();
60
+ if (!this.isSessionActive(runtime, session))
61
+ return;
62
+ this.host.setSessionStatus(session);
51
63
  this.host.showToast("Reloaded resources", "success");
52
64
  this.host.render();
53
65
  },
@@ -56,6 +68,12 @@ export class AppExtensionActionsController {
56
68
  async waitForSessionIdle(runtime) {
57
69
  await runtime.session.waitForIdle();
58
70
  }
71
+ isRuntimeActive(runtime) {
72
+ return this.host.isRunning() && this.host.runtime() === runtime;
73
+ }
74
+ isSessionActive(runtime, session) {
75
+ return this.isRuntimeActive(runtime) && runtime.session === session;
76
+ }
59
77
  handleExtensionError(error) {
60
78
  const sourceText = formatExtensionErrorSource(error.extensionPath);
61
79
  const pathText = error.extensionPath ? ` (${error.extensionPath})` : "";
@@ -8,6 +8,7 @@ export type ExtensionTerminalInputResult = {
8
8
  export type ExtensionUiControllerHost = {
9
9
  readonly theme: Theme;
10
10
  activeExtensionUiScope?(): string | undefined;
11
+ isExtensionUiScopeActive?(scopeKey: string): boolean;
11
12
  isRunning(): boolean;
12
13
  render(): void;
13
14
  showToast(message: string, kind?: ToastKind, options?: {
@@ -38,12 +39,13 @@ export declare class ExtensionUiController {
38
39
  clearWidgets(scopeKey?: string, options?: {
39
40
  cancelCustomUi?: boolean;
40
41
  }): void;
42
+ cancelCustomUi(scopeKey?: string): void;
41
43
  suppressWidget(key: string): void;
42
44
  handleTerminalInput(data: string): ExtensionTerminalInputResult;
43
45
  renderActiveCustomUi(width: number): string[] | undefined;
44
46
  activeCustomUiUsesEditor(): boolean;
45
47
  handleCustomUiMouse(event: ExtensionInputMouseEvent): boolean;
46
- widgetTuiHandle(): WidgetTuiHandle;
48
+ widgetTuiHandle(scopeKey?: string): WidgetTuiHandle;
47
49
  createExtensionUIContext(scopeKey?: string): PixExtensionUIContext;
48
50
  private setAboveInputWidget;
49
51
  private clearAboveInputWidget;
@@ -60,6 +62,8 @@ export declare class ExtensionUiController {
60
62
  private rejectActiveCustomUi;
61
63
  private finishActiveCustomUi;
62
64
  private activeCustomUiForActiveScope;
65
+ private isScopeActive;
66
+ private scopedMenuController;
63
67
  private activeScopeKey;
64
68
  private normalizeScopeKey;
65
69
  private scopedWidgetKey;