pi-ui-extend 0.1.36 → 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 (70) hide show
  1. package/dist/app/app.d.ts +7 -0
  2. package/dist/app/app.js +40 -5
  3. package/dist/app/commands/command-controller.js +1 -0
  4. package/dist/app/commands/command-registry.d.ts +1 -0
  5. package/dist/app/commands/command-registry.js +8 -0
  6. package/dist/app/commands/command-session-actions.d.ts +2 -0
  7. package/dist/app/commands/command-session-actions.js +79 -1
  8. package/dist/app/extensions/extension-actions-controller.d.ts +4 -1
  9. package/dist/app/extensions/extension-actions-controller.js +31 -2
  10. package/dist/app/input/input-controller.d.ts +1 -0
  11. package/dist/app/input/input-controller.js +23 -2
  12. package/dist/app/input/terminal-edit-shortcuts.d.ts +1 -0
  13. package/dist/app/input/terminal-edit-shortcuts.js +7 -0
  14. package/dist/app/input/voice-controller.js +1 -1
  15. package/dist/app/popup/popup-action-controller.d.ts +1 -3
  16. package/dist/app/popup/popup-action-controller.js +1 -5
  17. package/dist/app/rendering/message-content.js +4 -3
  18. package/dist/app/rendering/render-controller.js +21 -38
  19. package/dist/app/rendering/status-line-renderer.d.ts +1 -0
  20. package/dist/app/rendering/status-line-renderer.js +14 -2
  21. package/dist/app/runtime.js +12 -2
  22. package/dist/app/screen/mouse-controller.js +2 -0
  23. package/dist/app/session/session-event-controller.d.ts +7 -0
  24. package/dist/app/session/session-event-controller.js +10 -13
  25. package/dist/app/terminal/terminal-controller.js +1 -0
  26. package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
  27. package/dist/app/terminal/terminal-output-buffer.js +24 -16
  28. package/dist/bundled-extensions/terminal-bell/index.js +118 -33
  29. package/dist/markdown-format.d.ts +1 -0
  30. package/dist/markdown-format.js +30 -16
  31. package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
  32. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  33. package/dist/tool-renderers/apply-patch.js +6 -1
  34. package/dist/tool-renderers/patch-normalize.d.ts +24 -0
  35. package/dist/tool-renderers/patch-normalize.js +163 -0
  36. package/external/pi-tools-suite/README.md +3 -2
  37. package/external/pi-tools-suite/package.json +3 -3
  38. package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
  39. package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
  40. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
  41. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
  42. package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
  43. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
  44. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
  45. package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
  46. package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
  47. package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
  48. package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
  49. package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
  50. package/external/pi-tools-suite/src/dcp/config.ts +10 -6
  51. package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
  52. package/external/pi-tools-suite/src/dcp/index.ts +204 -27
  53. package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
  54. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
  55. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
  56. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
  57. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
  58. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  59. package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
  60. package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
  61. package/external/pi-tools-suite/src/dcp/state.ts +62 -4
  62. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
  63. package/external/pi-tools-suite/src/index.ts +1 -0
  64. package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
  65. package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
  66. package/external/pi-tools-suite/src/todo/index.ts +24 -0
  67. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
  68. package/external/pi-tools-suite/src/usage/index.ts +18 -4
  69. package/package.json +4 -4
  70. 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;
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,7 +173,18 @@ 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,
@@ -373,6 +391,16 @@ export class PiUiExtendApp {
373
391
  setSessionActivity: (activity) => this.setSessionActivity(activity),
374
392
  updateQueuedMessageStatus: () => this.queuedMessages.updateQueuedMessageStatus(),
375
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
+ },
376
404
  prepareWorkspaceMutation: (toolName, args) => this.workspaceActions.prepareWorkspaceMutation(toolName, args),
377
405
  workspaceMutationFromToolExecution: (input) => this.workspaceActions.workspaceMutationFromToolExecution(input),
378
406
  recordWorkspaceMutationForUserEntry: (entryId, mutation) => this.workspaceActions.recordWorkspaceMutationForUserEntry(entryId, mutation),
@@ -522,10 +550,8 @@ export class PiUiExtendApp {
522
550
  setSessionStatus: (session) => this.setSessionStatus(session),
523
551
  showToast: (message, kind) => this.showToast(message, kind),
524
552
  render: () => this.render(),
525
- resetSessionView: () => this.resetSessionView(),
526
553
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
527
- bindCurrentSession: () => this.bindCurrentSession(),
528
- loadSessionHistory: () => this.loadSessionHistory(),
554
+ afterSessionReplacement: (message) => this.afterSessionReplacement(message),
529
555
  scrollToConversationEntry: (entryId) => this.scrollController.scrollToConversationEntry(entryId),
530
556
  scrollToUserMessageJumpTarget: (target) => this.scrollToUserMessageJumpTarget(target),
531
557
  }, this.popupMenus, this.commandController, this.menuItems, this.queuedMessages, this.workspaceActions);
