@linxiraos/pi-tui 1.2.1 → 1.3.0

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 (274) hide show
  1. package/CHANGELOG.md +188 -125
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +21 -20
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +26 -6
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/annotation-overlay.ts +422 -168
  157. package/src/overlays/approval-dialog.ts +389 -0
  158. package/src/overlays/ask-dialog.ts +244 -105
  159. package/src/overlays/btw-history-panel.ts +208 -147
  160. package/src/overlays/extensions/extension-list.ts +4 -6
  161. package/src/overlays/hook-editor.ts +90 -32
  162. package/src/overlays/hook-input.ts +105 -19
  163. package/src/overlays/job-tree.ts +177 -0
  164. package/src/overlays/jobs-panel.ts +222 -142
  165. package/src/overlays/login-dialog.ts +201 -91
  166. package/src/overlays/mcp-add-wizard.ts +562 -279
  167. package/src/overlays/model-browser.ts +32 -7
  168. package/src/overlays/model-hub.ts +4 -10
  169. package/src/overlays/model-picker.ts +58 -21
  170. package/src/overlays/pause-screen.ts +81 -23
  171. package/src/overlays/plan-review-overlay.ts +16 -0
  172. package/src/overlays/plugin-selector.ts +17 -1
  173. package/src/overlays/queue-mode-selector.ts +10 -0
  174. package/src/overlays/report-panel.ts +6 -2
  175. package/src/overlays/reset-usage-selector.ts +130 -21
  176. package/src/overlays/session-account-selector.ts +36 -0
  177. package/src/overlays/session-info-overlay.ts +199 -76
  178. package/src/overlays/session-selector.ts +15 -10
  179. package/src/overlays/settings-selector.ts +3 -0
  180. package/src/overlays/show-images-selector.ts +1 -0
  181. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  182. package/src/overlays/theme-selector.ts +6 -1
  183. package/src/overlays/thinking-selector.ts +8 -4
  184. package/src/overlays/tree-selector.ts +117 -2
  185. package/src/overlays/usage-dashboard.ts +498 -144
  186. package/src/prompt/composer-cache.ts +1 -1
  187. package/src/prompt/composer.ts +194 -74
  188. package/src/prompt/custom-editor.ts +168 -65
  189. package/src/prompt/image-references.d.ts +22 -0
  190. package/src/prompt/image-references.ts +79 -2
  191. package/src/prompt/internal-url-autocomplete.ts +1 -0
  192. package/src/prompt/macos-spelling.ts +28 -5
  193. package/src/prompt/prose-gate.ts +3 -0
  194. package/src/prompt/queued-messages.ts +9 -2
  195. package/src/prompt/tips.txt +1 -2
  196. package/src/prompt/typing-word.ts +59 -0
  197. package/src/prompt/welcome.ts +97 -45
  198. package/src/render/code-cell.ts +18 -5
  199. package/src/render/hanging-wrap.d.ts +7 -0
  200. package/src/render/hanging-wrap.ts +58 -0
  201. package/src/render/hyperlink.d.ts +2 -0
  202. package/src/render/hyperlink.ts +5 -0
  203. package/src/render/render-utils.d.ts +9 -1
  204. package/src/render/render-utils.ts +16 -2
  205. package/src/render/utils.d.ts +1 -1
  206. package/src/render/utils.ts +1 -1
  207. package/src/render/width-aware-text.ts +6 -5
  208. package/src/setup/scenes/splash.ts +22 -8
  209. package/src/status-line/component.ts +23 -10
  210. package/src/status-line/context-usage.ts +176 -73
  211. package/src/status-line/footer.ts +18 -35
  212. package/src/status-line/host.ts +6 -1
  213. package/src/status-line/segments.ts +16 -5
  214. package/src/status-line/startup.ts +6 -6
  215. package/src/status-line/types.ts +1 -0
  216. package/src/stdin-buffer.d.ts +7 -0
  217. package/src/stdin-buffer.ts +11 -1
  218. package/src/terminal-capabilities.d.ts +16 -1
  219. package/src/terminal-capabilities.ts +66 -28
  220. package/src/terminal-multiplexer.d.ts +7 -0
  221. package/src/terminal-multiplexer.ts +14 -0
  222. package/src/terminal.d.ts +8 -4
  223. package/src/terminal.ts +160 -46
  224. package/src/theme/symbols.d.ts +1 -1
  225. package/src/theme/symbols.ts +1 -0
  226. package/src/theme/theme-class.ts +1 -0
  227. package/src/theme/theme.d.ts +11 -0
  228. package/src/theme/theme.ts +46 -14
  229. package/src/thinking.d.ts +5 -0
  230. package/src/thinking.ts +9 -1
  231. package/src/tmux.d.ts +39 -4
  232. package/src/tmux.ts +110 -16
  233. package/src/tools/ask.ts +20 -9
  234. package/src/tools/ast-edit.d.ts +2 -2
  235. package/src/tools/ast-edit.ts +30 -7
  236. package/src/tools/ast-grep.d.ts +2 -2
  237. package/src/tools/ast-grep.ts +39 -35
  238. package/src/tools/bash.d.ts +6 -1
  239. package/src/tools/bash.ts +15 -9
  240. package/src/tools/debug.ts +2 -1
  241. package/src/tools/default-renderer.ts +3 -2
  242. package/src/tools/edit.d.ts +2 -0
  243. package/src/tools/edit.ts +100 -21
  244. package/src/tools/eval.d.ts +53 -4
  245. package/src/tools/eval.ts +173 -137
  246. package/src/tools/find.d.ts +14 -4
  247. package/src/tools/find.ts +71 -18
  248. package/src/tools/glob.d.ts +3 -3
  249. package/src/tools/glob.ts +51 -17
  250. package/src/tools/grep.d.ts +2 -2
  251. package/src/tools/grep.ts +99 -22
  252. package/src/tools/grouped-file-output.d.ts +9 -4
  253. package/src/tools/grouped-file-output.ts +21 -6
  254. package/src/tools/lsp.ts +30 -18
  255. package/src/tools/mcp.ts +2 -2
  256. package/src/tools/native-view.d.ts +1 -1
  257. package/src/tools/native-view.ts +2 -2
  258. package/src/tools/read.d.ts +32 -10
  259. package/src/tools/read.ts +89 -43
  260. package/src/tools/renderer.d.ts +2 -0
  261. package/src/tools/renderer.ts +2 -0
  262. package/src/tools/task.d.ts +31 -0
  263. package/src/tools/task.ts +40 -7
  264. package/src/tools/todo.ts +3 -2
  265. package/src/tools/wait.ts +16 -30
  266. package/src/tools/web-search.ts +2 -1
  267. package/src/tools/write.d.ts +2 -0
  268. package/src/tools/write.ts +36 -12
  269. package/src/tools/yield.ts +2 -2
  270. package/src/tui.d.ts +20 -3
  271. package/src/tui.ts +140 -36
  272. package/src/utils.ts +5 -3
  273. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  274. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -1,5 +1,4 @@
