@oh-my-pi/pi-tui 18.13.2 → 18.14.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 (245) hide show
  1. package/CHANGELOG.md +40 -54
  2. package/README.md +34 -34
  3. package/dist/types/app-keybindings.d.ts +15 -0
  4. package/dist/types/apps/session-picker.d.ts +5 -0
  5. package/dist/types/charts/chart-plan.d.ts +9 -7
  6. package/dist/types/chat/assistant-message.d.ts +15 -3
  7. package/dist/types/chat/bash-execution.d.ts +2 -0
  8. package/dist/types/chat/display-preferences.d.ts +2 -0
  9. package/dist/types/chat/extension-types.d.ts +31 -0
  10. package/dist/types/chat/reply-write.d.ts +48 -0
  11. package/dist/types/chat/table-chart.d.ts +4 -3
  12. package/dist/types/chat/tool-execution.d.ts +1 -2
  13. package/dist/types/chat/transcript-outline.d.ts +7 -0
  14. package/dist/types/chat/transcript-render-helpers.d.ts +2 -0
  15. package/dist/types/chrome/index.d.ts +1 -0
  16. package/dist/types/chrome/transcript-notice.d.ts +35 -0
  17. package/dist/types/chrome/transcript-status.d.ts +20 -8
  18. package/dist/types/components/editor.d.ts +8 -0
  19. package/dist/types/components/markdown.d.ts +7 -0
  20. package/dist/types/native/blobs.d.ts +2 -1
  21. package/dist/types/native/picker.d.ts +3 -3
  22. package/dist/types/overlays/annotation-overlay.d.ts +7 -1
  23. package/dist/types/overlays/annotation-types.d.ts +28 -0
  24. package/dist/types/overlays/login-dialog.d.ts +5 -1
  25. package/dist/types/overlays/model-browser.d.ts +10 -1
  26. package/dist/types/overlays/oauth-selector.d.ts +23 -0
  27. package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
  28. package/dist/types/overlays/rewind-selector.d.ts +1 -1
  29. package/dist/types/overlays/session-selector.d.ts +22 -2
  30. package/dist/types/prompt/composer.d.ts +41 -4
  31. package/dist/types/prompt/interactive-context-helpers.d.ts +10 -3
  32. package/dist/types/prompt/welcome.d.ts +6 -2
  33. package/dist/types/render/extension-links.d.ts +65 -0
  34. package/dist/types/render/hyperlink.d.ts +25 -7
  35. package/dist/types/render/index.d.ts +1 -0
  36. package/dist/types/setup/scenes/model.d.ts +1 -1
  37. package/dist/types/setup/scenes/outro.d.ts +5 -4
  38. package/dist/types/setup/scenes/sign-in.d.ts +13 -8
  39. package/dist/types/setup/scenes/splash.d.ts +4 -4
  40. package/dist/types/setup/scenes/types.d.ts +16 -1
  41. package/dist/types/setup/startup-splash.d.ts +1 -1
  42. package/dist/types/setup/wizard-overlay.d.ts +11 -7
  43. package/dist/types/setup/wizard.d.ts +5 -1
  44. package/dist/types/status-line/component.d.ts +1 -1
  45. package/dist/types/status-line/context-usage.d.ts +28 -6
  46. package/dist/types/status-line/host.d.ts +6 -2
  47. package/dist/types/theme/theme.d.ts +0 -23
  48. package/dist/types/tools/bash-interactive.d.ts +6 -0
  49. package/dist/types/tools/bash.d.ts +2 -0
  50. package/dist/types/tools/default-renderer.d.ts +6 -4
  51. package/dist/types/tools/irc.d.ts +6 -0
  52. package/dist/types/tools/json-tree.d.ts +23 -4
  53. package/dist/types/tools/lsp.d.ts +2 -0
  54. package/dist/types/tools/mcp.d.ts +6 -5
  55. package/dist/types/tools/native-view.d.ts +1 -1
  56. package/dist/types/tools/renderer.d.ts +2 -0
  57. package/dist/types/tools/streaming-output.d.ts +11 -0
  58. package/dist/types/tools/task.d.ts +9 -1
  59. package/dist/types/tools/url-scheme-host.d.ts +17 -0
  60. package/dist/types/tools/yield.d.ts +1 -1
  61. package/dist/types/tui.d.ts +7 -2
  62. package/package.json +10 -15
  63. package/src/app-keybindings.ts +15 -0
  64. package/src/apps/session-picker.ts +7 -0
  65. package/src/charts/chart-plan.ts +20 -50
  66. package/src/chat/advisor-message.ts +2 -0
  67. package/src/chat/assistant-message.ts +137 -19
  68. package/src/chat/background-tan-message.ts +4 -0
  69. package/src/chat/bash-execution.ts +6 -0
  70. package/src/chat/chat-transcript-builder.ts +58 -28
  71. package/src/chat/compaction-summary-message.ts +2 -1
  72. package/src/chat/display-preferences.ts +3 -0
  73. package/src/chat/extension-types.ts +34 -0
  74. package/src/chat/reply-write.ts +127 -0
  75. package/src/chat/table-chart.ts +10 -4
  76. package/src/chat/tool-execution.ts +20 -13
  77. package/src/chat/transcript-outline.ts +27 -0
  78. package/src/chat/transcript-render-helpers.ts +66 -24
  79. package/src/chrome/index.ts +1 -0
  80. package/src/chrome/transcript-notice.ts +83 -0
  81. package/src/chrome/transcript-status.ts +29 -29
  82. package/src/components/editor.ts +10 -1
  83. package/src/components/markdown.ts +116 -6
  84. package/src/hotkeys-markdown.ts +1 -0
  85. package/src/native/backend.ts +5 -66
  86. package/src/native/blobs.ts +3 -2
  87. package/src/native/picker.ts +3 -3
  88. package/src/overlays/advisor-config.ts +1 -10
  89. package/src/overlays/annotation-overlay.ts +125 -37
  90. package/src/overlays/annotation-types.ts +24 -0
  91. package/src/overlays/hook-selector.ts +2 -2
  92. package/src/overlays/login-dialog.ts +31 -5
  93. package/src/overlays/model-browser.ts +37 -0
  94. package/src/overlays/model-hub.ts +1 -7
  95. package/src/overlays/model-picker.ts +1 -10
  96. package/src/overlays/oauth-selector.ts +44 -6
  97. package/src/overlays/plan-review-overlay.ts +13 -2
  98. package/src/overlays/rewind-selector.ts +29 -12
  99. package/src/overlays/session-selector.ts +305 -39
  100. package/src/overlays/usage-dashboard.ts +5 -12
  101. package/src/prompt/composer.ts +213 -5
  102. package/src/prompt/custom-editor.ts +6 -1
  103. package/src/prompt/emoji-autocomplete.ts +0 -2
  104. package/src/prompt/interactive-context-helpers.ts +29 -4
  105. package/src/prompt/welcome.ts +31 -45
  106. package/src/render/extension-links.ts +214 -0
  107. package/src/render/hyperlink.ts +65 -9
  108. package/src/render/index.ts +1 -0
  109. package/src/setup/scenes/composer.ts +2 -0
  110. package/src/setup/scenes/glyph.ts +2 -0
  111. package/src/setup/scenes/model.ts +192 -29
  112. package/src/setup/scenes/outro.ts +30 -17
  113. package/src/setup/scenes/sign-in.ts +196 -400
  114. package/src/setup/scenes/splash.ts +25 -21
  115. package/src/setup/scenes/theme.ts +2 -0
  116. package/src/setup/scenes/types.ts +18 -1
  117. package/src/setup/startup-splash.ts +8 -7
  118. package/src/setup/wizard-overlay.ts +164 -33
  119. package/src/setup/wizard.ts +11 -2
  120. package/src/status-line/component.ts +4 -10
  121. package/src/status-line/context-usage.ts +72 -21
  122. package/src/status-line/host.ts +6 -2
  123. package/src/terminal.ts +24 -2
  124. package/src/theme/defaults.json +1 -0
  125. package/src/theme/loader.ts +1 -1
  126. package/src/theme/theme.ts +2 -75
  127. package/src/tools/bash-interactive.ts +6 -1
  128. package/src/tools/bash.ts +2 -0
  129. package/src/tools/default-renderer.ts +9 -6
  130. package/src/tools/eval.ts +14 -4
  131. package/src/tools/irc.ts +6 -0
  132. package/src/tools/job-widget.ts +13 -1
  133. package/src/tools/json-tree.ts +132 -42
  134. package/src/tools/lsp.ts +36 -15
  135. package/src/tools/mcp.ts +20 -12
  136. package/src/tools/native-view.ts +1 -1
  137. package/src/tools/renderer.ts +2 -0
  138. package/src/tools/streaming-output.ts +41 -0
  139. package/src/tools/task.ts +59 -17
  140. package/src/tools/url-scheme-host.ts +21 -0
  141. package/src/tools/yield.ts +18 -9
  142. package/src/tui.ts +43 -15
  143. package/THIRD-PARTY-NOTICES.txt +0 -22901
  144. package/dist/types/theme/defaults/index.d.ts +0 -10289
  145. package/src/theme/defaults/alabaster.json +0 -103
  146. package/src/theme/defaults/amethyst.json +0 -106
  147. package/src/theme/defaults/anthracite.json +0 -103
  148. package/src/theme/defaults/basalt.json +0 -101
  149. package/src/theme/defaults/birch.json +0 -105
  150. package/src/theme/defaults/dark-abyss.json +0 -101
  151. package/src/theme/defaults/dark-arctic.json +0 -114
  152. package/src/theme/defaults/dark-aurora.json +0 -105
  153. package/src/theme/defaults/dark-catppuccin.json +0 -117
  154. package/src/theme/defaults/dark-cavern.json +0 -101
  155. package/src/theme/defaults/dark-celestial.json +0 -116
  156. package/src/theme/defaults/dark-copper.json +0 -105
  157. package/src/theme/defaults/dark-cosmos.json +0 -100
  158. package/src/theme/defaults/dark-cyberpunk.json +0 -112
  159. package/src/theme/defaults/dark-dracula.json +0 -108
  160. package/src/theme/defaults/dark-eclipse.json +0 -101
  161. package/src/theme/defaults/dark-ember.json +0 -105
  162. package/src/theme/defaults/dark-equinox.json +0 -100
  163. package/src/theme/defaults/dark-forest.json +0 -106
  164. package/src/theme/defaults/dark-github.json +0 -115
  165. package/src/theme/defaults/dark-gruvbox.json +0 -122
  166. package/src/theme/defaults/dark-lavender.json +0 -105
  167. package/src/theme/defaults/dark-lunar.json +0 -99
  168. package/src/theme/defaults/dark-midnight.json +0 -105
  169. package/src/theme/defaults/dark-monochrome.json +0 -104
  170. package/src/theme/defaults/dark-monokai.json +0 -108
  171. package/src/theme/defaults/dark-nebula.json +0 -100
  172. package/src/theme/defaults/dark-neon-noir.json +0 -110
  173. package/src/theme/defaults/dark-nord.json +0 -108
  174. package/src/theme/defaults/dark-ocean.json +0 -111
  175. package/src/theme/defaults/dark-one.json +0 -110
  176. package/src/theme/defaults/dark-poimandres.json +0 -125
  177. package/src/theme/defaults/dark-rainforest.json +0 -101
  178. package/src/theme/defaults/dark-reef.json +0 -101
  179. package/src/theme/defaults/dark-retro.json +0 -102
  180. package/src/theme/defaults/dark-rose-pine.json +0 -106
  181. package/src/theme/defaults/dark-sakura.json +0 -105
  182. package/src/theme/defaults/dark-slate.json +0 -105
  183. package/src/theme/defaults/dark-solarized.json +0 -107
  184. package/src/theme/defaults/dark-solstice.json +0 -100
  185. package/src/theme/defaults/dark-starfall.json +0 -101
  186. package/src/theme/defaults/dark-sunset.json +0 -109
  187. package/src/theme/defaults/dark-swamp.json +0 -100
  188. package/src/theme/defaults/dark-synthwave.json +0 -113
  189. package/src/theme/defaults/dark-taiga.json +0 -101
  190. package/src/theme/defaults/dark-terminal.json +0 -105
  191. package/src/theme/defaults/dark-tokyo-night.json +0 -111
  192. package/src/theme/defaults/dark-tundra.json +0 -101
  193. package/src/theme/defaults/dark-twilight.json +0 -101
  194. package/src/theme/defaults/dark-volcanic.json +0 -101
  195. package/src/theme/defaults/graphite.json +0 -102
  196. package/src/theme/defaults/index.ts +0 -203
  197. package/src/theme/defaults/light-arctic.json +0 -117
  198. package/src/theme/defaults/light-aurora-day.json +0 -101
  199. package/src/theme/defaults/light-canyon.json +0 -101
  200. package/src/theme/defaults/light-catppuccin.json +0 -116
  201. package/src/theme/defaults/light-cirrus.json +0 -100
  202. package/src/theme/defaults/light-coral.json +0 -105
  203. package/src/theme/defaults/light-cyberpunk.json +0 -106
  204. package/src/theme/defaults/light-dawn.json +0 -100
  205. package/src/theme/defaults/light-dunes.json +0 -101
  206. package/src/theme/defaults/light-eucalyptus.json +0 -105
  207. package/src/theme/defaults/light-forest.json +0 -110
  208. package/src/theme/defaults/light-frost.json +0 -105
  209. package/src/theme/defaults/light-github.json +0 -125
  210. package/src/theme/defaults/light-glacier.json +0 -101
  211. package/src/theme/defaults/light-gruvbox.json +0 -118
  212. package/src/theme/defaults/light-haze.json +0 -100
  213. package/src/theme/defaults/light-honeycomb.json +0 -105
  214. package/src/theme/defaults/light-lagoon.json +0 -101
  215. package/src/theme/defaults/light-lavender.json +0 -105
  216. package/src/theme/defaults/light-meadow.json +0 -101
  217. package/src/theme/defaults/light-mint.json +0 -105
  218. package/src/theme/defaults/light-monochrome.json +0 -111
  219. package/src/theme/defaults/light-ocean.json +0 -109
  220. package/src/theme/defaults/light-one.json +0 -109
  221. package/src/theme/defaults/light-opal.json +0 -101
  222. package/src/theme/defaults/light-orchard.json +0 -101
  223. package/src/theme/defaults/light-paper.json +0 -105
  224. package/src/theme/defaults/light-poimandres.json +0 -125
  225. package/src/theme/defaults/light-prism.json +0 -100
  226. package/src/theme/defaults/light-retro.json +0 -108
  227. package/src/theme/defaults/light-sand.json +0 -105
  228. package/src/theme/defaults/light-savanna.json +0 -101
  229. package/src/theme/defaults/light-solarized.json +0 -112
  230. package/src/theme/defaults/light-soleil.json +0 -100
  231. package/src/theme/defaults/light-sunset.json +0 -109
  232. package/src/theme/defaults/light-synthwave.json +0 -108
  233. package/src/theme/defaults/light-tokyo-night.json +0 -121
  234. package/src/theme/defaults/light-wetland.json +0 -101
  235. package/src/theme/defaults/light-zenith.json +0 -99
  236. package/src/theme/defaults/limestone.json +0 -104
  237. package/src/theme/defaults/mahogany.json +0 -107
  238. package/src/theme/defaults/marble.json +0 -103
  239. package/src/theme/defaults/obsidian.json +0 -101
  240. package/src/theme/defaults/onyx.json +0 -101
  241. package/src/theme/defaults/pearl.json +0 -103
  242. package/src/theme/defaults/porcelain.json +0 -101
  243. package/src/theme/defaults/quartz.json +0 -106
  244. package/src/theme/defaults/sandstone.json +0 -105
  245. package/src/theme/defaults/titanium.json +0 -100