@@ -768,10 +794,16 @@ export class PiUiExtendApp {
768
794
  this.slashCommands = this.commandController.slashCommands;
769
795
  }
770
796
  createRuntime(options, runtimeOptions = {}) {
797
+ const eventBus = this.createExtensionEventBus();
771
798
  return createPixRuntime(options, {
772
- eventBus: this.createExtensionEventBus(),
799
+ eventBus,
773
800
  config: this.pixConfig,
774
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;
775
807
  });
776
808
  }
777
809
  async loadStartupConfig() {
@@ -837,6 +869,9 @@ export class PiUiExtendApp {
837
869
  runtime.setRebindSession(async () => {
838
870
  await this.bindCurrentSession({ awaitExtensions: false });
839
871
  });
872
+ runtime.setBeforeSessionInvalidate(() => {
873
+ this.extensionUiController.clearWidgets(this.activeExtensionUiScope());
874
+ });
840
875
  await this.bindCurrentSession(options);
841
876
  }
842
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(),
@@ -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";
@@ -272,6 +272,79 @@ export class SessionCommandActions {
272
272
  this.host.toast.error("Reload failed");
273
273
  }
274
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
+ }
275
348
  async runNewSessionCommand() {
276
349
  const runtime = getIdleRuntime(this.host, "new");
277
350
  if (!runtime)
@@ -336,3 +409,8 @@ function splitUpdateArguments(argumentsText) {
336
409
  const trimmed = argumentsText.trim();
337
410
  return trimmed ? trimmed.split(/\s+/u) : [];
338
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,4 +1,5 @@
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;
@@ -14,9 +15,11 @@ export type AppExtensionActionsHost = {
14
15
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
15
16
  render(): void;
16
17
  };
18
+ export type ExtensionErrorLogger = (level: PixLogLevel, event: string, details?: PixLogDetails) => void;
17
19
  export declare class AppExtensionActionsController {
18
20
  private readonly host;
19
- constructor(host: AppExtensionActionsHost);
21
+ private readonly logExtensionError;
22
+ constructor(host: AppExtensionActionsHost, logExtensionError?: ExtensionErrorLogger);
20
23
  createCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
21
24
  waitForSessionIdle(runtime: AgentSessionRuntime): Promise<void>;
22
25
  handleExtensionError(error: ExtensionError): void;
@@ -1,8 +1,12 @@
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 {
@@ -55,10 +59,35 @@ export class AppExtensionActionsController {
55
59
  }
56
60
  }
57
61
  handleExtensionError(error) {
62
+ const sourceText = formatExtensionErrorSource(error.extensionPath);
58
63
  const pathText = error.extensionPath ? ` (${error.extensionPath})` : "";
59
- 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
+ });
60
70
  this.host.showToast(`Extension ${error.event} failed`, "error");
61
71
  if (this.host.isRunning())
62
72
  this.host.render();
63
73
  }
64
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
+ }
@@ -47,6 +47,7 @@ export declare class AppInputController {
47
47
  private consumeTerminalEditShortcutSequence;
48
48
  private consumeIgnoredModifiedKeySequence;
49
49
  private consumeModifiedArrowKeySequence;
50
+ private consumeEscapeKeySequence;
50
51
  private consumeClipboardImagePasteSequence;
51
52
  private consumeShiftEnterSequence;
52
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, terminalKeyArrowDirection, 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;
@@ -117,6 +117,11 @@ export class AppInputController {
117
117
  continue;
118
118
  if (modifiedArrowKeySequence === "pending")
119
119
  return;
120
+ const escapeKeySequence = this.consumeEscapeKeySequence();
121
+ if (escapeKeySequence === "consumed")
122
+ continue;
123
+ if (escapeKeySequence === "pending")
124
+ return;
120
125
  const ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
121
126
  if (ignoredModifiedKeySequence === "consumed")
122
127
  continue;
@@ -230,7 +235,10 @@ export class AppInputController {
230
235
  }
231
236
  if (/^\x1b\[(?:8|127);\d*$/.test(this.inputBuffer))
232
237
  return "pending";
233
- 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))
234
242
  return "pending";
235
243
  return "none";
236
244
  }
