@oh-my-pi/pi-coding-agent 13.14.2 → 13.15.3

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 (85) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/package.json +10 -8
  3. package/src/autoresearch/command-initialize.md +34 -0
  4. package/src/autoresearch/command-resume.md +17 -0
  5. package/src/autoresearch/contract.ts +332 -0
  6. package/src/autoresearch/dashboard.ts +447 -0
  7. package/src/autoresearch/git.ts +243 -0
  8. package/src/autoresearch/helpers.ts +458 -0
  9. package/src/autoresearch/index.ts +693 -0
  10. package/src/autoresearch/prompt.md +227 -0
  11. package/src/autoresearch/resume-message.md +16 -0
  12. package/src/autoresearch/state.ts +386 -0
  13. package/src/autoresearch/tools/init-experiment.ts +310 -0
  14. package/src/autoresearch/tools/log-experiment.ts +833 -0
  15. package/src/autoresearch/tools/run-experiment.ts +640 -0
  16. package/src/autoresearch/types.ts +218 -0
  17. package/src/cli/args.ts +8 -2
  18. package/src/cli/initial-message.ts +58 -0
  19. package/src/config/keybindings.ts +423 -212
  20. package/src/config/model-registry.ts +1 -0
  21. package/src/config/model-resolver.ts +57 -9
  22. package/src/config/settings-schema.ts +38 -10
  23. package/src/config/settings.ts +1 -4
  24. package/src/export/html/template.css +43 -13
  25. package/src/export/html/template.generated.ts +1 -1
  26. package/src/export/html/template.html +1 -0
  27. package/src/export/html/template.js +107 -0
  28. package/src/extensibility/extensions/types.ts +31 -8
  29. package/src/internal-urls/docs-index.generated.ts +1 -1
  30. package/src/lsp/index.ts +1 -1
  31. package/src/main.ts +44 -44
  32. package/src/mcp/oauth-discovery.ts +1 -1
  33. package/src/modes/acp/acp-agent.ts +957 -0
  34. package/src/modes/acp/acp-event-mapper.ts +531 -0
  35. package/src/modes/acp/acp-mode.ts +13 -0
  36. package/src/modes/acp/index.ts +2 -0
  37. package/src/modes/components/agent-dashboard.ts +5 -4
  38. package/src/modes/components/custom-editor.ts +53 -51
  39. package/src/modes/components/extensions/extension-dashboard.ts +2 -1
  40. package/src/modes/components/history-search.ts +2 -1
  41. package/src/modes/components/hook-editor.ts +2 -1
  42. package/src/modes/components/hook-input.ts +8 -7
  43. package/src/modes/components/hook-selector.ts +15 -10
  44. package/src/modes/components/keybinding-hints.ts +9 -9
  45. package/src/modes/components/login-dialog.ts +3 -3
  46. package/src/modes/components/mcp-add-wizard.ts +2 -1
  47. package/src/modes/components/model-selector.ts +14 -3
  48. package/src/modes/components/oauth-selector.ts +2 -1
  49. package/src/modes/components/session-selector.ts +2 -1
  50. package/src/modes/components/settings-selector.ts +2 -1
  51. package/src/modes/components/status-line-segment-editor.ts +2 -1
  52. package/src/modes/components/tree-selector.ts +3 -2
  53. package/src/modes/components/user-message-selector.ts +3 -8
  54. package/src/modes/components/user-message.ts +16 -0
  55. package/src/modes/controllers/extension-ui-controller.ts +89 -4
  56. package/src/modes/controllers/input-controller.ts +48 -29
  57. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  58. package/src/modes/index.ts +1 -0
  59. package/src/modes/interactive-mode.ts +17 -5
  60. package/src/modes/print-mode.ts +1 -1
  61. package/src/modes/prompt-action-autocomplete.ts +7 -7
  62. package/src/modes/rpc/rpc-mode.ts +7 -2
  63. package/src/modes/rpc/rpc-types.ts +1 -0
  64. package/src/modes/theme/theme.ts +53 -44
  65. package/src/modes/types.ts +9 -2
  66. package/src/modes/utils/hotkeys-markdown.ts +20 -20
  67. package/src/modes/utils/keybinding-matchers.ts +21 -0
  68. package/src/modes/utils/ui-helpers.ts +1 -1
  69. package/src/patch/hashline.ts +139 -127
  70. package/src/patch/index.ts +77 -59
  71. package/src/patch/shared.ts +19 -11
  72. package/src/prompts/tools/hashline.md +43 -116
  73. package/src/sdk.ts +34 -17
  74. package/src/session/agent-session.ts +436 -86
  75. package/src/session/messages.ts +23 -0
  76. package/src/session/session-manager.ts +97 -31
  77. package/src/tools/ask.ts +56 -30
  78. package/src/tools/bash-interceptor.ts +1 -39
  79. package/src/tools/bash-skill-urls.ts +1 -1
  80. package/src/tools/browser.ts +1 -1
  81. package/src/tools/gemini-image.ts +1 -1
  82. package/src/tools/resolve.ts +1 -1
  83. package/src/utils/child-process.ts +88 -0
  84. package/src/utils/image-input.ts +11 -1
  85. package/src/web/search/providers/codex.ts +10 -3