1
- import { existsSync, unlinkSync } from "node:fs";
2
- import { createServer, type Server, type Socket } from "node:net";
1
+ import { type IpcConnection, IpcServer } from "@linxiraos/pi-natives";
3
2
  import { type Component, isFocusable, type OverlayOptions, type TUI } from "./tui";
4
3
  import { plainText } from "./native/spans";
5
4
 
@@ -209,72 +208,60 @@ function mouseSequence(x: number, y: number, action: string): string {
209
208
  export class TuiDebugServer {
210
209
  readonly #tui: TUI;
211
210
  readonly #path: string;
212
- #server: Server | undefined;
213
- #sockets = new Set<Socket>();
211
+ #server: IpcServer | undefined;
212
+ #listening: Promise<boolean> | undefined;
213
+ #stopped = false;
214
+ #connections = new Set<IpcConnection>();
214
215
 
215
216
  constructor(tui: TUI, path: string) {
216
217
  this.#tui = tui;
217
218
  this.#path = path;
218
219
  }
219
220
 
221
+ /** Resolves true once the socket accepts connections, false if listen failed or the server stopped first. */
222
+ get ready(): Promise<boolean> {
223
+ return this.#listening ?? Promise.resolve(false);
224
+ }
225
+
220
226
  start(): void {
221
- if (this.#server !== undefined) return;
222
- try {
223
- if (existsSync(this.#path)) unlinkSync(this.#path);
224
- } catch {
225
- // The listen error below is isolated from the host application.
226
- }
227
- const server = createServer(socket => this.#accept(socket));
228
- this.#server = server;
229
- server.on("error", error => {
230
- void error;
231
- });
232
- try {
233
- server.listen(this.#path);
234
- } catch {
235
- this.#server = undefined;
236
- server.close();
237
- }
227
+ if (this.#listening !== undefined) return;
228
+ this.#stopped = false;
229
+ this.#listening = IpcServer.listen(this.#path, { framing: "lines" }, connection => this.#accept(connection)).then(
230
+ server => {
231
+ if (this.#stopped) {
232
+ void server.close().catch(() => {});
233
+ return false;
234
+ }
235
+ this.#server = server;
236
+ return true;
237
+ },
238
+ () => false, // A live peer or unusable path disables the debug socket; the host application is unaffected.
239
+ );
238
240
  }
239
241
 
240
242
  stop(): void {
243
+ this.#stopped = true;
241
244
  const server = this.#server;
242
245
  this.#server = undefined;
243
- for (const socket of this.#sockets) socket.destroy();
244
- this.#sockets.clear();
245
- try {
246
- server?.close();
247
- } catch {
248
- // A failed or already-closed listener still needs its stale path removed.
249
- }
250
- try {
251
- if (existsSync(this.#path)) unlinkSync(this.#path);
252
- } catch {
253
- // Teardown must not affect the host application.
254
- }
246
+ this.#listening = undefined;
247
+ for (const connection of this.#connections) connection.destroy();
248
+ this.#connections.clear();
249
+ void server?.close().catch(() => {});
255
250
  }
256
251
 
257
- #accept(socket: Socket): void {
258
- this.#sockets.add(socket);
259
- let buffer = "";
260
- socket.setEncoding("utf8");
261
- socket.on("data", chunk => {
262
- buffer += chunk;
263
- let newline = buffer.indexOf("\n");
264
- while (newline !== -1) {
265
- const line = buffer.slice(0, newline).replace(/\r$/, "");
266
- buffer = buffer.slice(newline + 1);
267
- if (line.length > 0) this.#handleLine(socket, line);
268
- newline = buffer.indexOf("\n");
269
- }
270
- });
271
- socket.on("error", error => {
272
- void error;
273
- });
274
- socket.on("close", () => this.#sockets.delete(socket));
252
+ #accept(connection: IpcConnection): void {
253
+ if (this.#stopped) {
254
+ connection.destroy();
255
+ return;
256
+ }
257
+ this.#connections.add(connection);
258
+ connection.start(
259
+ line => void this.#handleLine(connection, line),
260
+ () => this.#connections.delete(connection),
261
+ );
275
262
  }
276
263
 
277
- #handleLine(socket: Socket, line: string): void {
264
+ async #handleLine(connection: IpcConnection, line: string): Promise<void> {
278
265
  let request: DebugRequest;
279
266
  try {
280
267
  const parsed: unknown = JSON.parse(line);
@@ -282,36 +269,31 @@ export class TuiDebugServer {
282
269
  throw new Error("request must be an object");
283
270
  request = parsed as DebugRequest;
284
271
  } catch (error) {
285
- this.#write(socket, { ok: false, error: `malformed JSON: ${errorMessage(error)}` });
272
+ this.#write(connection, { ok: false, error: `malformed JSON: ${errorMessage(error)}` });
286
273
  return;
287
274
  }
275
+ let response: DebugResponse;
288
276
  try {
289
- const response = this.#dispatch(request);
290
- if (request.op === "quit" && response.ok) {
291
- this.#write(socket, response, () => {
292
- this.#tui.stop();
293
- process.exit(0);
294
- });
295
- return;
296
- }
297
- this.#write(socket, response);
277
+ response = this.#dispatch(request);
298
278
  } catch (error) {
299
- this.#write(socket, { ok: false, error: errorMessage(error) });
279
+ response = { ok: false, error: errorMessage(error) };
280
+ }
281
+ this.#write(connection, response);
282
+ if (request.op === "quit" && response.ok) {
283
+ await connection.drained();
284
+ this.#tui.stop();
285
+ process.exit(0);
300
286
  }
301
287
  }
302
288
 
303
- #write(socket: Socket, response: DebugResponse, flushed?: () => void): void {
289
+ #write(connection: IpcConnection, response: DebugResponse): void {
304
290
  let line: string;
305
291
  try {
306
292
  line = JSON.stringify(response);
307
293
  } catch (error) {
308
294
  line = JSON.stringify({ ok: false, error: errorMessage(error) });
309
295
  }
310
- try {
311
- socket.write(`${line}\n`, flushed);
312
- } catch {
313
- // Broken clients cannot affect the TUI.
314
- }
296
+ connection.send(line);
315
297
  }
316
298
 
317
299
  #dispatch(request: DebugRequest): DebugResponse {
@@ -135,6 +135,8 @@ function hotkeyGroups(bindings: HotkeysMarkdownBindings): HotkeyGroup[] {
135
135
  { keys: [act("app.history.search")], action: "Search prompt history" },
136
136
  { keys: [act("app.tools.expand")], action: "Toggle tool output expansion" },
137
137
  { keys: [act("app.tools.toggleVisibility")], action: "Toggle tool activity visibility" },
138
+ { keys: [act("app.ask.hide")], action: "Hide/show a pending ask question" },
139
+ { keys: [act("app.approval.approve")], action: "Approve a pending tool call" },
138
140
  { keys: [act("app.thinking.toggle")], action: "Toggle thinking block visibility" },
139
141
  { keys: [act("app.editor.external")], action: "Edit message in external editor" },
140
142
  { keys: [act("app.retry")], action: "Retry last failed assistant turn" },
@@ -9,6 +9,8 @@ import type { KeyId } from "./app-keybindings";
9
9
  export declare function matchesAppInterrupt(data: string): boolean;
10
10
  /** Match `app.tools.expand`, falling back to Ctrl+O when the action is unbound. */
11
11
  export declare function matchesAppToolsExpand(data: string): boolean;
12
+ /** Effective tools-expand key: the first bound key, or Ctrl+O when unbound. */
13
+ export declare function appToolsExpandKey(): KeyId;
12
14
  /** Match the generic selector cancel keybinding. */
13
15
  export declare function matchesSelectCancel(data: string): boolean;
14
16
  /** Match the generic selector up-navigation keybinding. */
@@ -36,3 +38,7 @@ export declare function matchesAppExternalEditor(data: string): boolean;
36
38
  * effective follow-up binding.
37
39
  */
38
40
  export declare function matchesAppFollowUp(data: string): boolean;
41
+ /** Effective tool-approval keys: the `app.approval.approve` bindings, or Ctrl+Enter/Ctrl+Q when unbound. */
42
+ export declare function appApproveKeys(): readonly KeyId[];
43
+ /** Match the tool-approval key, recognizing modifier-tagged LF as Ctrl+Enter like {@link matchesAppFollowUp}. */
44
+ export declare function matchesAppApprove(data: string): boolean;
@@ -28,6 +28,11 @@ export function matchesAppToolsExpand(data: string): boolean {
28
28
  return matchesKey(data, "ctrl+o");
29
29
  }
30
30
 
31
+ /** Effective tools-expand key: the first bound key, or Ctrl+O when unbound. */
32
+ export function appToolsExpandKey(): KeyId {
33
+ return getKeybindings().getKeys("app.tools.expand")[0] ?? "ctrl+o";
34
+ }
35
+
31
36
  /** Match the generic selector cancel keybinding. */
32
37
  export function matchesSelectCancel(data: string): boolean {
33
38
  return getKeybindings().matches(data, "tui.select.cancel");
@@ -101,3 +106,17 @@ export function matchesAppFollowUp(data: string): boolean {
101
106
  }
102
107
  return matchesEffectiveKeys(data, ["ctrl+enter", "ctrl+q"]);
103
108
  }
109
+
110
+ /** Approve-key defaults when `app.approval.approve` is unbound: approval must stay reachable by keyboard. */
111
+ const APPROVE_DEFAULT_KEYS: readonly KeyId[] = ["ctrl+enter", "ctrl+q"];
112
+
113
+ /** Effective tool-approval keys: the `app.approval.approve` bindings, or Ctrl+Enter/Ctrl+Q when unbound. */
114
+ export function appApproveKeys(): readonly KeyId[] {
115
+ const keys = getKeybindings().getKeys("app.approval.approve");
116
+ return keys.length > 0 ? keys : APPROVE_DEFAULT_KEYS;
117
+ }
118
+
119
+ /** Match the tool-approval key, recognizing modifier-tagged LF as Ctrl+Enter like {@link matchesAppFollowUp}. */
120
+ export function matchesAppApprove(data: string): boolean {
121
+ return matchesEffectiveKeys(data, appApproveKeys());
122
+ }
@@ -22,6 +22,7 @@ export declare const tmuxMultiplexer: {
22
22
  readonly passthrough: typeof wrapTmuxPassthrough;
23
23
  readonly settleMs: 100;
24
24
  };
25
+ readonly gatesBracketedPaste: true;
25
26
  readonly expandsRepPadding: true;
26
27
  readonly expiresSynchronizedOutput: true;
27
28
  readonly altRestoreEndsSynchronizedOutput: true;
@@ -32,6 +32,9 @@ export const tmuxMultiplexer = {
32
32
  // An explicit appearance refresh passes OSC 11 to the outer terminal, then
33
33
  // re-reads tmux's cache once it has consumed the reply.
34
34
  backgroundColorCache: { passthrough: wrapTmuxPassthrough, settleMs: 100 },
35
+ // tmux drops paste markers for a pane whose own DEC 2004 flag another
36
+ // writer cleared, and only answers DECRQM 2004 since 3.6.
37
+ gatesBracketedPaste: true,
35
38
  expandsRepPadding: true,
36
39
  // tmux expires synchronized output after one second.
37
40
  expiresSynchronizedOutput: true,
@@ -73,6 +73,12 @@ export interface TerminalMultiplexerModule<Id extends string = string> {
73
73
  * session, and an unrecognized report does not turn it off.
74
74
  */
75
75
  readonly honorsSynchronizedOutput?: boolean;
76
+ /**
77
+ * Tracks DEC 2004 per pane and forwards paste markers only while it is set,
78
+ * yet may leave DECRQM 2004 unanswered, so bracketed paste is reasserted
79
+ * without waiting for a DECRPM confirmation.
80
+ */
81
+ readonly gatesBracketedPaste?: boolean;
76
82
  /**
77
83
  * The session does not reveal whether its client renders graphics while
78
84
  * outer-terminal identity variables leak in, so images and Kitty
@@ -83,6 +83,12 @@ export interface TerminalMultiplexerModule<Id extends string = string> {
83
83
  * session, and an unrecognized report does not turn it off.
84
84
  */
85
85
  readonly honorsSynchronizedOutput?: boolean;
86
+ /**
87
+ * Tracks DEC 2004 per pane and forwards paste markers only while it is set,
88
+ * yet may leave DECRQM 2004 unanswered, so bracketed paste is reasserted
89
+ * without waiting for a DECRPM confirmation.
90
+ */
91
+ readonly gatesBracketedPaste?: boolean;
86
92
  /**
87
93
  * The session does not reveal whether its client renders graphics while
88
94
  * outer-terminal identity variables leak in, so images and Kitty
@@ -47,10 +47,11 @@ export interface NativeBackendOptions {
47
47
  }
48
48
  /**
49
49
  * The reply a v1 terminal is assumed to give before its real `hello` arrives
50
- * (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary, the
51
- * default APC limit and credits, the terminal's width, the appearance omp
52
- * already detected, full motion, and `blobs`, so a resumed session's images
53
- * are asked about rather than resent before the reply comes.
50
+ * (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary except
51
+ * {@link UNASSUMED_KINDS}, the default APC limit and credits, the terminal's
52
+ * width, the appearance omp already detected, full motion, and `blobs`, so a
53
+ * resumed session's images are asked about rather than resent before the
54
+ * reply comes.
54
55
  */
55
56
  export declare function assumedTspHello(terminal: Terminal): TspHello;
56
57
  export declare class NativeBackend {
@@ -202,12 +202,20 @@ function sameSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
202
202
  return true;
203
203
  }
204
204
 
205
+ /**
206
+ * Kinds newer than some Tern builds that answer `TERM_PROGRAM=tern`: used only
207
+ * once the real `hello` lists them, so the optimistic start never emits a node
208
+ * an older terminal cannot draw (`media`: files Tern loads by path).
209
+ */
210
+ const UNASSUMED_KINDS: Partial<Record<TspKind, true>> = { media: true };
211
+
205
212
  /**
206
213
  * The reply a v1 terminal is assumed to give before its real `hello` arrives
207
- * (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary, the
208
- * default APC limit and credits, the terminal's width, the appearance omp
209
- * already detected, full motion, and `blobs`, so a resumed session's images
210
- * are asked about rather than resent before the reply comes.
214
+ * (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary except
215
+ * {@link UNASSUMED_KINDS}, the default APC limit and credits, the terminal's
216
+ * width, the appearance omp already detected, full motion, and `blobs`, so a
217
+ * resumed session's images are asked about rather than resent before the
218
+ * reply comes.
211
219
  */
212
220
  export function assumedTspHello(terminal: Terminal): TspHello {
213
221
  const appearance = terminal.appearance;
@@ -215,7 +223,7 @@ export function assumedTspHello(terminal: Terminal): TspHello {
215
223
  r: "hello",
216
224
  v: TSP_VERSION,
217
225
  term: "tern",
218
- kinds: TSP_KINDS,
226
+ kinds: TSP_KINDS.filter(kind => !UNASSUMED_KINDS[kind]),
219
227
  features: ["blobs"],
220
228
  apc: TSP_DEFAULT_APC_LIMIT,
221
229
  credits: TSP_DEFAULT_CREDITS,
@@ -1,14 +1,5 @@
1
- /**
2
- * Content-addressed binary blobs (images) for `image` nodes.
3
- *
4
- * `describe()` registers the bytes and puts the returned id in `image.p.blob`;
5
- * the native backend delivers each referenced blob once per terminal
6
- * connection (asking the terminal first which it holds, or through its blob
7
- * cache), else uploads it with verb `b` before the frame that first
8
- * references it.
9
- */
10
1
  import type { TspProps } from "@linxiraos/pi-wire";
11
- import type { NativeNode } from "./node";
2
+ import type { NativeChild, NativeNode } from "./node";
12
3
  export interface NativeBlob {
13
4
  readonly id: string;
14
5
  readonly mime: string;
@@ -26,8 +17,29 @@ export declare function nativeImageNode(bytes: Uint8Array, mime: string, p?: Omi
26
17
  * so callers cache the node per payload ({@link NativeImageCache}).
27
18
  */
28
19
  export declare function base64ImageNode(data: string, mimeType: string, p?: Omit<TspProps<"image">, "blob" | "builtin" | "w" | "h">, key?: string): NativeNode;
29
- /** `image` nodes for base64 payloads, one per slot key, rebuilt only when the slot's payload changes. */
20
+ /**
21
+ * Props that make an `image` node open the file at `filePath` (as a `file://` URL, the TSP
22
+ * open-target form): a click zooms the image in the terminal, and the file opens from its
23
+ * context menu. Empty without a file.
24
+ */
25
+ export declare function openableImageProps(filePath: string | undefined): Pick<TspProps<"image">, "href" | "actions">;
26
+ /**
27
+ * `image` nodes for base64 payloads, one per slot key, rebuilt only when the slot's payload
28
+ * or the link policy changes. Each opens its file (`sourcePath`, else a blob copy).
29
+ */
30
30
  export declare class NativeImageCache {
31
31
  #private;
32
- get(key: string, data: string, mimeType: string): NativeNode;
32
+ get(key: string, data: string, mimeType: string, sourcePath?: string): NativeNode;
33
33
  }
34
+ /** The disk file a tool result's image was read from (`read` details `mediaPath`), when it has one. */
35
+ export declare function readMediaPath(details: unknown): string | undefined;
36
+ /**
37
+ * A `media` node the host loads by path, capped at the inline image preview
38
+ * height. Callers check `cx.supports("media")` first.
39
+ */
40
+ export declare function nativeMediaNode(path: string, key: string): NativeNode;
41
+ /** Blob ids of the `image` nodes and paths of the `media` nodes under `children` (described nodes only). */
42
+ export declare function shownMedia(children: readonly NativeChild[] | undefined): {
43
+ blobs: Set<string>;
44
+ paths: Set<string>;
45
+ };
@@ -7,10 +7,13 @@
7
7
  * cache), else uploads it with verb `b` before the frame that first
8
8
  * references it.
9
9
  */
10
+ import * as url from "node:url";
10
11
  import type { TspProps } from "@linxiraos/pi-wire";
12
+ import { resolveImageOptions } from "../render/render-utils";
11
13
  import { getImageDimensions } from "../terminal-capabilities";
12
14
  import { node } from "./describe";
13
- import type { NativeNode } from "./node";
15
+ import { imageLinksEnabled, transcriptImageLink } from "../prompt/image-references";
16
+ import type { NativeChild, NativeNode } from "./node";
14
17
 
15
18
  export interface NativeBlob {
16
19
  readonly id: string;
@@ -91,15 +94,61 @@ export function base64ImageNode(
91
94
  );
92
95
  }
93
96
 
94
- /** `image` nodes for base64 payloads, one per slot key, rebuilt only when the slot's payload changes. */
97
+ /**
98
+ * Props that make an `image` node open the file at `filePath` (as a `file://` URL, the TSP
99
+ * open-target form): a click zooms the image in the terminal, and the file opens from its
100
+ * context menu. Empty without a file.
101
+ */
102
+ export function openableImageProps(filePath: string | undefined): Pick<TspProps<"image">, "href" | "actions"> {
103
+ return filePath ? { href: url.pathToFileURL(filePath).href, actions: { menu: ["open"] } } : {};
104
+ }
105
+
106
+ /**
107
+ * `image` nodes for base64 payloads, one per slot key, rebuilt only when the slot's payload
108
+ * or the link policy changes. Each opens its file (`sourcePath`, else a blob copy).
109
+ */
95
110
  export class NativeImageCache {
96
- #entries = new Map<string, { data: string; node: NativeNode }>();
111
+ #entries = new Map<string, { data: string; linked: boolean; node: NativeNode }>();
97
112
 
98
- get(key: string, data: string, mimeType: string): NativeNode {
113
+ get(key: string, data: string, mimeType: string, sourcePath?: string): NativeNode {
114
+ const linked = imageLinksEnabled();
99
115
  const cached = this.#entries.get(key);
100
- if (cached?.data === data) return cached.node;
101
- const image = base64ImageNode(data, mimeType, { alt: mimeType }, key);
102
- this.#entries.set(key, { data, node: image });
116
+ if (cached?.data === data && cached.linked === linked) return cached.node;
117
+ const open = openableImageProps(transcriptImageLink({ data, mimeType }, sourcePath));
118
+ const image = base64ImageNode(data, mimeType, { alt: mimeType, ...open }, key);
119
+ this.#entries.set(key, { data, linked, node: image });
103
120
  return image;
104
121
  }
105
122
  }
123
+
124
+ /** The disk file a tool result's image was read from (`read` details `mediaPath`), when it has one. */
125
+ export function readMediaPath(details: unknown): string | undefined {
126
+ if (typeof details !== "object" || details === null) return undefined;
127
+ const mediaPath = (details as { mediaPath?: unknown }).mediaPath;
128
+ return typeof mediaPath === "string" && mediaPath.length > 0 ? mediaPath : undefined;
129
+ }
130
+
131
+ /**
132
+ * A `media` node the host loads by path, capped at the inline image preview
133
+ * height. Callers check `cx.supports("media")` first.
134
+ */
135
+ export function nativeMediaNode(path: string, key: string): NativeNode {
136
+ const maxRows = resolveImageOptions().maxHeightCells;
137
+ return node("media", { path, max: maxRows && maxRows > 0 ? { h: `${maxRows}lines` } : undefined }, undefined, key);
138
+ }
139
+
140
+ /** Blob ids of the `image` nodes and paths of the `media` nodes under `children` (described nodes only). */
141
+ export function shownMedia(children: readonly NativeChild[] | undefined): { blobs: Set<string>; paths: Set<string> } {
142
+ const shown = { blobs: new Set<string>(), paths: new Set<string>() };
143
+ const visit = (list: readonly NativeChild[] | undefined): void => {
144
+ for (const child of list ?? []) {
145
+ if (!("k" in child)) continue;
146
+ const p = child.p as { blob?: unknown; path?: unknown } | undefined;
147
+ if (child.k === "image" && typeof p?.blob === "string") shown.blobs.add(p.blob);
148
+ else if (child.k === "media" && typeof p?.path === "string") shown.paths.add(p.path);
149
+ visit(child.c);
150
+ }
151
+ };
152
+ visit(children);
153
+ return shown;
154
+ }
@@ -405,13 +405,31 @@ export class Reconciler {
405
405
  return this.#findEditorIn(node, state.id, "", state.owner, depth);
406
406
  }
407
407
 
408
- #findEditorIn(node: NativeNode, id: string, keypath: string, owner: Owner, depth: number): string | null {
408
+ /**
409
+ * The first editor under `node`. An `overlay`/`picker` child outside a hoisted
410
+ * sheet is hoisted into `layer` under its `^` keypath (see {@link #entries}), so
411
+ * a field in a sheet a dock component describes is found under that id.
412
+ */
413
+ #findEditorIn(
414
+ node: NativeNode,
415
+ id: string,
416
+ keypath: string,
417
+ owner: Owner,
418
+ depth: number,
419
+ hoist = true,
420
+ ): string | null {
409
421
  if (node.k === "editor" || node.k === "input") return id;
410
- for (const entry of this.#entries(node.c, keypath, owner, true, null)) {
411
- const found =
412
- entry.comp === undefined
413
- ? this.#findEditorIn(entry.node, entry.id, entry.keypath, owner, depth)
414
- : this.#findEditorOf(entry.comp, depth + 1);
422
+ for (const entry of this.#entries(node.c, keypath, owner, false, null)) {
423
+ let found: string | null;
424
+ if (entry.comp !== undefined) {
425
+ found = this.#findEditorOf(entry.comp, depth + 1);
426
+ } else if (hoist && (entry.node.k === "overlay" || entry.node.k === "picker")) {
427
+ const path = entry.keypath;
428
+ const portalPath = keypath === "" ? `^${path}` : `${keypath}/^${path.slice(keypath.length + 1)}`;
429
+ found = this.#findEditorIn(entry.node, nodeId(owner.base, portalPath), portalPath, owner, depth, false);
430
+ } else {
431
+ found = this.#findEditorIn(entry.node, entry.id, entry.keypath, owner, depth, hoist);
432
+ }
415
433
  if (found !== null) return found;
416
434
  }
417
435
  return null;
@@ -682,24 +700,28 @@ export class Reconciler {
682
700
  this.#ops.push(["text", id, "append", newText.slice(oldText.length)]);
683
701
  return;
684
702
  }
685
- if ((kind === "editor" || kind === "input") && oldText.length > 0) {
703
+ // A streaming `md` node rewrites only its tail (the speculatively closed span it ends in), so an
704
+ // edit past a shared head splices too instead of resending the whole text.
705
+ if ((kind === "editor" || kind === "input" || kind === "md") && oldText.length > 0) {
686
706
  const limit = Math.min(oldText.length, newText.length);
687
707
  let prefix = 0;
688
708
  while (prefix < limit && oldText.charCodeAt(prefix) === newText.charCodeAt(prefix)) prefix++;
689
- let suffix = 0;
690
- while (
691
- suffix < limit - prefix &&
692
- oldText.charCodeAt(oldText.length - 1 - suffix) === newText.charCodeAt(newText.length - 1 - suffix)
693
- )
694
- suffix++;
695
- this.#ops.push([
696
- "splice",
697
- id,
698
- prefix,
699
- oldText.length - prefix - suffix,
700
- newText.slice(prefix, newText.length - suffix),
701
- ]);
702
- return;
709
+ if (kind !== "md" || prefix > 0) {
710
+ let suffix = 0;
711
+ while (
712
+ suffix < limit - prefix &&
713
+ oldText.charCodeAt(oldText.length - 1 - suffix) === newText.charCodeAt(newText.length - 1 - suffix)
714
+ )
715
+ suffix++;
716
+ this.#ops.push([
717
+ "splice",
718
+ id,
719
+ prefix,
720
+ oldText.length - prefix - suffix,
721
+ newText.slice(prefix, newText.length - suffix),
722
+ ]);
723
+ return;
724
+ }
703
725
  }
704
726
  this.#ops.push(["text", id, "replace", newText]);
705
727
  }