@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -4,7 +4,7 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
4
4
  import { settings } from "../../config/settings";
5
5
  import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
6
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
- import { isSilentAbort } from "../../session/messages";
7
+ import { isSilentAbort, resolveAbortLabel } from "../../session/messages";
8
8
  import { resolveImageOptions } from "../../tools/render-utils";
9
9
 
10
10
  /**
@@ -208,10 +208,6 @@ export class AssistantMessageComponent extends Container {
208
208
  c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
209
209
  );
210
210
 
211
- if (hasVisibleContent) {
212
- this.#contentContainer.addChild(new Spacer(1));
213
- }
214
-
215
211
  // Render content in order
216
212
  let thinkingIndex = 0;
217
213
  for (let i = 0; i < message.content.length; i++) {
@@ -257,10 +253,7 @@ export class AssistantMessageComponent extends Container {
257
253
  const hasToolCalls = message.content.some(c => c.type === "toolCall");
258
254
  if (!hasToolCalls) {
259
255
  if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
260
- const abortMessage =
261
- message.errorMessage && message.errorMessage !== "Request was aborted"
262
- ? message.errorMessage
263
- : "Operation aborted";
256
+ const abortMessage = resolveAbortLabel(message.errorMessage);
264
257
  if (hasVisibleContent) {
265
258
  this.#contentContainer.addChild(new Spacer(1));
266
259
  } else {
@@ -0,0 +1,111 @@
1
+ import { Container } from "@oh-my-pi/pi-tui";
2
+
3
+ /**
4
+ * Capabilities a mounted {@link ChatBlock} may use against its host transcript.
5
+ * Kept minimal so blocks never reach into the full TUI/InteractiveMode surface.
6
+ */
7
+ export interface ChatBlockHost {
8
+ /** Schedule a repaint of the transcript. */
9
+ requestRender(): void;
10
+ }
11
+
12
+ /**
13
+ * Lifecycle-aware transcript block — the "return a block, let the host mount it"
14
+ * primitive, modelled on React/Svelte component lifecycles.
15
+ *
16
+ * Producers build and return a `ChatBlock` instead of poking `chatContainer` and
17
+ * `ui.requestRender()` directly. The host (`ctx.present`) appends it and calls
18
+ * {@link mount}, which runs {@link onMount}; effects started there register
19
+ * teardown via {@link onCleanup}. The block repaints through {@link requestRender}
20
+ * — never touching the TUI — and tears down exactly once on {@link finish}
21
+ * (self-complete: stop the animation, keep the final frame in the transcript) or
22
+ * {@link dispose} (host discards it, e.g. a transcript reset).
23
+ *
24
+ * While mounted and unfinished a block reports `isTranscriptBlockFinalized() ===
25
+ * false` so {@link "../components/transcript-container".TranscriptContainer}
26
+ * keeps it in the live, repaintable region on ED3-risk terminals; after
27
+ * `finish()`/`dispose()` it reports `true` and freezes at its final content.
28
+ */
29
+ export abstract class ChatBlock extends Container {
30
+ #host: ChatBlockHost | undefined;
31
+ #cleanups: Array<() => void> = [];
32
+ #active = false;
33
+ #disposed = false;
34
+
35
+ /**
36
+ * Run setup after the block is in the transcript: start timers/subscriptions
37
+ * and register their teardown with {@link onCleanup}. Default: no-op (a block
38
+ * whose content is fixed at construction needs no mount work).
39
+ */
40
+ protected onMount(): void {}
41
+
42
+ /**
43
+ * Register a teardown to run on {@link finish}/{@link dispose}, à la a
44
+ * `useEffect` cleanup. If the block is already disposed the cleanup runs
45
+ * immediately so callers never leak.
46
+ */
47
+ protected onCleanup(cleanup: () => void): void {
48
+ if (this.#disposed) {
49
+ cleanup();
50
+ return;
51
+ }
52
+ this.#cleanups.push(cleanup);
53
+ }
54
+
55
+ /** Ask the host to repaint. No-op before mount or after dispose. */
56
+ protected requestRender(): void {
57
+ this.#host?.requestRender();
58
+ }
59
+
60
+ /** True between {@link mount} and {@link finish}/{@link dispose}. */
61
+ protected get active(): boolean {
62
+ return this.#active;
63
+ }
64
+
65
+ /**
66
+ * Host-only: attach the host and run {@link onMount}. Idempotent — a second
67
+ * call (e.g. a transcript rebuild that re-presents the same instance) is a
68
+ * no-op.
69
+ */
70
+ mount(host: ChatBlockHost): void {
71
+ if (this.#host || this.#disposed) return;
72
+ this.#host = host;
73
+ this.#active = true;
74
+ this.onMount();
75
+ }
76
+
77
+ /**
78
+ * Self-complete: stop ongoing effects and freeze the block at its current
79
+ * content, leaving it rendered in the transcript. Use when the operation the
80
+ * block represents finishes (connection resolved, download done).
81
+ */
82
+ finish(): void {
83
+ if (!this.#active) return;
84
+ this.#active = false;
85
+ this.#runCleanups();
86
+ this.requestRender();
87
+ }
88
+
89
+ /**
90
+ * Host-only teardown: release everything and propagate to children. Called
91
+ * when the host permanently discards the block (transcript reset). Idempotent.
92
+ */
93
+ override dispose(): void {
94
+ if (this.#disposed) return;
95
+ this.#disposed = true;
96
+ this.#active = false;
97
+ this.#runCleanups();
98
+ super.dispose();
99
+ this.#host = undefined;
100
+ }
101
+
102
+ /** Live blocks stay repaintable; finished/disposed ones may freeze. */
103
+ isTranscriptBlockFinalized(): boolean {
104
+ return !this.#active;
105
+ }
106
+
107
+ #runCleanups(): void {
108
+ const cleanups = this.#cleanups.splice(0);
109
+ for (const cleanup of cleanups) cleanup();
110
+ }
111
+ }
@@ -10,6 +10,7 @@ import {
10
10
  matchesSelectUp,
11
11
  } from "../utils/keybinding-matchers";
12
12
  import { keyHint, rawKeyHint } from "./keybinding-hints";
13
+ import { bottomBorder, divider, row, topBorder } from "./overlay-box";
13
14
 
14
15
  /** Minimum rows reserved for the tree even on short terminals. */
15
16
  const MIN_TREE_ROWS = 3;
@@ -32,50 +33,6 @@ interface FlatNode {
32
33
  ancestorHasNext: boolean[];
33
34
  }
34
35
 
35
- /** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
36
- function fit(text: string, width: number): string {
37
- if (width <= 0) return "";
38
- const w = visibleWidth(text);
39
- if (w === width) return text;
40
- if (w < width) return text + padding(width - w);
41
- const cut = truncateToWidth(text, width);
42
- const cw = visibleWidth(cut);
43
- return cw < width ? cut + padding(width - cw) : cut;
44
- }
45
-
46
- function paint(s: string): string {
47
- return theme.fg("border", s);
48
- }
49
-
50
- function topBorder(width: number, title: string): string {
51
- const box = theme.boxSharp;
52
- const inner = Math.max(0, width - 2);
53
- if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
54
- const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
55
- const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
56
- return (
57
- paint(box.topLeft + box.horizontal) +
58
- theme.bold(theme.fg("accent", shown)) +
59
- paint(box.horizontal.repeat(fillWidth) + box.topRight)
60
- );
61
- }
62
-
63
- function divider(width: number): string {
64
- const box = theme.boxSharp;
65
- return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
66
- }
67
-
68
- function bottomBorder(width: number): string {
69
- const box = theme.boxSharp;
70
- return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
71
- }
72
-
73
- /** Wrap pre-styled content in vertical borders with single-column insets. */
74
- function row(content: string, width: number): string {
75
- const box = theme.boxSharp;
76
- return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
77
- }
78
-
79
36
  /** Render one tree connector as exactly three cells (e.g. "├─ ", "└─ ", "|--"). */
80
37
  function connectorCells(symbol: string): string {
81
38
  const chars = Array.from(symbol);
@@ -1,4 +1,4 @@
1
- import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
1
+ import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
2
2
  import type { AppKeybinding } from "../../config/keybindings";
3
3
  import { imageReferenceHyperlink, renderImageReferences } from "../image-references";
4
4
  import { highlightMagicKeywords } from "../magic-keywords";
@@ -47,13 +47,36 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
47
47
  "app.clipboard.copyPrompt": ["alt+shift+c"],
48
48
  };
49
49
 
50
+ function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
51
+ const matchKeys = new Set<string>();
52
+ for (const key of keys) {
53
+ addKeyAliases(matchKeys, key);
54
+ }
55
+ return matchKeys;
56
+ }
57
+
58
+ const BRACKETED_PASTE_START = "\x1b[200~";
59
+ const BRACKETED_PASTE_END = "\x1b[201~";
60
+ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
61
+
62
+ export function extractBracketedImagePastePath(data: string): string | undefined {
63
+ if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
64
+ const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
65
+ if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
66
+
67
+ const pasted = data.slice(BRACKETED_PASTE_START.length, endIndex).trim();
68
+ if (!pasted || /[\r\n]/.test(pasted)) return undefined;
69
+ if (!BRACKETED_IMAGE_PATH_REGEX.test(pasted)) return undefined;
70
+ return pasted;
71
+ }
72
+
50
73
  /**
51
74
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
52
75
  */
53
76
  export class CustomEditor extends Editor {
54
77
  imageLinks?: readonly (string | undefined)[];
55
78
 
56
- /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflow" keywords as the user types
79
+ /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
57
80
  * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
58
81
  * pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
59
82
  decorateText = (text: string): string =>
@@ -82,6 +105,8 @@ export class CustomEditor extends Editor {
82
105
  onCopyPrompt?: () => void;
83
106
  /** Called when the configured image-paste shortcut is pressed. */
84
107
  onPasteImage?: () => Promise<boolean>;
108
+ /** Called when a bracketed paste contains exactly one image-file path. */
109
+ onPasteImagePath?: (path: string) => void;
85
110
  /** Called when the configured raw text-paste shortcut is pressed. */
86
111
  onPasteTextRaw?: () => void;
87
112
  /** Called when the configured dequeue shortcut is pressed. */
@@ -91,21 +116,38 @@ export class CustomEditor extends Editor {
91
116
 
92
117
  /** Custom key handlers from extensions and non-built-in app actions. */
93
118
  #customKeyHandlers = new Map<KeyId, () => void>();
119
+ #customMatchKeys = new Map<string, () => void>();
94
120
  #actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
95
121
  Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
96
122
  );
123
+ #actionMatchKeys = new Map<ConfigurableEditorAction, Set<string>>(
124
+ Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [
125
+ action as ConfigurableEditorAction,
126
+ buildMatchKeys(keys),
127
+ ]),
128
+ );
97
129
 
98
130
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
99
131
  this.#actionKeys.set(action, [...keys]);
132
+ this.#rebuildActionMatchKeys(action);
100
133
  }