@@ -1,6 +1,11 @@
1
1
  import { Container, Markdown, Spacer } from "@oh-my-pi/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
3
 
4
+ // OSC 133 shell integration: marks prompt zones for terminal multiplexers
5
+ const OSC133_ZONE_START = "\x1b]133;A\x07";
6
+ const OSC133_ZONE_END = "\x1b]133;B\x07";
7
+ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
8
+
4
9
  /**
5
10
  * Component that renders a user message
6
11
  */
@@ -19,4 +24,15 @@ export class UserMessageComponent extends Container {
19
24
  }),
20
25
  );
21
26
  }
27
+
28
+ override render(width: number): string[] {
29
+ const lines = super.render(width);
30
+ if (lines.length === 0) {
31
+ return lines;
32
+ }
33
+
34
+ lines[0] = OSC133_ZONE_START + lines[0];
35
+ lines[lines.length - 1] = lines[lines.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
36
+ return lines;
37
+ }
22
38
  }
@@ -1,5 +1,5 @@
1
1
  import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
2
- import { Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
4
  import { KeybindingsManager } from "../../config/keybindings";
5
5
  import type {
@@ -9,6 +9,9 @@ import type {
9
9
  ExtensionError,
10
10
  ExtensionUIContext,
11
11
  ExtensionUIDialogOptions,
12
+ ExtensionUiComponent,
13
+ ExtensionWidgetContent,
14
+ ExtensionWidgetOptions,
12
15
  TerminalInputHandler,
13
16
  } from "../../extensibility/extensions";
14
17
  import { HookEditorComponent } from "../../modes/components/hook-editor";
@@ -18,8 +21,12 @@ import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, them
18
21
  import type { InteractiveModeContext } from "../../modes/types";
19
22
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
20
23
 
24
+ const MAX_WIDGET_LINES = 10;
25
+
21
26
  export class ExtensionUiController {
22
27
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
28
+ #hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
29
+ #hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
23
30
  constructor(private ctx: InteractiveModeContext) {}
24
31
 
25
32
  /**
@@ -35,7 +42,7 @@ export class ExtensionUiController {
35
42
  onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
36
43
  setStatus: (key, text) => this.setHookStatus(key, text),
37
44
  setWorkingMessage: message => this.ctx.setWorkingMessage(message),
38
- setWidget: (key, content) => this.setHookWidget(key, content),
45
+ setWidget: (key, content, options) => this.setHookWidget(key, content, options),
39
46
  setTitle: title => setTerminalTitle(title),
40
47
  custom: (factory, options) => this.showHookCustom(factory, options),
41
48
  setEditorText: text => this.ctx.editor.setText(text),
@@ -150,6 +157,7 @@ export class ExtensionUiController {
150
157
 
151
158
  // Create new session
152
159
  this.clearExtensionTerminalInputListeners();
160
+ this.clearHookWidgets();
153
161
  const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
154
162
  if (!success) {
155
163
  return { cancelled: true };
@@ -227,6 +235,7 @@ export class ExtensionUiController {
227
235
  await this.ctx.executeCompaction(instructionsOrOptions, false);
228
236
  },
229
237
  switchSession: async sessionPath => {
238
+ this.clearHookWidgets();
230
239
  const result = await this.ctx.session.switchSession(sessionPath);
231
240
  if (!result) {
232
241
  return { cancelled: true };
@@ -252,11 +261,73 @@ export class ExtensionUiController {
252
261
  });
253
262
  }
254
263
 
255
- setHookWidget(key: string, content: unknown): void {
256
- this.ctx.statusLine.setHookStatus(key, String(content));
264
+ setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
265
+ const placement = options?.placement ?? "aboveEditor";
266
+ this.#removeHookWidget(this.#hookWidgetsAbove, key);
267
+ this.#removeHookWidget(this.#hookWidgetsBelow, key);
268
+
269
+ if (content === undefined) {
270
+ this.#rebuildHookWidgets();
271
+ return;
272
+ }
273
+
274
+ const target = placement === "belowEditor" ? this.#hookWidgetsBelow : this.#hookWidgetsAbove;
275
+ target.set(key, this.#createHookWidget(content));
276
+ this.#rebuildHookWidgets();
277
+ }
278
+
279
+ #removeHookWidget(widgets: Map<string, ExtensionUiComponent>, key: string): void {
280
+ const existing = widgets.get(key);
281
+ existing?.dispose?.();
282
+ widgets.delete(key);
283
+ }
284
+
285
+ #createHookWidget(content: ExtensionWidgetContent): ExtensionUiComponent {
286
+ if (Array.isArray(content)) {
287
+ const container = new Container();
288
+ for (const line of content.slice(0, MAX_WIDGET_LINES)) {
289
+ container.addChild(new Text(line, 1, 0));
290
+ }
291
+ if (content.length > MAX_WIDGET_LINES) {
292
+ container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
293
+ }
294
+ return container;
295
+ }
296
+ if (content === undefined) {
297
+ throw new Error("Widget content missing");
298
+ }
299
+ return content(this.ctx.ui, theme);
300
+ }
301
+
302
+ #rebuildHookWidgets(): void {
303
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true, true);
304
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false, false);
257
305
  this.ctx.ui.requestRender();
258
306
  }
259
307
 
308
+ #renderHookWidgetContainer(
309
+ container: Container,
310
+ widgets: Map<string, ExtensionUiComponent>,
311
+ spacerWhenEmpty: boolean,
312
+ leadingSpacer: boolean,
313
+ ): void {
314
+ container.clear();
315
+
316
+ if (widgets.size === 0) {
317
+ if (spacerWhenEmpty) {
318
+ container.addChild(new Spacer(1));
319
+ }
320
+ return;
321
+ }
322
+
323
+ if (leadingSpacer) {
324
+ container.addChild(new Spacer(1));
325
+ }
326
+ for (const widget of widgets.values()) {
327
+ container.addChild(widget);
328
+ }
329
+ }
330
+
260
331
  initializeHookRunner(uiContext: ExtensionUIContext, _hasUI: boolean): void {
261
332
  const extensionRunner = this.ctx.session.extensionRunner;
262
333
  if (!extensionRunner) {
@@ -353,6 +424,7 @@ export class ExtensionUiController {
353
424
 
354
425
  // Create new session
355
426
  this.clearExtensionTerminalInputListeners();
427
+ this.clearHookWidgets();
356
428
  const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
357
429
  if (!success) {
358
430
  return { cancelled: true };
@@ -432,6 +504,7 @@ export class ExtensionUiController {
432
504
  if (this.ctx.isBackgrounded) {
433
505
  return { cancelled: true };
434
506
  }
507
+ this.clearHookWidgets();
435
508
  const result = await this.ctx.session.switchSession(sessionPath);
436
509
  if (!result) {
437
510
  return { cancelled: true };
@@ -828,6 +901,18 @@ export class ExtensionUiController {
828
901
  };
829
902
  }
830
903
 
904
+ clearHookWidgets(): void {
905
+ for (const widget of this.#hookWidgetsAbove.values()) {
906
+ widget.dispose?.();
907
+ }
908
+ for (const widget of this.#hookWidgetsBelow.values()) {
909
+ widget.dispose?.();
910
+ }
911
+ this.#hookWidgetsAbove.clear();
912
+ this.#hookWidgetsBelow.clear();
913
+ this.#rebuildHookWidgets();
914
+ }
915
+
831
916
  clearExtensionTerminalInputListeners(): void {
832
917
  for (const unsubscribe of this.#extensionTerminalInputUnsubscribers) {
833
918
  unsubscribe();
@@ -11,6 +11,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
11
11
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
12
12
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
13
13
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
14
+ import { ensureSupportedImageInput } from "../../utils/image-input";
14
15
  import { resizeImage } from "../../utils/image-resize";
15
16
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
16
17
 
@@ -26,7 +27,7 @@ export class InputController {
26
27
  constructor(private ctx: InteractiveModeContext) {}
27
28
 
28
29
  setupKeyHandlers(): void {
29
- this.ctx.editor.setActionKeys("interrupt", this.ctx.keybindings.getKeys("interrupt"));
30
+ this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
30
31
  this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
31
32
  Boolean(
32
33
  this.ctx.loadingAnimation ||
@@ -83,68 +84,78 @@ export class InputController {
83
84
  }
84
85
  };
85
86
 
86
- this.ctx.editor.setActionKeys("clear", this.ctx.keybindings.getKeys("clear"));
87
+ this.ctx.editor.setActionKeys("app.clear", this.ctx.keybindings.getKeys("app.clear"));
87
88
  this.ctx.editor.onClear = () => this.handleCtrlC();
88
- this.ctx.editor.setActionKeys("exit", this.ctx.keybindings.getKeys("exit"));
89
+ this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
89
90
  this.ctx.editor.onExit = () => this.handleCtrlD();
90
- this.ctx.editor.setActionKeys("suspend", this.ctx.keybindings.getKeys("suspend"));
91
+ this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
91
92
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
92
- this.ctx.editor.setActionKeys("cycleThinkingLevel", this.ctx.keybindings.getKeys("cycleThinkingLevel"));
93
+ this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
93
94
  this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
94
- this.ctx.editor.setActionKeys("cycleModelForward", this.ctx.keybindings.getKeys("cycleModelForward"));
95
+ this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
95
96
  this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
96
- this.ctx.editor.setActionKeys("cycleModelBackward", this.ctx.keybindings.getKeys("cycleModelBackward"));
97
+ this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
97
98
  this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
98
- this.ctx.editor.onQuickSelectModel = () => this.ctx.showModelSelector({ temporaryOnly: true });
99
+ this.ctx.editor.setActionKeys(
100
+ "app.model.selectTemporary",
101
+ this.ctx.keybindings.getKeys("app.model.selectTemporary"),
102
+ );
103
+ this.ctx.editor.onSelectModelTemporary = () => this.ctx.showModelSelector({ temporaryOnly: true });
99
104
 
100
105
  // Global debug handler on TUI (works regardless of focus)
101
106
  this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
102
- this.ctx.editor.setActionKeys("selectModel", this.ctx.keybindings.getKeys("selectModel"));
107
+ this.ctx.editor.setActionKeys("app.model.select", this.ctx.keybindings.getKeys("app.model.select"));
103
108
  this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector();
104
- this.ctx.editor.setActionKeys("historySearch", this.ctx.keybindings.getKeys("historySearch"));
109
+ this.ctx.editor.setActionKeys("app.history.search", this.ctx.keybindings.getKeys("app.history.search"));
105
110
  this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch();
106
- this.ctx.editor.setActionKeys("toggleThinking", this.ctx.keybindings.getKeys("toggleThinking"));
111
+ this.ctx.editor.setActionKeys("app.thinking.toggle", this.ctx.keybindings.getKeys("app.thinking.toggle"));
107
112
  this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
108
- this.ctx.editor.setActionKeys("externalEditor", this.ctx.keybindings.getKeys("externalEditor"));
113
+ this.ctx.editor.setActionKeys("app.editor.external", this.ctx.keybindings.getKeys("app.editor.external"));
109
114
  this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
110
115
  this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
111
- this.ctx.editor.setActionKeys("pasteImage", this.ctx.keybindings.getKeys("pasteImage"));
116
+ this.ctx.editor.setActionKeys(
117
+ "app.clipboard.pasteImage",
118
+ this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
119
+ );
112
120
  this.ctx.editor.onPasteImage = () => this.handleImagePaste();
113
- this.ctx.editor.setActionKeys("copyPrompt", this.ctx.keybindings.getKeys("copyPrompt"));
121
+ this.ctx.editor.setActionKeys(
122
+ "app.clipboard.copyPrompt",
123
+ this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
124
+ );
114
125
  this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
115
- this.ctx.editor.setActionKeys("expandTools", this.ctx.keybindings.getKeys("expandTools"));
126
+ this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
116
127
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
117
- this.ctx.editor.setActionKeys("dequeue", this.ctx.keybindings.getKeys("dequeue"));
128
+ this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
118
129
  this.ctx.editor.onDequeue = () => this.handleDequeue();
119
130
 
120
131
  this.ctx.editor.clearCustomKeyHandlers();
121
132
  // Wire up extension shortcuts
122
133
  this.registerExtensionShortcuts();
123
134
 
124
- const planModeKeys = this.ctx.keybindings.getKeys("togglePlanMode");
135
+ const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
125
136
  for (const key of planModeKeys) {
126
137
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
127
138
  }
128
139
 
129
- for (const key of this.ctx.keybindings.getKeys("newSession")) {
140
+ for (const key of this.ctx.keybindings.getKeys("app.session.new")) {
130
141
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.handleClearCommand());
131
142
  }
132
- for (const key of this.ctx.keybindings.getKeys("tree")) {
143
+ for (const key of this.ctx.keybindings.getKeys("app.session.tree")) {
133
144
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showTreeSelector());
134
145
  }
135
- for (const key of this.ctx.keybindings.getKeys("fork")) {
146
+ for (const key of this.ctx.keybindings.getKeys("app.session.fork")) {
136
147
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showUserMessageSelector());
137
148
  }
138
- for (const key of this.ctx.keybindings.getKeys("resume")) {
149
+ for (const key of this.ctx.keybindings.getKeys("app.session.resume")) {
139
150
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionSelector());
140
151
  }
141
- for (const key of this.ctx.keybindings.getKeys("followUp")) {
152
+ for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
142
153
  this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
143
154
  }
144
- for (const key of this.ctx.keybindings.getKeys("toggleSTT")) {
155
+ for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
145
156
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
146
157
  }
147
- for (const key of this.ctx.keybindings.getKeys("copyLine")) {
158
+ for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
148
159
  this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
149
160
  }
150
161
 
@@ -492,17 +503,25 @@ export class InputController {
492
503
  const image = await readImageFromClipboard();
493
504
  if (image) {
494
505
  const base64Data = image.data.toBase64();
495
- let imageData = { data: base64Data, mimeType: image.mimeType };
506
+ let imageData = await ensureSupportedImageInput({
507
+ type: "image",
508
+ data: base64Data,
509
+ mimeType: image.mimeType,
510
+ });
511
+ if (!imageData) {
512
+ this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
513
+ return false;
514
+ }
496
515
  if (settings.get("images.autoResize")) {
497
516
  try {
498
517
  const resized = await resizeImage({
499
518
  type: "image",
500
- data: base64Data,
501
- mimeType: image.mimeType,
519
+ data: imageData.data,
520
+ mimeType: imageData.mimeType,
502
521
  });
503
- imageData = { data: resized.data, mimeType: resized.mimeType };
522
+ imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
504
523
  } catch {
505
- imageData = { data: base64Data, mimeType: image.mimeType };
524
+ // Keep the normalized image when resize fails.
506
525
  }
507
526
  }
508
527
 
@@ -660,7 +660,7 @@ export class MCPCommandController {
660
660
  }
661
661
 
662
662
  async #removeManagedOAuthCredential(credentialId: string | undefined): Promise<void> {
663
- if (!credentialId || !credentialId.startsWith("mcp_oauth_")) return;
663
+ if (!credentialId?.startsWith("mcp_oauth_")) return;
664
664
  await this.ctx.session.modelRegistry.authStorage.remove(credentialId);
665
665
  }
666
666
 
@@ -4,6 +4,7 @@ import { postmortem } from "@oh-my-pi/pi-utils";
4
4
  /**
5
5
  * Run modes for the coding agent.
6
6
  */
7
+ export { runAcpMode } from "./acp";
7
8
  export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
8
9
  export { type PrintModeOptions, runPrintMode } from "./print-mode";
9
10
  export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client";
@@ -12,7 +12,12 @@ import chalk from "chalk";
12
12
  import { KeybindingsManager } from "../config/keybindings";
13
13
  import { renderPromptTemplate } from "../config/prompt-templates";
14
14
  import { type Settings, settings } from "../config/settings";
15
- import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
15
+ import type {
16
+ ExtensionUIContext,
17
+ ExtensionUIDialogOptions,
18
+ ExtensionWidgetContent,
19
+ ExtensionWidgetOptions,
20
+ } from "../extensibility/extensions";
16
21
  import type { CompactOptions } from "../extensibility/extensions/types";
17
22
  import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
18
23
  import { resolveLocalUrlToPath } from "../internal-urls";
@@ -93,6 +98,8 @@ export class InteractiveMode implements InteractiveModeContext {
93
98
  btwContainer: Container;
94
99
  editor: CustomEditor;
95
100
  editorContainer: Container;
101
+ hookWidgetContainerAbove: Container;
102
+ hookWidgetContainerBelow: Container;
96
103
  statusLine: StatusLineComponent;
97
104
 
98
105
  isInitialized = false;
@@ -216,6 +223,9 @@ export class InteractiveMode implements InteractiveModeContext {
216
223
  } catch (error) {
217
224
  logger.warn("History storage unavailable", { error: String(error) });
218
225
  }
226
+ this.hookWidgetContainerAbove = new Container();
227
+ this.hookWidgetContainerAbove.addChild(new Spacer(1));
228
+ this.hookWidgetContainerBelow = new Container();
219
229
  this.editorContainer = new Container();
220
230
  this.editorContainer.addChild(this.editor);
221
231
  this.statusLine = new StatusLineComponent(session);
@@ -263,7 +273,7 @@ export class InteractiveMode implements InteractiveModeContext {
263
273
  async init(): Promise<void> {
264
274
  if (this.isInitialized) return;
265
275
 
266
- this.keybindings = await logger.timeAsync("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
276
+ this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
267
277
 
268
278
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
269
279
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
@@ -329,8 +339,9 @@ export class InteractiveMode implements InteractiveModeContext {
329
339
  this.ui.addChild(this.todoContainer);
330
340
  this.ui.addChild(this.btwContainer);
331
341
  this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
332
- this.ui.addChild(new Spacer(1));
342
+ this.ui.addChild(this.hookWidgetContainerAbove);
333
343
  this.ui.addChild(this.editorContainer);
344
+ this.ui.addChild(this.hookWidgetContainerBelow);
334
345
  this.ui.setFocus(this.editor);
335
346
 
336
347
  this.#inputController.setupKeyHandlers();
@@ -837,6 +848,7 @@ export class InteractiveMode implements InteractiveModeContext {
837
848
  this.#sttController = undefined;
838
849
  }
839
850
  this.#extensionUiController.clearExtensionTerminalInputListeners();
851
+ this.#extensionUiController.clearHookWidgets();
840
852
  this.statusLine.dispose();
841
853
  if (this.#resizeHandler) {
842
854
  process.stdout.removeListener("resize", this.#resizeHandler);
@@ -1359,8 +1371,8 @@ export class InteractiveMode implements InteractiveModeContext {
1359
1371
  return this.#extensionUiController.emitCustomToolSessionEvent(reason, previousSessionFile);
1360
1372
  }
1361
1373
 
1362
- setHookWidget(key: string, content: unknown): void {
1363
- this.#extensionUiController.setHookWidget(key, content);
1374
+ setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
1375
+ this.#extensionUiController.setHookWidget(key, content, options);
1364
1376
  }
1365
1377
 
1366
1378
  setHookStatus(key: string, text: string | undefined): void {
@@ -146,7 +146,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
146
146
  });
147
147
 
148
148
  // Send initial message with attachments
149
- if (initialMessage) {
149
+ if (initialMessage !== undefined) {
150
150
  await session.prompt(initialMessage, { images: initialImages });
151
151
  }
152
152
 
@@ -2,7 +2,7 @@ import {
2
2
  type AutocompleteItem,
3
3
  type AutocompleteProvider,
4
4
  CombinedAutocompleteProvider,
5
- getEditorKeybindings,
5
+ getKeybindings,
6
6
  type SlashCommand,
7
7
  } from "@oh-my-pi/pi-tui";
8
8
  import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
@@ -174,26 +174,26 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
174
174
  export function createPromptActionAutocompleteProvider(
175
175
  options: PromptActionAutocompleteOptions,
176
176
  ): PromptActionAutocompleteProvider {
177
- const editorKeybindings = getEditorKeybindings();
177
+ const editorKeybindings = getKeybindings();
178
178
  const actions: PromptActionDefinition[] = [
179
179
  {
180
180
  id: "copy-line",
181
181
  label: "Copy current line",
182
- description: formatKeyHints(options.keybindings.getKeys("copyLine")),
182
+ description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyLine")),
183
183
  keywords: ["copy", "line", "clipboard", "current"],
184
184
  execute: options.copyCurrentLine,
185
185
  },
186
186
  {
187
187
  id: "copy-prompt",
188
188
  label: "Copy whole prompt",
189
- description: formatKeyHints(options.keybindings.getKeys("copyPrompt")),
189
+ description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyPrompt")),
190
190
  keywords: ["copy", "prompt", "clipboard", "message"],
191
191
  execute: options.copyPrompt,
192
192
  },
193
193
  {
194
194
  id: "undo",
195
195
  label: "Undo",
196
- description: formatKeyHints(editorKeybindings.getKeys("undo")),
196
+ description: formatKeyHints(editorKeybindings.getKeys("tui.editor.undo")),
197
197
  keywords: ["undo", "revert", "edit", "history"],
198
198
  execute: options.undo,
199
199
  },
@@ -214,14 +214,14 @@ export function createPromptActionAutocompleteProvider(
214
214
  {
215
215
  id: "cursor-line-start",
216
216
  label: "Move cursor to beginning of line",
217
- description: formatKeyHints(editorKeybindings.getKeys("cursorLineStart")),
217
+ description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineStart")),
218
218
  keywords: ["move", "cursor", "line", "start", "beginning", "home"],
219
219
  execute: options.moveCursorToLineStart,
220
220
  },
221
221
  {
222
222
  id: "cursor-line-end",
223
223
  label: "Move cursor to end of line",
224
- description: formatKeyHints(editorKeybindings.getKeys("cursorLineEnd")),
224
+ description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineEnd")),
225
225
  keywords: ["move", "cursor", "line", "end"],
226
226
  execute: options.moveCursorToLineEnd,
227
227
  },
@@ -11,7 +11,11 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
14
- import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
14
+ import type {
15
+ ExtensionUIContext,
16
+ ExtensionUIDialogOptions,
17
+ ExtensionWidgetOptions,
18
+ } from "../../extensibility/extensions";
15
19
  import { type Theme, theme } from "../../modes/theme/theme";
16
20
  import type { AgentSession } from "../../session/agent-session";
17
21
  import type {
@@ -198,7 +202,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
198
202
  // Not supported in RPC mode
199
203
  }
200
204
 
201
- setWidget(key: string, content: unknown): void {
205
+ setWidget(key: string, content: unknown, options?: ExtensionWidgetOptions): void {
202
206
  // Only support string arrays in RPC mode - factory functions are ignored
203
207
  if (content === undefined || Array.isArray(content)) {
204
208
  this.output({
@@ -207,6 +211,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
207
211
  method: "setWidget",
208
212
  widgetKey: key,
209
213
  widgetLines: content as string[] | undefined,
214
+ widgetPlacement: options?.placement,
210
215
  } as RpcExtensionUIRequest);
211
216
  }
212
217
  // Component factories are not supported in RPC mode - would need TUI access
@@ -215,6 +215,7 @@ export type RpcExtensionUIRequest =
215
215
  method: "setWidget";
216
216
  widgetKey: string;
217
217
  widgetLines: string[] | undefined;
218
+ widgetPlacement?: "aboveEditor" | "belowEditor";
218
219
  }
219
220
  | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
220
221
  | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };