pi-ui-extend 0.1.35 → 0.1.37

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 (80) hide show
  1. package/dist/app/app.d.ts +8 -0
  2. package/dist/app/app.js +48 -5
  3. package/dist/app/commands/command-controller.js +1 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-registry.d.ts +1 -0
  9. package/dist/app/commands/command-registry.js +8 -0
  10. package/dist/app/commands/command-session-actions.d.ts +2 -0
  11. package/dist/app/commands/command-session-actions.js +81 -1
  12. package/dist/app/extensions/extension-actions-controller.d.ts +5 -1
  13. package/dist/app/extensions/extension-actions-controller.js +35 -2
  14. package/dist/app/input/input-controller.d.ts +2 -0
  15. package/dist/app/input/input-controller.js +50 -2
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +2 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +49 -0
  18. package/dist/app/input/voice-controller.js +1 -1
  19. package/dist/app/popup/popup-action-controller.d.ts +2 -3
  20. package/dist/app/popup/popup-action-controller.js +2 -5
  21. package/dist/app/rendering/message-content.js +4 -3
  22. package/dist/app/rendering/render-controller.js +21 -38
  23. package/dist/app/rendering/status-line-renderer.d.ts +1 -0
  24. package/dist/app/rendering/status-line-renderer.js +14 -2
  25. package/dist/app/runtime.js +12 -2
  26. package/dist/app/screen/mouse-controller.js +2 -0
  27. package/dist/app/session/session-event-controller.d.ts +7 -0
  28. package/dist/app/session/session-event-controller.js +10 -13
  29. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  30. package/dist/app/session/session-lifecycle-controller.js +7 -0
  31. package/dist/app/session/tabs-controller.d.ts +1 -0
  32. package/dist/app/session/tabs-controller.js +1 -0
  33. package/dist/app/terminal/terminal-controller.js +1 -0
  34. package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
  35. package/dist/app/terminal/terminal-output-buffer.js +24 -16
  36. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  37. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  38. package/dist/bundled-extensions/terminal-bell/index.js +118 -33
  39. package/dist/markdown-format.d.ts +1 -0
  40. package/dist/markdown-format.js +30 -16
  41. package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
  42. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  43. package/dist/tool-renderers/apply-patch.js +6 -1
  44. package/dist/tool-renderers/patch-normalize.d.ts +24 -0
  45. package/dist/tool-renderers/patch-normalize.js +163 -0
  46. package/external/pi-tools-suite/README.md +3 -2
  47. package/external/pi-tools-suite/package.json +5 -5
  48. package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
  49. package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
  50. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
  51. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
  52. package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
  53. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
  54. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
  55. package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
  56. package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
  57. package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
  58. package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
  59. package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
  60. package/external/pi-tools-suite/src/dcp/config.ts +10 -6
  61. package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
  62. package/external/pi-tools-suite/src/dcp/index.ts +204 -27
  63. package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
  64. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
  65. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
  66. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
  67. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
  68. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  69. package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
  70. package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
  71. package/external/pi-tools-suite/src/dcp/state.ts +62 -4
  72. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
  73. package/external/pi-tools-suite/src/index.ts +1 -0
  74. package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
  75. package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
  76. package/external/pi-tools-suite/src/todo/index.ts +24 -0
  77. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
  78. package/external/pi-tools-suite/src/usage/index.ts +18 -4
  79. package/package.json +4 -4
  80. package/schemas/pi-tools-suite.json +24 -0