101
134
 
102
- #matchesAction(data: string, action: ConfigurableEditorAction): boolean {
103
- const keys = this.#actionKeys.get(action);
104
- if (!keys) return false;
105
- for (const key of keys) {
106
- if (matchesKey(data, key)) return true;
135
+ #rebuildActionMatchKeys(action: ConfigurableEditorAction): void {
136
+ this.#actionMatchKeys.set(action, buildMatchKeys(this.#actionKeys.get(action) ?? []));
137
+ }
138
+
139
+ #rebuildCustomMatchKeys(): void {
140
+ this.#customMatchKeys.clear();
141
+ for (const [keyId, handler] of this.#customKeyHandlers) {
142
+ for (const alias of buildMatchKeys([keyId])) {
143
+ // Preserve current iteration behavior: the first registered handler for colliding aliases wins.
144
+ if (!this.#customMatchKeys.has(alias)) this.#customMatchKeys.set(alias, handler);
145
+ }
107
146
  }
108
- return false;
147
+ }
148
+
149
+ #matchesAction(canonical: string | undefined, action: ConfigurableEditorAction): boolean {
150
+ return canonical !== undefined && (this.#actionMatchKeys.get(action)?.has(canonical) ?? false);
109
151
  }
110
152
 
111
153
  /**
@@ -113,6 +155,7 @@ export class CustomEditor extends Editor {
113
155
  */