@@ -135,6 +135,7 @@ 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.links.hint")], action: "Pick an extension link on screen by its tag" },
138
139
  { keys: [act("app.ask.hide")], action: "Hide/show a pending ask question" },
139
140
  { keys: [act("app.approval.approve")], action: "Approve a pending tool call" },
140
141
  { keys: [act("app.thinking.toggle")], action: "Toggle thinking block visibility" },
@@ -39,15 +39,7 @@ import {
39
39
  type TspOp,
40
40
  } from "@oh-my-pi/pi-wire";
41
41
  import type { Terminal } from "../terminal";
42
- import {
43
- bindTheme,
44
- getCurrentThemeName,
45
- getNativeThemePalette,
46
- getNativeThemePaletteKey,
47
- isLightTheme,
48
- type NativeThemePalette,
49
- setNativeSymbolPreset,
50
- } from "../theme/theme";
42
+ import { getCurrentThemeName, isLightTheme, setNativeSymbolPreset } from "../theme/theme";
51
43
  import type { Component, OverlayOptions, RenderScheduler, RenderTimer } from "../tui";
52
44
  import { TspDocument } from "./apply";
53
45
  import { getNativeBlob, type NativeBlob } from "./blobs";
@@ -206,9 +198,10 @@ function sameSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
206
198
  /**
207
199
  * Kinds newer than some Tern builds that answer `TERM_PROGRAM=tern`: used only
208
200
  * once the real `hello` lists them, so the optimistic start never emits a node
209
- * an older terminal cannot draw (`media`: files Tern loads by path).
201
+ * an older terminal cannot draw (`media`: files Tern loads by path; `json`:
202
+ * raw JSON values Tern draws as its own tree).
210
203
  */