package/dist/app/app.d.ts CHANGED
@@ -43,6 +43,13 @@ export declare class PiUiExtendApp {
43
43
  private readonly toastNotifier;
44
44
  private readonly extensionShutdownHandler;
45
45
  private runtime;
46
+ /**
47
+ * Maps each session runtime to the isolated extension event bus it was
48
+ * created with. The renderer uses this to emit signals (e.g. retry state)
49
+ * back to the extensions running inside a specific runtime/tab, so a signal
50
+ * for one tab never reaches another tab's extensions.
51
+ */
52
+ private readonly extensionEventBusByRuntime;
46
53
  private readonly inputEditor;
47
54
  private lastInputEditorContentVersion;
48
55
  private readonly requestHistory;
@@ -66,6 +73,7 @@ export declare class PiUiExtendApp {
66
73
  start(): Promise<void>;
67
74
  private checkPixUpdateOnStartup;
68
75
  private bindCurrentSession;
76
+ private awaitCurrentSessionExtensions;
69
77
  private activateRuntime;
70
78
  private createExtensionEventBus;
71
79
  private handleTerminalBellAttention;
package/dist/app/app.js CHANGED
@@ -112,6 +112,13 @@ export class PiUiExtendApp {
112
112
  };
113
113
  extensionShutdownHandler = () => { };
114
114
  runtime;
115
+ /**
116
+ * Maps each session runtime to the isolated extension event bus it was
117
+ * created with. The renderer uses this to emit signals (e.g. retry state)
118
+ * back to the extensions running inside a specific runtime/tab, so a signal
119
+ * for one tab never reaches another tab's extensions.
120
+ */
121
+ extensionEventBusByRuntime = new WeakMap();
115
122
  inputEditor = new InputEditor();
116
123
  lastInputEditorContentVersion = this.inputEditor.contentVersion;
117
124
  requestHistory;
@@ -166,12 +173,24 @@ export class PiUiExtendApp {
166
173
  maxProjectSessions: () => this.pixConfig.maxProjectSessions,
167
174
  blinkController: this.blinkController,
168
175
  runtime: () => this.runtime,
169
- createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options), this.runtime === undefined ? {} : { reuseServicesFrom: this.runtime }),
176
+ createRuntimeForNewSession: () => this.createRuntime(
177
+ // Never reuse services across tabs. The SDK ties the extension
178
+ // runtime (resourceLoader.getExtensions().runtime) to the
179
+ // resourceLoader, and that runtime is shared by every session
180
+ // built from the same services. When any such session is
181
+ // disposed (tab close / session replacement / reload), the SDK
182
+ // invalidates that shared runtime, which makes every sibling
183
+ // session's captured `pi`/ctx stale — session_start handlers
184
+ // then throw "ctx is stale after session replacement or reload".
185
+ // Fresh services per session = fresh extension runtime = no
186
+ // cross-session invalidation.
187
+ newTabRuntimeOptions(this.options)),
170
188
  createRuntimeForSession: (sessionPath) => this.createRuntime({
171
189
  ...this.options,
172
190
  noSession: false,
173
191
  sessionPath,
174
192
  }),
193
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
175
194
  activateRuntime: (runtime, options) => this.activateRuntime(runtime, options),
176
195
  disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
177
196
  isRunning: () => this.running,
@@ -317,6 +336,7 @@ export class PiUiExtendApp {
317
336
  isRunning: () => this.running,
318
337
  getInput: () => this.input,
319
338
  setInput: (value) => this.setInput(value),
339
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
320
340
  resetSessionView: () => this.resetSessionView(),
321
341
  loadSessionHistory: () => this.loadSessionHistory(),
322
342
  afterSessionReplacement: (message) => this.afterSessionReplacement(message),
@@ -341,6 +361,7 @@ export class PiUiExtendApp {
341
361
  this.workspaceActions = new AppWorkspaceActionsController({
342
362
  entries: this.entries,
343
363
  runtime: () => this.runtime,
364
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
344
365
  findUserEntry: (entryId) => this.findUserEntry(entryId),
345
366
  touchEntry: (entry) => this.touchEntry(entry),
346
367
  resetSessionView: () => this.resetSessionView(),
@@ -370,6 +391,16 @@ export class PiUiExtendApp {
370
391
  setSessionActivity: (activity) => this.setSessionActivity(activity),
371
392
  updateQueuedMessageStatus: () => this.queuedMessages.updateQueuedMessageStatus(),
372
393
  flushAutoUserMessages: () => { void this.queuedMessages.flushAutoUserMessages(); },
394
+ emitExtensionEvent: (channel, data) => {
395
+ // Emit a signal to the extensions running inside the active
396
+ // runtime's event bus. Used to inform extensions (terminal-bell,
397
+ // todo) about session lifecycle state the SDK doesn't forward to
398
+ // them, such as auto-retry being in progress.
399
+ const runtime = this.runtime;
400
+ if (!runtime)
401
+ return;
402
+ this.extensionEventBusByRuntime.get(runtime)?.emit(channel, data);
403
+ },
373
404
  prepareWorkspaceMutation: (toolName, args) => this.workspaceActions.prepareWorkspaceMutation(toolName, args),
374
405
  workspaceMutationFromToolExecution: (input) => this.workspaceActions.workspaceMutationFromToolExecution(input),
375
406
  recordWorkspaceMutationForUserEntry: (entryId, mutation) => this.workspaceActions.recordWorkspaceMutationForUserEntry(entryId, mutation),
@@ -471,6 +502,7 @@ export class PiUiExtendApp {
471
502
  showMenu: (items, options) => this.popupMenus.menuController.show(items, options),
472
503
  getModelMenuItems: (query) => this.menuItems.getModelMenuItems(query),
473
504
  getThinkingMenuItems: (query) => this.menuItems.getThinkingMenuItems(query),
505
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
474
506
  modelRef: (model) => this.menuItems.modelRef(model),
475
507
  getFavoriteScopedModels: () => this.menuItems.getFavoriteScopedModels(),
476
508
  setSessionStatus: (session) => this.setSessionStatus(session),
@@ -518,9 +550,8 @@ export class PiUiExtendApp {
518
550
  setSessionStatus: (session) => this.setSessionStatus(session),
519
551
  showToast: (message, kind) => this.showToast(message, kind),
520
552
  render: () => this.render(),
521
- resetSessionView: () => this.resetSessionView(),
522
- bindCurrentSession: () => this.bindCurrentSession(),
523
- loadSessionHistory: () => this.loadSessionHistory(),
553
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
554
+ afterSessionReplacement: (message) => this.afterSessionReplacement(message),
524
555
  scrollToConversationEntry: (entryId) => this.scrollController.scrollToConversationEntry(entryId),
525
556
  scrollToUserMessageJumpTarget: (target) => this.scrollToUserMessageJumpTarget(target),
526
557
  }, this.popupMenus, this.commandController, this.menuItems, this.queuedMessages, this.workspaceActions);
@@ -763,10 +794,16 @@ export class PiUiExtendApp {
763
794
  this.slashCommands = this.commandController.slashCommands;
764
795
  }
765
796
  createRuntime(options, runtimeOptions = {}) {
797
+ const eventBus = this.createExtensionEventBus();
766
798
  return createPixRuntime(options, {
767
- eventBus: this.createExtensionEventBus(),
799
+ eventBus,
768
800
  config: this.pixConfig,
769
801
  ...runtimeOptions,
802
+ }).then((runtime) => {
803
+ // Record the bus this runtime's extensions use, so the renderer can
804
+ // emit signals (retry state, etc.) targeted at this runtime's extensions.
805
+ this.extensionEventBusByRuntime.set(runtime, eventBus);
806
+ return runtime;
770
807
  });
771
808
  }
772
809
  async loadStartupConfig() {
@@ -824,11 +861,17 @@ export class PiUiExtendApp {
824
861
  async bindCurrentSession(options) {
825
862
  await this.sessionLifecycle.bindCurrentSession(options);
826
863
  }
864
+ async awaitCurrentSessionExtensions(runtime) {
865
+ await this.sessionLifecycle.awaitCurrentSessionExtensions(runtime);
866
+ }
827
867
  async activateRuntime(runtime, options) {
828
868
  this.runtime = runtime;
829
869
  runtime.setRebindSession(async () => {
830
870
  await this.bindCurrentSession({ awaitExtensions: false });
831
871
  });
872
+ runtime.setBeforeSessionInvalidate(() => {
873
+ this.extensionUiController.clearWidgets(this.activeExtensionUiScope());
874
+ });
832
875
  await this.bindCurrentSession(options);
833
876
  }
834
877
  createExtensionEventBus() {
@@ -49,6 +49,7 @@ export class AppCommandController {
49
49
  runReloadCommand: () => this.sessionActions.runReloadCommand(),
50
50
  runNewSessionCommand: () => this.sessionActions.runNewSessionCommand(),
51
51
  runNewTabCommand: () => this.host.openNewTab(),
52
+ runDeleteCommand: (argumentsText) => this.sessionActions.runDeleteCommand(argumentsText),
52
53
  runCompactCommand: (customInstructions) => this.sessionActions.runCompactCommand(customInstructions),
53
54
  runForkCommand: (argumentsText) => this.navigationActions.runForkCommand(argumentsText),
54
55
  runCloneCommand: () => this.navigationActions.runCloneCommand(),
@@ -7,6 +7,7 @@ export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
7
7
  export type CommandControllerHost = {
8
8
  readonly options: AppOptions;
9
9
  runtime(): AgentSessionRuntime | undefined;
10
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
10
11
  requestHistory(): AppRequestHistory;
11
12
  getInput(): string;
12
13
  setInput(value: string): void;
@@ -14,6 +14,7 @@ export declare class ModelCommandActions {
14
14
  runModelCommand(model: SessionModel): Promise<void>;
15
15
  runThinkingCommand(level: ThinkingLevel): Promise<void>;
16
16
  private addPersistentSystemEntry;
17
+ private reloadAfterModelChange;
17
18
  private saveDefaultModel;
18
19
  private saveDefaultThinking;
19
20
  }
@@ -269,6 +269,17 @@ export class ModelCommandActions {
269
269
  this.host.render();
270
270
  await runtime.session.setModel(model);
271
271
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected model ${ref}` });
272
+ if (runtime.session.isStreaming) {
273
+ this.host.addEntry({
274
+ id: createId("system"),
275
+ kind: "system",
276
+ text: "Skipped reload because the agent is still running. Run /reload when idle to refresh model-specific tools.",
277
+ });
278
+ this.host.toast.warning("Model changed; reload skipped while the agent is running");
279
+ this.host.setSessionStatus(runtime.session);
280
+ return;
281
+ }
282
+ await this.reloadAfterModelChange(runtime.session, ref);
272
283
  this.host.setSessionStatus(runtime.session);
273
284
  }
274
285
  async runThinkingCommand(level) {
@@ -285,6 +296,27 @@ export class ModelCommandActions {
285
296
  appendPixSystemDisplayEntry(session, text);
286
297
  this.host.addEntry({ id: createId("system"), kind: "system", text });
287
298
  }
299
+ async reloadAfterModelChange(session, ref) {
300
+ this.host.setStatus(`reloading resources for ${ref}`);
301
+ this.host.render();
302
+ try {
303
+ await session.reload();
304
+ this.host.addEntry({
305
+ id: createId("system"),
306
+ kind: "system",
307
+ text: `Reloaded resources after model change to ${ref}`,
308
+ });
309
+ this.host.toast.success("Model changed and resources reloaded");
310
+ }
311
+ catch (error) {
312
+ this.host.addEntry({
313
+ id: createId("error"),
314
+ kind: "error",
315
+ text: `Model changed to ${ref}, but reload failed: ${error instanceof Error ? error.message : String(error)}`,
316
+ });
317
+ this.host.toast.error("Model changed, but reload failed");
318
+ }
319
+ }
288
320
  saveDefaultModel(modelRef) {
289
321
  const saved = savePixDefaultModel(modelRef);
290
322
  if (!saved)
@@ -50,6 +50,7 @@ export class NavigationCommandActions {
50
50
  throw new Error("No user messages to fork from");
51
51
  this.host.setStatus("forking session");
52
52
  this.host.render();
53
+ await this.host.awaitCurrentSessionExtensions(runtime);
53
54
  const result = await runtime.fork(entryId);
54
55
  if (result.cancelled) {
55
56
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
@@ -74,6 +75,7 @@ export class NavigationCommandActions {
74
75
  }
75
76
  this.host.setStatus("cloning session");
76
77
  this.host.render();
78
+ await this.host.awaitCurrentSessionExtensions(runtime);
77
79
  const result = await runtime.fork(leafId, { position: "at" });
78
80
  if (result.cancelled) {
79
81
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Clone cancelled." });
@@ -228,6 +230,7 @@ export class NavigationCommandActions {
228
230
  const resolvedSessionPath = resolve(runtime.cwd, sessionPath);
229
231
  this.host.setStatus("switching session");
230
232
  this.host.render();
233
+ await this.host.awaitCurrentSessionExtensions(runtime);
231
234
  const result = await runtime.switchSession(resolvedSessionPath);
232
235
  if (result.cancelled) {
233
236
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
@@ -33,6 +33,7 @@ export type CommandRegistryActions = {
33
33
  runResumeCommand(): Promise<void>;
34
34
  runNewSessionCommand(): Promise<void>;
35
35
  runNewTabCommand(): Promise<void>;
36
+ runDeleteCommand(argumentsText: string): Promise<void>;
36
37
  runCompactCommand(customInstructions?: string): Promise<void>;
37
38
  };
38
39
  export declare function createSlashCommands(actions: CommandRegistryActions, host: CommandControllerHost): readonly SlashCommand[];
@@ -247,6 +247,14 @@ export function createSlashCommands(actions, host) {
247
247
  keywords: ["tab", "session", "fresh", "new"],
248
248
  run: () => actions.runNewTabCommand(),
249
249
  },
250
+ {
251
+ name: "delete",
252
+ description: "Delete the current (or specified) session file plus its sidecar DCP state",
253
+ kind: "builtin",
254
+ keywords: ["remove", "destroy", "purge", "session", "sidecar", "dcp"],
255
+ allowArguments: true,
256
+ run: (argumentsText) => actions.runDeleteCommand(argumentsText),
257
+ },
250
258
  {
251
259
  name: "compact",
252
260
  description: "Manually compact the session context",
@@ -14,6 +14,8 @@ export declare class SessionCommandActions {
14
14
  runUpdateCommand(argumentsText: string): Promise<void>;
15
15
  runHotkeysCommand(): Promise<void>;
16
16
  runReloadCommand(): Promise<void>;
17
+ runDeleteCommand(argumentsText: string): Promise<void>;
18
+ private removeDcpSidecarState;
17
19
  runNewSessionCommand(): Promise<void>;
18
20
  runCompactCommand(customInstructions?: string): Promise<void>;
19
21
  private piPackageRoot;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, rm } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
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
6
  import { getIdleRuntime, getRuntime, parsePathArgument } from "./command-runtime.js";
@@ -260,6 +260,7 @@ export class SessionCommandActions {
260
260
  this.host.setStatus("reloading");
261
261
  this.host.render();
262
262
  try {
263
+ await this.host.awaitCurrentSessionExtensions(runtime);
263
264
  await runtime.session.reload();
264
265
  this.host.setSessionStatus(runtime.session);
265
266
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
@@ -271,12 +272,86 @@ export class SessionCommandActions {
271
272
  this.host.toast.error("Reload failed");
272
273
  }
273
274
  }
275
+ async runDeleteCommand(argumentsText) {
276
+ const runtime = getIdleRuntime(this.host, "delete");
277
+ if (!runtime)
278
+ return;
279
+ const sessionManager = runtime.session.sessionManager;
280
+ const currentSessionFile = sessionManager.getSessionFile();
281
+ const currentSessionId = sessionManager.getSessionId();
282
+ const targetArgument = argumentsText.trim();
283
+ const isCurrent = !targetArgument;
284
+ const targetSessionFile = targetArgument ? resolve(runtime.cwd, targetArgument) : currentSessionFile;
285
+ if (!targetSessionFile) {
286
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Nothing to delete: this session is in-memory and not persisted." });
287
+ this.host.toast.info("Nothing to delete");
288
+ this.host.setSessionStatus(runtime.session);
289
+ return;
290
+ }
291
+ const targetSessionId = parseSessionIdFromFileName(targetSessionFile) ?? currentSessionId;
292
+ const confirm = await this.host.showMenu([
293
+ {
294
+ value: true,
295
+ label: `Yes, delete ${isCurrent ? "the current session" : basename(targetSessionFile)}`,
296
+ description: "This permanently removes the session file and any sidecar data. This cannot be undone.",
297
+ variant: "error",
298
+ },
299
+ { value: false, label: "Cancel" },
300
+ ], { title: "Delete session?", searchable: false, preserveStatus: true });
301
+ if (confirm !== true) {
302
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete cancelled." });
303
+ this.host.setSessionStatus(runtime.session);
304
+ return;
305
+ }
306
+ this.host.setStatus("deleting session");
307
+ this.host.render();
308
+ const sidecarRemoved = await this.removeDcpSidecarState(dirname(targetSessionFile), targetSessionId).catch(() => false);
309
+ await rm(targetSessionFile, { force: true }).catch(() => undefined);
310
+ const deleteCurrent = isCurrent || targetSessionFile === currentSessionFile;
311
+ if (deleteCurrent) {
312
+ await this.host.awaitCurrentSessionExtensions(runtime);
313
+ const result = await runtime.newSession();
314
+ if (result.cancelled) {
315
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete succeeded, but new session was cancelled." });
316
+ this.host.setSessionStatus(runtime.session);
317
+ return;
318
+ }
319
+ this.host.resetSessionView();
320
+ this.host.loadSessionHistory();
321
+ this.host.addEntry({
322
+ id: createId("system"),
323
+ kind: "system",
324
+ text: `Deleted session ${targetSessionId}. ${sidecarRemoved ? "Sidecar DCP state removed. " : ""}Started a new session. cwd=${runtime.cwd}`,
325
+ });
326
+ if (runtime.modelFallbackMessage)
327
+ this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
328
+ this.host.setSessionStatus(runtime.session);
329
+ }
330
+ else {
331
+ this.host.addEntry({
332
+ id: createId("system"),
333
+ kind: "system",
334
+ text: `Deleted session file ${targetSessionFile}${sidecarRemoved ? " and its sidecar DCP state" : ""}.`,
335
+ });
336
+ this.host.setSessionStatus(runtime.session);
337
+ }
338
+ this.host.toast.success("Session deleted");
339
+ }
340
+ async removeDcpSidecarState(sessionDir, sessionId) {
341
+ if (!sessionId)
342
+ return false;
343
+ const safeName = `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
344
+ const statePath = join(sessionDir, "dcp-state", safeName);
345
+ await rm(statePath, { force: true });
346
+ return true;
347
+ }
274
348
  async runNewSessionCommand() {
275
349
  const runtime = getIdleRuntime(this.host, "new");
276
350
  if (!runtime)
277
351
  return;
278
352
  this.host.setStatus("starting new session");
279
353
  this.host.render();
354
+ await this.host.awaitCurrentSessionExtensions(runtime);
280
355
  const result = await runtime.newSession();
281
356
  if (result.cancelled) {
282
357
  this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
@@ -334,3 +409,8 @@ function splitUpdateArguments(argumentsText) {
334
409
  const trimmed = argumentsText.trim();
335
410
  return trimmed ? trimmed.split(/\s+/u) : [];
336
411
  }
412
+ function parseSessionIdFromFileName(sessionFile) {
413
+ const base = basename(sessionFile).replace(/\.jsonl$/iu, "");
414
+ const separator = base.indexOf("_");
415
+ return separator >= 0 ? base.slice(separator + 1) : undefined;
416
+ }
@@ -1,9 +1,11 @@
1
1
  import type { AgentSessionRuntime, ExtensionCommandContextActions, ExtensionError } from "@earendil-works/pi-coding-agent";
2
+ import { type PixLogDetails, type PixLogLevel } from "../logger.js";
2
3
  import type { Entry } from "../types.js";
3
4
  export type AppExtensionActionsHost = {
4
5
  isRunning(): boolean;
5
6
  getInput(): string;
6
7
  setInput(value: string): void;
8
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
7
9
  resetSessionView(): void;
8
10
  loadSessionHistory(): void;
9
11
  afterSessionReplacement(message?: string): void;
@@ -13,9 +15,11 @@ export type AppExtensionActionsHost = {
13
15
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
14
16
  render(): void;
15
17
  };
18
+ export type ExtensionErrorLogger = (level: PixLogLevel, event: string, details?: PixLogDetails) => void;
16
19
  export declare class AppExtensionActionsController {
17
20
  private readonly host;
18
- constructor(host: AppExtensionActionsHost);
21
+ private readonly logExtensionError;
22
+ constructor(host: AppExtensionActionsHost, logExtensionError?: ExtensionErrorLogger);
19
23
  createCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
20
24
  waitForSessionIdle(runtime: AgentSessionRuntime): Promise<void>;
21
25
  handleExtensionError(error: ExtensionError): void;
@@ -1,19 +1,25 @@
1
+ import { basename } from "node:path";
1
2
  import { createId } from "../id.js";
3
+ import { logPixEvent } from "../logger.js";
2
4
  export class AppExtensionActionsController {
3
5
  host;
4
- constructor(host) {
6
+ logExtensionError;
7
+ constructor(host, logExtensionError = logPixEvent) {
5
8
  this.host = host;
9
+ this.logExtensionError = logExtensionError;
6
10
  }
7
11
  createCommandContextActions(runtime) {
8
12
  return {
9
13
  waitForIdle: () => this.waitForSessionIdle(runtime),
10
14
  newSession: async (options) => {
15
+ await this.host.awaitCurrentSessionExtensions(runtime);
11
16
  const result = await runtime.newSession(options);
12
17
  if (!result.cancelled)
13
18
  this.host.afterSessionReplacement("Started a new session.");
14
19
  return result;
15
20
  },
16
21
  fork: async (entryId, options) => {
22
+ await this.host.awaitCurrentSessionExtensions(runtime);
17
23
  const result = await runtime.fork(entryId, options);
18
24
  if (!result.cancelled)
19
25
  this.host.afterSessionReplacement("Forked to a new session.");
@@ -32,12 +38,14 @@ export class AppExtensionActionsController {
32
38
  return result;
33
39
  },
34
40
  switchSession: async (sessionPath, options) => {
41
+ await this.host.awaitCurrentSessionExtensions(runtime);
35
42
  const result = await runtime.switchSession(sessionPath, options);
36
43
  if (!result.cancelled)
37
44
  this.host.afterSessionReplacement(`Switched session: ${sessionPath}`);
38
45
  return result;
39
46
  },
40
47
  reload: async () => {
48
+ await this.host.awaitCurrentSessionExtensions(runtime);
41
49
  await runtime.session.reload();
42
50
  this.host.setSessionStatus(runtime.session);
43
51
  this.host.showToast("Reloaded resources", "success");
@@ -51,10 +59,35 @@ export class AppExtensionActionsController {
51
59
  }
52
60
  }
53
61
  handleExtensionError(error) {
62
+ const sourceText = formatExtensionErrorSource(error.extensionPath);
54
63
  const pathText = error.extensionPath ? ` (${error.extensionPath})` : "";
55
- this.host.addEntry({ id: createId("error"), kind: "error", text: `Extension ${error.event} failed${pathText}: ${error.error}` });
64
+ this.logExtensionError("error", "extension:error", extensionErrorLogDetails(error));
65
+ this.host.addEntry({
66
+ id: createId("error"),
67
+ kind: "error",
68
+ text: `Extension ${error.event} failed${sourceText}${pathText}: ${error.error}`,
69
+ });
56
70
  this.host.showToast(`Extension ${error.event} failed`, "error");
57
71
  if (this.host.isRunning())
58
72
  this.host.render();
59
73
  }
60
74
  }
75
+ function formatExtensionErrorSource(extensionPath) {
76
+ if (!extensionPath)
77
+ return "";
78
+ const extensionName = basename(extensionPath);
79
+ return extensionName && extensionName !== extensionPath ? ` [${extensionName}]` : "";
80
+ }
81
+ function extensionErrorLogDetails(error) {
82
+ const details = {
83
+ event: error.event,
84
+ error: error.error,
85
+ };
86
+ if (error.extensionPath) {
87
+ details.extensionPath = error.extensionPath;
88
+ details.extensionName = basename(error.extensionPath);
89
+ }
90
+ if (error.stack)
91
+ details.stack = error.stack;
92
+ return details;
93
+ }
@@ -46,6 +46,8 @@ export declare class AppInputController {
46
46
  private consumeCommandArrowPageMatch;
47
47
  private consumeTerminalEditShortcutSequence;
48
48
  private consumeIgnoredModifiedKeySequence;
49
+ private consumeModifiedArrowKeySequence;
50
+ private consumeEscapeKeySequence;
49
51
  private consumeClipboardImagePasteSequence;
50
52
  private consumeShiftEnterSequence;
51
53
  private consumeTerminalInterruptSequence;
@@ -1,6 +1,6 @@
1
1
  import { InputPasteHandler } from "./input-paste-handler.js";
2
2
  import { hasTerminalCommandModifier, isNativeCommandPressed, isNativeShiftPressed } from "./native-modifiers.js";
3
- import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
3
+ import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalKeyArrowDirection, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsEscape, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
4
4
  const SHIFT_ENTER_ESCAPE_SEQUENCES = ["\x1b\r", "\x1b\n"];
5
5
  export class AppInputController {
6
6
  host;
@@ -112,6 +112,16 @@ export class AppInputController {
112
112
  continue;
113
113
  if (terminalEditShortcutSequence === "pending")
114
114
  return;
115
+ const modifiedArrowKeySequence = this.consumeModifiedArrowKeySequence();
116
+ if (modifiedArrowKeySequence === "consumed")
117
+ continue;
118
+ if (modifiedArrowKeySequence === "pending")
119
+ return;
120
+ const escapeKeySequence = this.consumeEscapeKeySequence();
121
+ if (escapeKeySequence === "consumed")
122
+ continue;
123
+ if (escapeKeySequence === "pending")
124
+ return;
115
125
  const ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
116
126
  if (ignoredModifiedKeySequence === "consumed")
117
127
  continue;
@@ -225,7 +235,10 @@ export class AppInputController {
225
235
  }
226
236
  if (/^\x1b\[(?:8|127);\d*$/.test(this.inputBuffer))
227
237
  return "pending";
228
- if (this.inputBuffer.startsWith("\x1b[27;") && !this.inputBuffer.includes("~"))
238
+ // modifyOtherKeys backspace looks like \x1b[27;<mod>;(8|127)~ (two semicolons, no colons).
239
+ // Kitty key sequences also begin with \x1b[27; (e.g. the ESC release \x1b[27;1:3u), so only
240
+ // treat the buffer as a pending modifyOtherKeys partial when it matches that exact shape.
241
+ if (/^\x1b\[27;\d+;\d*$/.test(this.inputBuffer))
229
242
  return "pending";
230
243
  return "none";
231
244
  }
@@ -277,6 +290,41 @@ export class AppInputController {
277
290
  this.inputBuffer = this.inputBuffer.slice(result.key.length);
278
291
  return "consumed";
279
292
  }
293
+ consumeModifiedArrowKeySequence() {
294
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
295
+ if (result.kind === "pending")
296
+ return "pending";
297
+ if (result.kind === "none")
298
+ return "none";
299
+ const direction = terminalKeyArrowDirection(result.key);
300
+ if (!direction)
301
+ return "none";
302
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
303
+ if (terminalKeyShouldIgnore(result.key))
304
+ return "consumed";
305
+ if (direction === "up")
306
+ this.handleArrowUp();
307
+ else if (direction === "down")
308
+ this.handleArrowDown();
309
+ else if (direction === "right")
310
+ this.handleArrowRight();
311
+ else
312
+ this.handleArrowLeft();
313
+ return "consumed";
314
+ }
315
+ consumeEscapeKeySequence() {
316
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
317
+ if (result.kind === "pending")
318
+ return "pending";
319
+ if (result.kind === "none")
320
+ return "none";
321
+ if (!terminalKeyIsEscape(result.key))
322
+ return "none";
323
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
324
+ if (!terminalKeyShouldIgnore(result.key))
325
+ void this.host.handleEscape();
326
+ return "consumed";
327
+ }
280
328
  consumeClipboardImagePasteSequence() {
281
329
  const result = parseTerminalModifiedKeySequence(this.inputBuffer);
282
330
  if (result.kind === "pending")
@@ -39,5 +39,7 @@ export declare function parseTerminalInterruptSequence(input: string): {
39
39
  export declare function terminalKeyIsShiftEnter(key: ParsedModifiedKey): boolean;
40
40
  export declare function terminalKeyIsClipboardImagePaste(key: ParsedModifiedKey): boolean;
41
41
  export declare function terminalKeyShouldIgnore(key: ParsedModifiedKey): boolean;
42
+ export declare function terminalKeyIsEscape(key: ParsedModifiedKey): boolean;
43
+ export declare function terminalKeyArrowDirection(key: ParsedModifiedKey): "up" | "down" | "right" | "left" | undefined;
42
44
  export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
43
45
  export {};