114
156
  setCustomKeyHandler(key: KeyId, handler: () => void): void {
115
157
  this.#customKeyHandlers.set(key, handler);
158
+ this.#rebuildCustomMatchKeys();
116
159
  }
117
160
 
118
161
  /**
@@ -120,6 +163,7 @@ export class CustomEditor extends Editor {
120
163
  */
121
164
  removeCustomKeyHandler(key: KeyId): void {
122
165
  this.#customKeyHandlers.delete(key);
166
+ this.#rebuildCustomMatchKeys();
123
167
  }
124
168
 
125
169
  /**
@@ -127,135 +171,146 @@ export class CustomEditor extends Editor {
127
171
  */
128
172
  clearCustomKeyHandlers(): void {
129
173
  this.#customKeyHandlers.clear();
174
+ this.#rebuildCustomMatchKeys();
130
175
  }
131
176
 
132
177
  handleInput(data: string): void {
133
- const parsed = parseKittySequence(data);
134
- if (parsed && (parsed.modifier & 64) !== 0 && this.onCapsLock) {
178
+ const kittyParsed = parseKittySequence(data);
179
+ if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
135
180
  // Caps Lock is modifier bit 64
136
181
  this.onCapsLock();
137
182
  return;
138
183
  }
139
184
 
140
- // Intercept configured image paste (async - fires and handles result)
141
- if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
142
- void this.onPasteImage();
185
+ const pastedImagePath = extractBracketedImagePastePath(data);
186
+ if (pastedImagePath && this.onPasteImagePath) {
187
+ this.onPasteImagePath(pastedImagePath);
143
188
  return;
144
189
  }
145
190
 
146
- // Intercept configured raw text paste (fires and handles result)
147
- if (this.#matchesAction(data, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
148
- this.onPasteTextRaw();
149
- return;
150
- }
191
+ const parsedKey = parseKey(data);
192
+ const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
151
193
 
152
- // Intercept configured external editor shortcut
153
- if (this.#matchesAction(data, "app.editor.external") && this.onExternalEditor) {
154
- this.onExternalEditor();
155
- return;
156
- }
194
+ if (canonical !== undefined) {
195
+ // Intercept configured image paste (async - fires and handles result)
196
+ if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
197
+ void this.onPasteImage();
198
+ return;
199
+ }
157
200
 
158
- // Intercept configured temporary model selector shortcut
159
- if (this.#matchesAction(data, "app.model.selectTemporary") && this.onSelectModelTemporary) {
160
- this.onSelectModelTemporary();
161
- return;
162
- }
201
+ // Intercept configured raw text paste (fires and handles result)
202
+ if (this.#matchesAction(canonical, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
203
+ this.onPasteTextRaw();
204
+ return;
205
+ }
163
206
 
164
- // Intercept configured display reset shortcut
165
- if (this.#matchesAction(data, "app.display.reset") && this.onDisplayReset) {
166
- this.onDisplayReset();
167
- return;
168
- }
207
+ // Intercept configured external editor shortcut
208
+ if (this.#matchesAction(canonical, "app.editor.external") && this.onExternalEditor) {
209
+ this.onExternalEditor();
210
+ return;
211
+ }
169
212
 
170
- // Intercept configured suspend shortcut
171
- if (this.#matchesAction(data, "app.suspend") && this.onSuspend) {
172
- this.onSuspend();
173
- return;
174
- }
213
+ // Intercept configured temporary model selector shortcut
214
+ if (this.#matchesAction(canonical, "app.model.selectTemporary") && this.onSelectModelTemporary) {
215
+ this.onSelectModelTemporary();
216
+ return;
217
+ }
175
218
 
176
- // Intercept configured thinking block visibility toggle
177
- if (this.#matchesAction(data, "app.thinking.toggle") && this.onToggleThinking) {
178
- this.onToggleThinking();
179
- return;
180
- }
219
+ // Intercept configured display reset shortcut
220
+ if (this.#matchesAction(canonical, "app.display.reset") && this.onDisplayReset) {
221
+ this.onDisplayReset();
222
+ return;
223
+ }
181
224
 
182
- // Intercept configured model selector shortcut
183
- if (this.#matchesAction(data, "app.model.select") && this.onSelectModel) {
184
- this.onSelectModel();
185
- return;
186
- }
225
+ // Intercept configured suspend shortcut
226
+ if (this.#matchesAction(canonical, "app.suspend") && this.onSuspend) {
227
+ this.onSuspend();
228
+ return;
229
+ }
187
230
 
188
- // Intercept configured history search shortcut
189
- if (this.#matchesAction(data, "app.history.search") && this.onHistorySearch) {
190
- this.onHistorySearch();
191
- return;
192
- }
231
+ // Intercept configured thinking block visibility toggle
232
+ if (this.#matchesAction(canonical, "app.thinking.toggle") && this.onToggleThinking) {
233
+ this.onToggleThinking();
234
+ return;
235
+ }
193
236
 
194
- // Intercept configured tool output expansion shortcut
195
- if (this.#matchesAction(data, "app.tools.expand") && this.onExpandTools) {
196
- this.onExpandTools();
197
- return;
198
- }
237
+ // Intercept configured model selector shortcut
238
+ if (this.#matchesAction(canonical, "app.model.select") && this.onSelectModel) {
239
+ this.onSelectModel();
240
+ return;
241
+ }
199
242
 
200
- // Intercept configured backward model cycling (check before forward cycling)
201
- if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
202
- this.onCycleModelBackward();
203
- return;
204
- }
243
+ // Intercept configured history search shortcut
244
+ if (this.#matchesAction(canonical, "app.history.search") && this.onHistorySearch) {
245
+ this.onHistorySearch();
246
+ return;
247
+ }
205
248
 