211
- const UNASSUMED_KINDS: Partial<Record<TspKind, true>> = { media: true };
204
+ const UNASSUMED_KINDS: Partial<Record<TspKind, true>> = { media: true, json: true };
212
205
 
213
206
  /**
214
207
  * The reply a v1 terminal is assumed to give before its real `hello` arrives
@@ -313,11 +306,6 @@ export class NativeBackend {
313
306
  #sawResize = false;
314
307
  #live = false;
315
308
  #overlayNodes = new Map<Component, { key: string; node: NativeNode }>();
316
- #unbindTheme: (() => void) | undefined;
317
- #palette: NativeThemePalette | undefined;
318
- #paletteKey: string | undefined;
319
- /** Serialized palette last sent, to skip resends of an unchanged theme. */
320
- #paletteSent: string | undefined;
321
309
  /** Pending delivery owns bytes until sent or confirmed held; settled descriptions may already be released. */
322
310
  #blobs = new Map<string, NativeBlob | "sent">();
323
311
  /** `blobs` queries sent, oldest first: the terminal answers each once, in order. */
@@ -366,54 +354,11 @@ export class NativeBackend {
366
354
  if (this.#live) return;
367
355
  this.#live = true;
368
356
  this.#useNerdSymbols(true);
369
- this.#watchTheme();
370
357
  this.#open(this.#inline);
371
358
  setNativeRendering(true);
372
359
  this.render();
373
360
  }
374
361
 
375
- /**
376
- * Any theme swap (switch, selector preview, auto light/dark, file reload)
377
- * schedules a render; `render()` resends the palette when it changed, so
378
- * bursts coalesce into one `t` and no frame is needed for it.
379
- */
380
- #watchTheme(): void {
381
- this.#unbindTheme?.();
382
- let initial = true;
383
- this.#unbindTheme = bindTheme(() => {
384
- if (initial) return;
385
- this.#host.requestRender();
386
- });
387
- initial = false;
388
- }
389
-
390
- /** The current palette, recomputed only when its inputs changed. */
391
- #currentPalette(): NativeThemePalette {
392
- const key = getNativeThemePaletteKey();
393
- if (this.#palette === undefined || key !== this.#paletteKey) {
394
- this.#paletteKey = key;
395
- this.#palette = getNativeThemePalette();
396
- }
397
- return this.#palette;
398
- }
399
-
400
- /** Send the palette to `surface` (right after its `o`, before any `f`). */
401
- #sendPalette(surface: Surface): void {
402
- const palette = this.#currentPalette();
403
- if (!palette.dark && !palette.light) return;
404
- this.#paletteSent = JSON.stringify(palette);
405
- this.#write("t", { sf: surface.id, ...palette });
406
- }
407
-
408
- /** Resend the palette to every open surface when the theme changed since it was sent. */
409
- #refreshPalette(): void {
410
- const key = this.#paletteKey;
411
- const palette = this.#currentPalette();
412
- if (key === this.#paletteKey || JSON.stringify(palette) === this.#paletteSent) return;
413
- this.#sendPalette(this.#inline);
414
- if (this.#screen) this.#sendPalette(this.#screen);
415
- }
416
-
417
362
  /**
418
363
  * Tern's font carries every Nerd Font glyph, and it never answers the Glyph
419
364
  * Protocol, so icons come from the nerd preset (process-local, not the
@@ -435,8 +380,6 @@ export class NativeBackend {
435
380
  this.#close(this.#inline, keep);
436
381
  this.#inline.doc?.close();
437
382
  setNativeRendering(false);
438
- this.#unbindTheme?.();
439
- this.#unbindTheme = undefined;
440
383
  this.#useNerdSymbols(false);
441
384
  this.#clearStallTimer();
442
385
  // Replies no longer reach the backend: forget the queries outright.
@@ -455,7 +398,6 @@ export class NativeBackend {
455
398
  this.#applyHello(hello);
456
399
  this.#live = true;
457
400
  this.#useNerdSymbols(true);
458
- this.#watchTheme();
459
401
  this.#resetBlobs();
460
402
  const surface = this.#inline;
461
403
  surface.reconciler.detachLive();
@@ -466,7 +408,6 @@ export class NativeBackend {
466
408
  surface.focus = null;
467
409
  surface.dirty = false;
468
410
  this.#write("o", { id: surface.id, mode: "inline", title: "omp", role: SESSION_ROLE, adopt: true });
469
- this.#sendPalette(surface);
470
411
  // After the `o`, as in `start()`.
471
412
  setNativeRendering(true);
472
413
  this.render();
@@ -508,7 +449,6 @@ export class NativeBackend {
508
449
  /** Reconcile and send a frame, unless the target surface is out of credits. */
509
450
  render(): void {
510
451
  if (!this.#live) return;
511
- this.#refreshPalette();
512
452
  const overlays = this.#host.overlays();
513
453
  let fullscreen = -1;
514
454
  for (let i = overlays.length - 1; i >= 0; i--) {
@@ -608,7 +548,6 @@ export class NativeBackend {
608
548
 
609
549
  #open(surface: Surface): void {
610
550
  this.#write("o", { id: surface.id, mode: surface.mode, title: "omp", role: surface.role });
611
- this.#sendPalette(surface);
612
551
  }
613
552
 
614
553
  #close(surface: Surface, keep: boolean): void {
@@ -776,7 +715,7 @@ export class NativeBackend {
776
715
  }
777
716
  }
778
717
 
779
- #write(verb: "o" | "f" | "t" | "x" | "q", body: unknown): void {
718
+ #write(verb: "o" | "f" | "x" | "q", body: unknown): void {
780
719
  this.#record("out", verb, undefined, body);
781
720
  this.#host.terminal.write(encodeTspJson(verb, body, undefined, this.#limit));
782
721
  }
@@ -97,10 +97,11 @@ export function base64ImageNode(
97
97
  /**
98
98
  * Props that make an `image` node open the file at `filePath` (as a `file://` URL, the TSP
99
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.
100
+ * context menu. Always uses a local file URL because the open action runs locally,
101
+ * even when remote file handlers are configured. Empty without a file.
101
102
  */
102
103
  export function openableImageProps(filePath: string | undefined): Pick<TspProps<"image">, "href" | "actions"> {
103
- const href = filePath ? tryFileUri(filePath) : undefined;
104
+ const href = filePath ? tryFileUri(filePath, { local: true }) : undefined;
104
105
  return href ? { href, actions: { menu: ["open"] } } : {};
105
106
  }
106
107
 
@@ -16,7 +16,7 @@
16
16
  * the item id and `action` with an action id (plus `value` for
17
17
  * `scope`/`tab`/`strip`). {@link pickerEvent} folds them into one shape.
18
18
  */
19
- import type { TspPickerAction, TspPickerItem, TspPickerProps } from "@oh-my-pi/pi-wire";
19
+ import type { TspPickerAction, TspPickerItem, TspPickerProps, TspProps } from "@oh-my-pi/pi-wire";
20
20
  import type { KeyName } from "../key-hint-format";
21
21
  import { getKeybindings, type Keybinding } from "../keybindings";
22
22
  import type { Input } from "../components/input";
@@ -25,7 +25,7 @@ import { col, node } from "./describe";
25
25
  import type { NativeChild, NativeNode, NativeUiEvent } from "./node";
26
26
 
27
27
  /** The picker sheet; `preview` children fill its preview pane. */
28
- export function picker(p: TspPickerProps, preview?: readonly NativeChild[]): NativeNode {
28
+ export function picker(p: TspProps<"picker">, preview?: readonly NativeChild[]): NativeNode {
29
29
  return node("picker", p, preview);
30
30
  }
31
31
 
@@ -46,7 +46,7 @@ export const PICKER_KEY = "picker";
46
46
  * the picker as child {@link PICKER_KEY}, which the reconciler hoists into
47
47
  * `layer`. Route its events with `pickerEvent(event, PICKER_KEY)`.
48
48
  */
49
- export function dockedPicker(p: TspPickerProps, preview?: readonly NativeChild[]): NativeNode {
49
+ export function dockedPicker(p: TspProps<"picker">, preview?: readonly NativeChild[]): NativeNode {
50
50
  return col([node("picker", p, preview, PICKER_KEY)]);
51
51
  }
52
52
 
@@ -36,7 +36,6 @@ import { editorKey, editorKeys } from "../chrome/keybinding-hints";
36
36
  import { HookEditorComponent } from "./hook-editor";
37
37
  import {
38
38
  buildBrowserItems,
39
- MODEL_PICKER_COLUMNS,
40
39
  ModelBrowser,
41
40
  type ModelBrowserItem,
42
41
  type ModelBrowserSource,
@@ -749,16 +748,8 @@ export class AdvisorConfigOverlayComponent implements Component {
749
748
  size: "md",
750
749
  layout: "rows",
751
750
  preview: "below",
752
- query: browser.query,
753
- cursor: browser.cursor,
754
751
  placeholder: "Search models…",
755
- columns: MODEL_PICKER_COLUMNS,
756
- ...catalogue,
757
- order: order.order,
758
- ...(order.hits ? { hits: order.hits } : {}),
759
- selected: browser.pickerSelected,
760
- ...(this.#modelCurrent ? { current: [this.#modelCurrent] } : {}),
761
- total: browser.baseCount,
752
+ ...browser.pickerRows(catalogue, order, this.#modelCurrent),
762
753
  empty: "No models available",
763
754
  actions: [
764
755
  pickerAction("use", "Use for advisor", "enter", { primary: true }),
@@ -19,6 +19,7 @@ import type { Keybinding } from "../keybindings";
19
19
  import { sanitizeText } from "@oh-my-pi/pi-utils";
20
20
  import { type Theme } from "../theme/theme";
21
21
  import type {
22
+ AnnotationOverlayDraft,
22
23
  CodeReviewAnnotation,
23
24
  CodeReviewOverlayResult,
24
25
  ReviewDiffFile,
@@ -125,8 +126,14 @@ const MIN_BODY_ROWS = 3;
125
126
  const SIDEBAR_MIN_TOTAL_WIDTH = 64;
126
127
  const SIDEBAR_MIN_BODY_WIDTH = 40;
127
128
  const MAX_ANNOTATION_EDITOR_ROWS = 6;
128
- const CODE_REVIEW_ACTIONS = [CONTINUE_CODE_REVIEW_ACTION, PASTE_CODE_REVIEW_ACTION] as const;
129
- const TEXT_REVIEW_ACTIONS = [PASTE_CODE_REVIEW_ACTION] as const;
129
+ /** Clears every note and closes; the host then has nothing to keep. Needs a second confirm. */
130
+ export const DISCARD_ANNOTATIONS_ACTION = "Discard notes and close";
131
+ const CODE_REVIEW_ACTIONS = [
132
+ CONTINUE_CODE_REVIEW_ACTION,
133
+ PASTE_CODE_REVIEW_ACTION,
134
+ DISCARD_ANNOTATIONS_ACTION,
135
+ ] as const;
136
+ const TEXT_REVIEW_ACTIONS = [PASTE_CODE_REVIEW_ACTION, DISCARD_ANNOTATIONS_ACTION] as const;
130
137
 
131
138
  function isSourceRow(row: ReviewDiffRow): row is ReviewSourceRow {
132
139
  return row.kind === "context" || row.kind === "added" || row.kind === "removed";
@@ -229,6 +236,8 @@ export class AnnotationOverlay implements Focusable {
229
236
  /** Set once {@link replaceTextSource} runs; the paste result then carries the edited text. */
230
237
  #textSourceEdited = false;
231
238
  #finished = false;
239
+ /** Discard was chosen once; choosing it again discards, any other input disarms it. */
240
+ #discardArmed = false;
232
241
  #annotations: CommittedAnnotation[] = [];
233
242
  #textAnnotations: CommittedTextAnnotation[] = [];
234
243
  /** Annotation lists before each create/edit/delete, restored by `u`. */
@@ -295,6 +304,8 @@ export class AnnotationOverlay implements Focusable {
295
304
  this.#files = filesOrSource;
296
305
  this.#mode = modeOrCallbacks as string;
297
306
  this.#callbacks = maybeCallbacks!;
307
+ // Start on Paste, not the LLM review: a stray Enter must not submit (it does nothing until a note exists).
308
+ this.#actionIndex = 1;
298
309
  }
299
310
  const symbols = {
300
311
  cursor: theme.nav.cursor,
@@ -357,6 +368,60 @@ export class AnnotationOverlay implements Focusable {
357
368
  this.#finished = true;
358
369
  }
359
370
 
371
+ /** Notes, the unfinished note, and any source edit, in the form {@link restoreDraft} replays. */
372
+ getDraft(): AnnotationOverlayDraft {
373
+ const editing =
374
+ this.#editingAnnotationIndex === undefined ? {} : { editingAnnotationIndex: this.#editingAnnotationIndex };
375
+ const text = this.#editor.getExpandedText();
376
+ if (this.#textSource) {
377
+ const scope = this.#annotationScope === "text" ? "text" : "line";
378
+ const sourceIndex = scope === "text" ? -1 : this.#sourceIndex;
379
+ return {
380
+ kind: "text",
381
+ annotations: this.#textAnnotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
382
+ ...(this.#textSourceEdited ? { editedText: this.#textSource.text } : {}),
383
+ ...(this.#annotating ? { editor: { sourceIndex, scope, ...editing, text } } : {}),
384
+ };
385
+ }
386
+ const scope = this.#annotationScope === "file" ? "file" : "line";
387
+ const editor = { fileIndex: this.#fileIndex, sourceIndex: this.#sourceIndex, scope, ...editing, text } as const;
388
+ return {
389
+ kind: "diff",
390
+ annotations: this.#annotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
391
+ ...(this.#annotating ? { editor } : {}),
392
+ };
393
+ }
394
+
395
+ /** Replay a draft taken from this same source; anything anchored outside it is dropped. */
396
+ restoreDraft(draft: AnnotationOverlayDraft): void {
397
+ const rows = (fileIndex: number) =>
398
+ this.#textSource ? this.#textLines.length : (this.#files[fileIndex]?.rows.filter(isSourceRow).length ?? -1);
399
+ const fits = (fileIndex: number, sourceIndex: number, whole: boolean) =>
400
+ rows(fileIndex) >= 0 && (whole ? sourceIndex === -1 : sourceIndex >= 0 && sourceIndex < rows(fileIndex));
401
+ if (draft.kind === "text" && this.#textSource) {
402
+ if (draft.editedText !== undefined) this.replaceTextSource(draft.editedText);
403
+ this.#textAnnotations = draft.annotations.filter(e => fits(0, e.sourceIndex, e.annotation.scope === "text"));
404
+ } else if (draft.kind === "diff" && !this.#textSource) {
405
+ this.#annotations = draft.annotations.filter(e =>
406
+ fits(e.fileIndex, e.sourceIndex, e.annotation.scope === "file"),
407
+ );
408
+ } else return;
409
+ const editor = draft.editor;
410
+ const fileIndex = editor && "fileIndex" in editor ? editor.fileIndex : 0;
411
+ if (editor && fits(fileIndex, editor.sourceIndex, editor.scope === "text" || editor.scope === "file")) {
412
+ this.#fileIndex = fileIndex;
413
+ if (editor.sourceIndex >= 0) this.#sourceIndex = editor.sourceIndex;
414
+ this.#annotationScope = editor.scope;
415
+ const count = this.#textSource ? this.#textAnnotations.length : this.#annotations.length;
416
+ this.#editingAnnotationIndex =
417
+ (editor.editingAnnotationIndex ?? count) < count ? editor.editingAnnotationIndex : undefined;
418
+ this.#annotating = true;
419
+ this.#editor.setText(editor.text);
420
+ }
421
+ this.#annotationRev++;
422
+ this.#nativeBody = undefined;
423
+ }
424
+
360
425
  getAnnotations(): CodeReviewAnnotation[] {
361
426
  return this.#annotations.map(entry => ({ ...entry.annotation }));
362
427
  }
@@ -422,6 +487,8 @@ export class AnnotationOverlay implements Focusable {
422
487
 
423
488
  handleInput(data: string): void {
424
489
  if (this.#finished || this.#externalOperation) return;
490
+ const discardArmed = this.#discardArmed;
491
+ this.#discardArmed = false;
425
492
  if (this.#annotationChooser) {
426
493
  this.#handleAnnotationChooser(data);
427
494
  return;
@@ -431,8 +498,9 @@ export class AnnotationOverlay implements Focusable {
431
498
  void this.#openAnnotationEditor();
432
499
  return;
433
500
  }
501
+ // Close, keeping the unfinished note in the draft so it reopens in the editor.
434
502
  if (this.#keybindings.matches(data, "tui.select.cancel")) {
435
- this.#cancelAnnotation();
503
+ this.#finish(undefined);
436
504
  return;
437
505
  }
438
506
  this.#editor.handleInput(data);
@@ -476,7 +544,7 @@ export class AnnotationOverlay implements Focusable {
476
544
  }
477
545
  if (this.#focus === "files") this.#handleFiles(data);
478
546
  else if (this.#focus === "diff") this.#handleDiff(data);
479
- else this.#handleActions(data);
547
+ else this.#handleActions(data, discardArmed);
480
548
  }
481
549
 
482
550
  /** Primary key bound to `action` in this overlay's keybindings, formatted for footer hints. */
@@ -495,10 +563,9 @@ export class AnnotationOverlay implements Focusable {
495
563
  }
496
564
  }
497
565
 
498
- /** Esc, by state: leave the note chooser, drop the note draft, else close the review. */
566
+ /** Esc, by state: leave the note chooser, else close the review; the host keeps the draft. */
499
567
  #escape(): void {
500
568
  if (this.#annotationChooser) this.#annotationChooser = undefined;
501
- else if (this.#annotating) this.#cancelAnnotation();
502
569
  else this.#finish(undefined);
503
570
  }
504
571
 
@@ -602,33 +669,41 @@ export class AnnotationOverlay implements Focusable {
602
669
  }
603
670
  }
604
671
 
605
- #handleActions(data: string): void {
606
- const actionCount = this.#actions.length;
607
- if (actionCount === 0) return;
608
- const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
609
- if (this.#keybindings.matches(data, "tui.select.up") || matchesKey(data, "k")) {
610
- this.#actionIndex = this.#textSource ? (this.#actionIndex - 1 + actionCount) % actionCount : 0;
611
- return;
612
- }
613
- if (this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
614
- if (this.#textSource) {
615
- this.#actionIndex = (this.#actionIndex + 1) % actionCount;
616
- } else if (hasAnnotations) {
617
- this.#actionIndex = Math.min(actionCount - 1, this.#actionIndex + 1);
672
+ #handleActions(data: string, discardArmed: boolean): void {
673
+ const up = this.#keybindings.matches(data, "tui.select.up") || matchesKey(data, "k");
674
+ if (up || this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
675
+ const step = up ? -1 : 1;
676
+ for (let index = this.#actionIndex + step; index >= 0 && index < this.#actions.length; index += step) {
677
+ if (this.#actionDisabled(index)) continue;
678
+ this.#actionIndex = index;
679
+ break;
618
680
  }
619
681
  return;
620
682
  }
621
- if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction();
683
+ if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction(discardArmed);
622
684
  }
623
685
 
624
686
  #actionDisabled(index: number): boolean {
687
+ if (this.#actions[index] === DISCARD_ANNOTATIONS_ACTION) return false;
625
688
  const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
626
689
  return this.#textSource ? !hasAnnotations : index === 1 && !hasAnnotations;
627
690
  }
628
691
 
629
- #confirmAction(): void {
692
+ #confirmAction(discardArmed = false): void {
630
693
  if (this.#actionDisabled(this.#actionIndex)) return;
631
- if (this.#textSource) {
694
+ if (this.#actions[this.#actionIndex] === DISCARD_ANNOTATIONS_ACTION) {
695
+ if (!discardArmed) {
696
+ this.#discardArmed = true;
697
+ return;
698
+ }
699
+ this.#annotations = [];
700
+ this.#textAnnotations = [];
701
+ this.#textSourceEdited = false;
702
+ this.#annotating = false;
703
+ this.#editingAnnotationIndex = undefined;
704
+ this.#editor.setText("");
705
+ this.#finish(undefined);
706
+ } else if (this.#textSource) {
632
707
  this.#finish({
633
708
  action: "paste",
634
709
  annotations: this.getTextAnnotations(),
@@ -721,12 +796,6 @@ export class AnnotationOverlay implements Focusable {
721
796
  this.#editor.setText(existing?.annotation.note ?? "");
722
797
  }
723
798
 
724
- #cancelAnnotation(): void {
725
- this.#annotating = false;
726
- this.#editingAnnotationIndex = undefined;
727
- this.#editor.setText("");
728
- }
729
-
730
799
  #commitAnnotation(value: string): void {
731
800
  if (this.#textSource) {
732
801
  this.#commitTextAnnotation(value);
@@ -925,7 +994,6 @@ export class AnnotationOverlay implements Focusable {
925
994
  this.#annotationRev++;
926
995
  this.#annotations = snapshot.annotations;
927
996
  this.#textAnnotations = snapshot.textAnnotations;
928
- if (this.#annotations.length === 0 && this.#actionIndex === 1) this.#actionIndex = 0;
929
997
  }
930
998
 
931
999
  async #openAnnotationEditor(): Promise<void> {
@@ -1146,9 +1214,10 @@ export class AnnotationOverlay implements Focusable {
1146
1214
  }
1147
1215
 
1148
1216
  #renderActions(): string[] {
1149
- const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
1150
- return this.#actions.map((label, index) => {
1151
- const disabled = this.#textSource ? !hasAnnotations : index === 1 && !hasAnnotations;
1217
+ return this.#actions.map((action, index) => {
1218
+ const label =
1219
+ action === DISCARD_ANNOTATIONS_ACTION && this.#discardArmed ? "Confirm: discard all notes" : action;
1220
+ const disabled = this.#actionDisabled(index);
1152
1221
  const selected = index === this.#actionIndex;
1153
1222
  const cursor = selected ? `${this.#theme.nav.cursor} ` : " ";
1154
1223
  const text = disabled
@@ -1224,7 +1293,7 @@ export class AnnotationOverlay implements Focusable {
1224
1293
  const hints = [
1225
1294
  `${editorKey("tui.input.submit")} save`,
1226
1295
  `${editorKey("tui.input.newLine")} newline`,
1227
- `${this.#key("tui.select.cancel")} cancel`,
1296
+ `${this.#key("tui.select.cancel")} close, keep note`,
1228
1297
  ];
1229
1298
  hints.push(`${formatKeyHint(appExternalEditorKey())} editor`);
1230
1299
  return [caption, ...this.#editor.render(width), this.#theme.fg("dim", hints.join(" · "))];
@@ -1428,6 +1497,7 @@ export class AnnotationOverlay implements Focusable {
1428
1497
  this.#bodyHeight,
1429
1498
  this.#annotating ? `annotating:${this.#annotationScope}:${this.#editingAnnotationIndex}` : "",
1430
1499
  chooser ? `chooser:${chooser.selected}:${chooser.entries.join()}` : "",
1500
+ this.#discardArmed ? "discard-armed" : "",
1431
1501
  ].join("|");
1432
1502
  if (this.#nativeRoot && sig === this.#nativeSig && body === this.#nativeRootBody) return this.#nativeRoot;
1433
1503
 
@@ -1462,6 +1532,8 @@ export class AnnotationOverlay implements Focusable {
1462
1532
 
1463
1533
  handleNativeEvent(event: NativeUiEvent): void {
1464
1534
  if (this.#finished || this.#externalOperation) return;
1535
+ const discardArmed = this.#discardArmed;
1536
+ this.#discardArmed = false;
1465
1537
  if (event.type === "action") {
1466
1538
  if (event.act === "close") {
1467
1539
  this.#escape();
@@ -1470,10 +1542,18 @@ export class AnnotationOverlay implements Focusable {
1470
1542
  // A draft or the note chooser is open: finish or leave it first.
1471
1543
  if (this.#annotating || this.#annotationChooser) return;
1472
1544
  const index =
1473
- event.act === "review" && !this.#textSource ? 0 : event.act === "paste" ? (this.#textSource ? 0 : 1) : -1;
1545
+ event.act === "discard"
1546
+ ? this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)
1547
+ : event.act === "review" && !this.#textSource
1548
+ ? 0
1549
+ : event.act === "paste"
1550
+ ? this.#textSource
1551
+ ? 0
1552
+ : 1
1553
+ : -1;
1474
1554
  if (index < 0 || this.#actionDisabled(index)) return;
1475
1555
  this.#actionIndex = index;
1476
- this.#confirmAction();
1556
+ this.#confirmAction(discardArmed);
1477
1557
  return;
1478
1558
  }
1479
1559
  if (event.type !== "select" && event.type !== "activate") return;
@@ -1824,7 +1904,15 @@ export class AnnotationOverlay implements Focusable {
1824
1904
  mark ? ({ ...button, p: { ...button.p, mark } } as NativeNode) : button;
1825
1905
  const hasNotes = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
1826
1906
  const pasteTitle = hasNotes ? PASTE_CODE_REVIEW_ACTION : "Add a note to paste it into the prompt";
1827
- const buttons: NativeNode[] = [actionButton("Cancel", "close", { keys: "escape" })];
1907
+ const buttons: NativeNode[] = [
1908
+ actionButton("Cancel", "close", { keys: "escape" }),
1909
+ marked(
1910
+ actionButton(this.#discardArmed ? "Confirm discard" : "Discard", "discard", {
1911
+ title: DISCARD_ANNOTATIONS_ACTION,
1912
+ }),
1913
+ pick(this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)),
1914
+ ),
1915
+ ];
1828
1916
  if (this.#textSource) {
1829
1917
  buttons.push(
1830
1918
  marked(actionButton("Paste into prompt", "paste", { tone: "accent", title: pasteTitle }), pick(0)),
@@ -1860,7 +1948,7 @@ export class AnnotationOverlay implements Focusable {
1860
1948
  return [
1861
1949
  this.#hint("tui.input.submit", "save"),
1862
1950
  this.#hint("tui.input.newLine", "new line"),
1863
- this.#hint("tui.select.cancel", "discard"),
1951
+ this.#hint("tui.select.cancel", "close, keep note"),
1864
1952
  this.#callbacks.onAnnotationExternalEditor ? key(appExternalEditorKey(), "editor") : undefined,
1865
1953
  ];
1866
1954
  }
@@ -78,3 +78,27 @@ export interface TextReviewOverlayResult {
78
78
  /** Source text after an external-editor edit; absent when the source was not edited. */
79
79
  editedText?: string;
80
80
  }
81
+
82
+ export type AnnotationOverlayDraft =
83
+ | {
84
+ kind: "diff";
85
+ annotations: { fileIndex: number; sourceIndex: number; annotation: CodeReviewAnnotation }[];
86
+ editor?: {
87
+ fileIndex: number;
88
+ sourceIndex: number;
89
+ scope: "file" | "line";
90
+ editingAnnotationIndex?: number;
91
+ text: string;
92
+ };
93
+ }
94
+ | {
95
+ kind: "text";
96
+ annotations: { sourceIndex: number; annotation: TextReviewAnnotation }[];
97
+ editedText?: string;
98
+ editor?: {
99
+ sourceIndex: number;
100
+ scope: "text" | "line";
101
+ editingAnnotationIndex?: number;
102
+ text: string;
103
+ };
104
+ };
@@ -647,9 +647,9 @@ export class HookSelectorComponent extends OverlayPanel {
647
647
  return;
648
648
  }
649
649
 
650
- if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "k"))) {
650
+ if (matchesSelectUp(keyData)) {
651
651
  this.#moveSelection(-1);
652
- } else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "j"))) {
652
+ } else if (matchesSelectDown(keyData)) {
653
653
  this.#moveSelection(1);
654
654
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
655
655
  const selected = this.#menu.selectedItem;
@@ -64,7 +64,9 @@ function urlPlace(url: string): string {
64
64
  export class LoginDialogComponent extends OverlayPanel {
65
65
  #contentContainer: Container;
66
66
  #input: TextFormField;
67
- #tui: TUI;
67
+ #tui: Pick<TUI, "requestRender">;
68
+ #focused = false;
69
+ #useTerminalCursor = false;
68
70
  #onComplete: (success: boolean, message?: string) => void;
69
71
  #openUrl: (url: string) => void;
70
72
  #abortController = new AbortController();
@@ -78,7 +80,7 @@ export class LoginDialogComponent extends OverlayPanel {
78
80
  #nativeRoot: NativeNode | undefined;
79
81
 
80
82
  constructor(
81
- tui: TUI,
83
+ tui: Pick<TUI, "requestRender">,
82
84
  providerId: string,
83
85
  onComplete: (success: boolean, message?: string) => void,
84
86
  openUrl: (url: string) => void,
@@ -98,8 +100,29 @@ export class LoginDialogComponent extends OverlayPanel {
98
100
  this.#input = this.#createInput();
99
101
  }
100
102
 
103
+ /** Focus reaches the live field: it draws the caret (the hardware cursor when the terminal's is on). */
104
+ get focused(): boolean {
105
+ return this.#focused;
106
+ }
107
+
108
+ set focused(value: boolean) {
109
+ this.#focused = value;
110
+ this.#input.focused = value;
111
+ }
112
+
113
+ setUseTerminalCursor(useTerminalCursor: boolean): void {
114
+ this.#useTerminalCursor = useTerminalCursor;
115
+ this.#input.input.setUseTerminalCursor(useTerminalCursor);
116
+ }
117
+
118
+ /** A fresh field for the next answer; the previous one stops drawing a caret. */
119
+ #replaceInput(secret = false): void {
120
+ this.#input.focused = false;
121
+ this.#input = this.#createInput(secret);
122
+ }
123
+
101
124
  #createInput(secret = false): TextFormField {
102
- return new TextFormField({
125
+ const field = new TextFormField({
103
126
  theme: formTheme,
104
127
  secret,
105
128
  empty: "submit",
@@ -116,6 +139,9 @@ export class LoginDialogComponent extends OverlayPanel {
116
139
  },
117
140
  requestRender: () => this.#tui.requestRender(),
118
141
  });
142
+ field.focused = this.#focused;
143
+ field.input.setUseTerminalCursor(this.#useTerminalCursor);
144
+ return field;
119
145
  }
120
146
 
121
147
  get signal(): AbortSignal {
@@ -194,7 +220,7 @@ export class LoginDialogComponent extends OverlayPanel {
194
220
  // Keep retry chrome in place, but discard prior prompt undo/kill history.
195
221
  const mounted = this.#contentContainer.children.indexOf(this.#input);
196
222
  const previousInput = this.#input;
197
- this.#input = this.#createInput();
223
+ this.#replaceInput();
198
224
  const nativeMounted = this.#nativeSteps.find(step => step.kind === "input" && step.field === previousInput);
199
225
  if (mounted !== -1) {
200
226
  // The flow asks again only when the last paste carried no usable code.
@@ -247,7 +273,7 @@ export class LoginDialogComponent extends OverlayPanel {
247
273
  if (answered?.kind === "input") answered.answer = value;
248
274
  }
249
275
  // A new prompt must not recover a previous secret through undo or yank.
250
- this.#input = this.#createInput(prompt.secret === true);
276
+ this.#replaceInput(prompt.secret === true);
251
277
  if (prompt.placeholder) this.#input.input.placeholder = plainText(prompt.placeholder);
252
278
  this.#contentContainer.addChild(new Spacer(1));
253
279
  this.#contentContainer.addChild(new Text(theme.fg("text", prompt.message), 0, 0));