@@ -304,6 +312,19 @@ export class AppInputController {
304
312
  this.handleArrowLeft();
305
313
  return "consumed";
306
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
+ }
307
328
  consumeClipboardImagePasteSequence() {
308
329
  const result = parseTerminalModifiedKeySequence(this.inputBuffer);
309
330
  if (result.kind === "pending")
@@ -39,6 +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;
42
43
  export declare function terminalKeyArrowDirection(key: ParsedModifiedKey): "up" | "down" | "right" | "left" | undefined;
43
44
  export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
44
45
  export {};
@@ -4,6 +4,7 @@ const COMMAND_MODIFIER_FLAG = 8;
4
4
  const LOCK_MODIFIER_MASK = 64 + 128;
5
5
  const KEY_CODE_C = 99;
6
6
  const KEY_CODE_ENTER = 13;
7
+ const KEY_CODE_ESCAPE = 27;
7
8
  const KEY_CODE_V = 118;
8
9
  const KEY_CODE_Y = 121;
9
10
  const KEY_CODE_Z = 122;
@@ -67,6 +68,12 @@ export function terminalKeyIsClipboardImagePaste(key) {
67
68
  export function terminalKeyShouldIgnore(key) {
68
69
  return key.eventType === 3;
69
70
  }
71
+ export function terminalKeyIsEscape(key) {
72
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
73
+ if (effectiveModifier !== 0)
74
+ return false;
75
+ return key.codepoint === KEY_CODE_ESCAPE;
76
+ }
70
77
  export function terminalKeyArrowDirection(key) {
71
78
  const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
72
79
  if (effectiveModifier !== 0)
@@ -66,7 +66,7 @@ export class AppVoiceController {
66
66
  case "installing":
67
67
  return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
68
68
  case "downloading":
69
- return `${APP_ICONS.down}${languageLabel}`;
69
+ return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
70
70
  case "loading":
71
71
  return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
72
72
  case "listening":
@@ -16,9 +16,7 @@ export type AppPopupActionControllerHost = {
16
16
  setSessionStatus(session: AgentSession | undefined): void;
17
17
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
18
18
  render(): void;
19
- resetSessionView(): void;
20
- bindCurrentSession(): Promise<void>;
21
- loadSessionHistory(): void;
19
+ afterSessionReplacement(message?: string): void;
22
20
  scrollToConversationEntry(entryId: string): boolean;
23
21
  scrollToUserMessageJumpTarget(target: UserMessageJumpMenuValue): Promise<boolean>;
24
22
  };
@@ -236,12 +236,8 @@ export class AppPopupActionController {
236
236
  this.host.render();
237
237
  return true;
238
238
  }
239
- this.host.resetSessionView();
240
- await this.host.bindCurrentSession();
241
- this.host.loadSessionHistory();
242
239
  const name = runtime.session.sessionName ?? session.id.slice(0, 8);
243
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Resumed session "${name}"` });
244
- this.host.setSessionStatus(runtime.session);
240
+ this.host.afterSessionReplacement(`Resumed session "${name}"`);
245
241
  }
246
242
  catch (error) {
247
243
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Resume failed: ${error instanceof Error ? error.message : String(error)}` });
@@ -1,3 +1,4 @@
1
+ import { stripDcpControlMetadata } from "../../markdown-format.js";
1
2
  import { isRecord } from "../guards.js";
2
3
  const MAX_FORMAT_STRING_CHARS = 256 * 1024;
3
4
  const MAX_RENDERED_CONTENT_CHARS = 512 * 1024;
@@ -79,11 +80,11 @@ export function renderContent(content) {
79
80
  if (!pushPart(stringifyUnknown(item)))
80
81
  break;
81
82
  }
82
- return parts.join("\n");
83
+ return stripDcpControlMetadata(parts.join("\n"));
83
84
  }
84
85
  export function renderUserMessageContent(content) {
85
86
  if (typeof content === "string")
86
- return content;
87
+ return stripDcpControlMetadata(content);
87
88
  if (!Array.isArray(content))
88
89
  return stringifyUnknown(content);
89
90
  const textParts = [];
@@ -103,7 +104,7 @@ export function renderUserMessageContent(content) {
103
104
  }
104
105
  textParts.push(stringifyUnknown(item));
105
106
  }
106
- const text = textParts.join("\n").replace(/\[Image \d+(?:: [^\]]+)?\]/g, "").trimEnd();
107
+ const text = stripDcpControlMetadata(textParts.join("\n")).replace(/\[Image \d+(?:: [^\]]+)?\]/g, "").trimEnd();
107
108
  if (imageCount === 0)
108
109
  return text;
109
110
  const imageText = userImageLabels(imageCount);