206
- // Intercept configured forward model cycling
207
- if (this.#matchesAction(data, "app.model.cycleForward") && this.onCycleModelForward) {
208
- this.onCycleModelForward();
209
- return;
210
- }
249
+ // Intercept configured tool output expansion shortcut
250
+ if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
251
+ this.onExpandTools();
252
+ return;
253
+ }
211
254
 
212
- // Intercept configured thinking level cycling
213
- if (this.#matchesAction(data, "app.thinking.cycle") && this.onCycleThinkingLevel) {
214
- this.onCycleThinkingLevel();
215
- return;
216
- }
255
+ // Intercept configured backward model cycling (check before forward cycling)
256
+ if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
257
+ this.onCycleModelBackward();
258
+ return;
259
+ }
217
260
 
218
- // Intercept configured interrupt shortcut.
219
- // When the autocomplete popup is visible, ESC's first job is to dismiss
220
- // the popup — let super.handleInput() route it to #cancelAutocomplete().
221
- // The user can press ESC again afterward to fire the global interrupt
222
- // handler. This matches the standard TUI/IDE pattern and prevents a
223
- // single ESC from both closing an @ completion and aborting an active
224
- // agent run (#1655).
225
- if (this.#matchesAction(data, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
226
- this.onEscape();
227
- return;
228
- }
261
+ // Intercept configured forward model cycling
262
+ if (this.#matchesAction(canonical, "app.model.cycleForward") && this.onCycleModelForward) {
263
+ this.onCycleModelForward();
264
+ return;
265
+ }
229
266
 
230
- // Intercept configured clear shortcut
231
- if (this.#matchesAction(data, "app.clear") && this.onClear) {
232
- this.onClear();
233
- return;
234
- }
267
+ // Intercept configured thinking level cycling
268
+ if (this.#matchesAction(canonical, "app.thinking.cycle") && this.onCycleThinkingLevel) {
269
+ this.onCycleThinkingLevel();
270
+ return;
271
+ }
235
272
 
236
- // Intercept configured exit shortcut. Always consume the shortcut so it
237
- // never reaches the parent handler; firing onExit is the controller's
238
- // chance to snapshot the current text as a draft before shutting down.
239
- if (this.#matchesAction(data, "app.exit")) {
240
- this.onExit?.();
241
- return;
242
- }
273
+ // Intercept configured interrupt shortcut.
274
+ // When the autocomplete popup is visible, ESC's first job is to dismiss
275
+ // the popup let super.handleInput() route it to #cancelAutocomplete().
276
+ // The user can press ESC again afterward to fire the global interrupt
277
+ // handler. This matches the standard TUI/IDE pattern and prevents a
278
+ // single ESC from both closing an @ completion and aborting an active
279
+ // agent run (#1655).
280
+ if (this.#matchesAction(canonical, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
281
+ this.onEscape();
282
+ return;
283
+ }
243
284
 
244
- // Intercept configured dequeue shortcut (restore queued message to editor)
245
- if (this.#matchesAction(data, "app.message.dequeue") && this.onDequeue) {
246
- this.onDequeue();
247
- return;
248
- }
285
+ // Intercept configured clear shortcut
286
+ if (this.#matchesAction(canonical, "app.clear") && this.onClear) {
287
+ this.onClear();
288
+ return;
289
+ }
249
290
 
250
- // Intercept configured copy-prompt shortcut
251
- if (this.#matchesAction(data, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
252
- this.onCopyPrompt();
253
- return;
254
- }
291
+ // Intercept configured exit shortcut. Always consume the shortcut so it
292
+ // never reaches the parent handler; firing onExit is the controller's
293
+ // chance to snapshot the current text as a draft before shutting down.
294
+ if (this.#matchesAction(canonical, "app.exit")) {
295
+ this.onExit?.();
296
+ return;
297
+ }
255
298
 
256
- // Check custom key handlers (extensions)
257
- for (const [keyId, handler] of this.#customKeyHandlers) {
258
- if (matchesKey(data, keyId)) {
299
+ // Intercept configured dequeue shortcut (restore queued message to editor)
300
+ if (this.#matchesAction(canonical, "app.message.dequeue") && this.onDequeue) {
301
+ this.onDequeue();
302
+ return;
303
+ }
304
+
305
+ // Intercept configured copy-prompt shortcut
306
+ if (this.#matchesAction(canonical, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
307
+ this.onCopyPrompt();
308
+ return;
309
+ }
310
+
311
+ // Check custom key handlers (extensions)
312
+ const handler = this.#customMatchKeys.get(canonical);
313
+ if (handler) {
259
314
  handler();
260
315
  return;
261
316
  }
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
- import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
2
+ import { Box, Container } from "@oh-my-pi/pi-tui";
3
3
  import type { MessageRenderer } from "../../extensibility/extensions/types";
4
4
  import { theme } from "../../modes/theme/theme";
5
5
  import type { CustomMessage } from "../../session/messages";
@@ -20,8 +20,6 @@ export class CustomMessageComponent extends Container {
20
20
  ) {
21
21
  super();
22
22
 
23
- this.addChild(new Spacer(1));
24
-
25
23
  // Create box with custom background (used for default rendering)
26
24
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
27
25
 
@@ -7,7 +7,7 @@
7
7
  * stay in their respective files.
8
8
  */
9
9
 
10
- import { type Component, Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
10
+ import { type Component, Container, Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
11
11
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
12
12
  import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
13
13
  import { DynamicBorder } from "./dynamic-border";
@@ -31,7 +31,6 @@ export function buildExecutionFrame(
31
31
  ): { contentContainer: Container; loader: Loader } {
32
32
  const borderColor = (str: string) => theme.fg(colorKey, str);
33
33
 
34
- parent.addChild(new Spacer(1));
35
34
  parent.addChild(new DynamicBorder(borderColor));
36
35
 
37
36
  const contentContainer = new Container();
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
- import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
2
+ import { Box, Container } from "@oh-my-pi/pi-tui";
3
3
  import type { HookMessageRenderer } from "../../extensibility/hooks/types";
4
4
  import { theme } from "../../modes/theme/theme";
5
5
  import type { HookMessage } from "../../session/messages";
@@ -23,8 +23,6 @@ export class HookMessageComponent extends Container {
23
23
  ) {
24
24
  super();
25
25
 
26
- this.addChild(new Spacer(1));
27
-
28
26
  // Create box with purple background (used for default rendering)
29
27
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
30
28