@bastani/atomic 0.8.4 → 0.8.5

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 +12 -0
  2. package/README.md +24 -23
  3. package/dist/builtin/intercom/README.md +5 -5
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +19 -1
  7. package/dist/builtin/intercom/ui/session-list.ts +19 -1
  8. package/dist/builtin/mcp/README.md +3 -3
  9. package/dist/builtin/mcp/commands.ts +1 -1
  10. package/dist/builtin/mcp/host-html-template.ts +1 -1
  11. package/dist/builtin/mcp/mcp-panel.ts +14 -14
  12. package/dist/builtin/mcp/mcp-setup-panel.ts +4 -4
  13. package/dist/builtin/mcp/package.json +1 -1
  14. package/dist/builtin/mcp/tool-result-renderer.ts +1 -1
  15. package/dist/builtin/subagents/README.md +3 -3
  16. package/dist/builtin/subagents/package.json +1 -1
  17. package/dist/builtin/subagents/src/tui/render.ts +1844 -1062
  18. package/dist/builtin/web-access/README.md +1 -1
  19. package/dist/builtin/web-access/curator-page.ts +2 -2
  20. package/dist/builtin/web-access/index.ts +1 -1
  21. package/dist/builtin/web-access/package.json +1 -1
  22. package/dist/builtin/workflows/README.md +34 -7
  23. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +23 -4
  24. package/dist/builtin/workflows/builtin/ralph.ts +1 -1
  25. package/dist/builtin/workflows/package.json +1 -1
  26. package/dist/builtin/workflows/skills/workflow/SKILL.md +75 -16
  27. package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +34 -11
  28. package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +111 -20
  29. package/dist/builtin/workflows/src/extension/discovery.ts +32 -4
  30. package/dist/builtin/workflows/src/extension/index.ts +347 -63
  31. package/dist/builtin/workflows/src/extension/render-call.ts +3 -1
  32. package/dist/builtin/workflows/src/extension/render-result.ts +7 -0
  33. package/dist/builtin/workflows/src/extension/runtime.ts +4 -2
  34. package/dist/builtin/workflows/src/extension/wiring.ts +32 -8
  35. package/dist/builtin/workflows/src/extension/workflow-schema.ts +36 -14
  36. package/dist/builtin/workflows/src/runs/background/runner.ts +2 -2
  37. package/dist/builtin/workflows/src/runs/background/status.ts +89 -0
  38. package/dist/builtin/workflows/src/runs/foreground/executor.ts +338 -78
  39. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +2 -0
  40. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +55 -7
  41. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +146 -10
  42. package/dist/builtin/workflows/src/shared/store.ts +29 -0
  43. package/dist/builtin/workflows/src/shared/types.ts +25 -4
  44. package/dist/builtin/workflows/src/tui/graph-canvas.ts +69 -2
  45. package/dist/builtin/workflows/src/tui/graph-view.ts +97 -182
  46. package/dist/builtin/workflows/src/tui/header.ts +36 -20
  47. package/dist/builtin/workflows/src/tui/inline-form-card.ts +129 -46
  48. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +111 -36
  49. package/dist/builtin/workflows/src/tui/inputs-picker.ts +311 -91
  50. package/dist/builtin/workflows/src/tui/layout.ts +1 -1
  51. package/dist/builtin/workflows/src/tui/node-card.ts +66 -37
  52. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +20 -6
  53. package/dist/builtin/workflows/src/tui/prompt-card.ts +262 -85
  54. package/dist/builtin/workflows/src/tui/run-detail.ts +50 -31
  55. package/dist/builtin/workflows/src/tui/session-confirm.ts +21 -14
  56. package/dist/builtin/workflows/src/tui/session-picker.ts +35 -26
  57. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +531 -960
  58. package/dist/builtin/workflows/src/tui/status-helpers.ts +6 -0
  59. package/dist/builtin/workflows/src/tui/status-list.ts +8 -4
  60. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -2
  61. package/dist/builtin/workflows/src/tui/switcher.ts +55 -25
  62. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +33 -1
  63. package/dist/builtin/workflows/src/tui/workflow-list.ts +10 -6
  64. package/dist/cli/args.d.ts.map +1 -1
  65. package/dist/cli/args.js +1 -1
  66. package/dist/cli/args.js.map +1 -1
  67. package/dist/config.d.ts.map +1 -1
  68. package/dist/config.js +20 -6
  69. package/dist/config.js.map +1 -1
  70. package/dist/core/agent-session-services.d.ts +3 -3
  71. package/dist/core/agent-session-services.d.ts.map +1 -1
  72. package/dist/core/agent-session-services.js.map +1 -1
  73. package/dist/core/agent-session.d.ts +7 -7
  74. package/dist/core/agent-session.d.ts.map +1 -1
  75. package/dist/core/agent-session.js.map +1 -1
  76. package/dist/core/compaction/branch-summarization.d.ts +2 -2
  77. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  78. package/dist/core/compaction/branch-summarization.js.map +1 -1
  79. package/dist/core/compaction/compaction.d.ts +3 -3
  80. package/dist/core/compaction/compaction.d.ts.map +1 -1
  81. package/dist/core/compaction/compaction.js.map +1 -1
  82. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  83. package/dist/core/export-html/tool-renderer.js.map +1 -1
  84. package/dist/core/extensions/loader.d.ts +3 -2
  85. package/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/dist/core/extensions/loader.js +24 -12
  87. package/dist/core/extensions/loader.js.map +1 -1
  88. package/dist/core/extensions/runner.d.ts.map +1 -1
  89. package/dist/core/extensions/runner.js +6 -0
  90. package/dist/core/extensions/runner.js.map +1 -1
  91. package/dist/core/extensions/types.d.ts +28 -17
  92. package/dist/core/extensions/types.d.ts.map +1 -1
  93. package/dist/core/extensions/types.js.map +1 -1
  94. package/dist/core/package-manager.d.ts +1 -0
  95. package/dist/core/package-manager.d.ts.map +1 -1
  96. package/dist/core/package-manager.js +65 -28
  97. package/dist/core/package-manager.js.map +1 -1
  98. package/dist/core/resource-loader.d.ts.map +1 -1
  99. package/dist/core/resource-loader.js +13 -5
  100. package/dist/core/resource-loader.js.map +1 -1
  101. package/dist/core/sdk.d.ts +3 -3
  102. package/dist/core/sdk.d.ts.map +1 -1
  103. package/dist/core/sdk.js.map +1 -1
  104. package/dist/core/session-manager.d.ts.map +1 -1
  105. package/dist/core/session-manager.js +1 -1
  106. package/dist/core/session-manager.js.map +1 -1
  107. package/dist/core/settings-manager.d.ts +2 -0
  108. package/dist/core/settings-manager.d.ts.map +1 -1
  109. package/dist/core/settings-manager.js.map +1 -1
  110. package/dist/core/slash-commands.d.ts.map +1 -1
  111. package/dist/core/slash-commands.js +1 -1
  112. package/dist/core/slash-commands.js.map +1 -1
  113. package/dist/core/system-prompt.d.ts.map +1 -1
  114. package/dist/core/system-prompt.js +5 -3
  115. package/dist/core/system-prompt.js.map +1 -1
  116. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts +1 -1
  117. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts.map +1 -1
  118. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js +1 -1
  119. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js.map +1 -1
  120. package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts +8 -8
  121. package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts.map +1 -1
  122. package/dist/core/tools/ask-user-question/view/dialog-builder.js +6 -6
  123. package/dist/core/tools/ask-user-question/view/dialog-builder.js.map +1 -1
  124. package/dist/core/tools/bash.d.ts.map +1 -1
  125. package/dist/core/tools/bash.js +1 -1
  126. package/dist/core/tools/bash.js.map +1 -1
  127. package/dist/core/tools/find.d.ts.map +1 -1
  128. package/dist/core/tools/find.js +1 -1
  129. package/dist/core/tools/find.js.map +1 -1
  130. package/dist/core/tools/grep.d.ts.map +1 -1
  131. package/dist/core/tools/grep.js +7 -4
  132. package/dist/core/tools/grep.js.map +1 -1
  133. package/dist/core/tools/index.d.ts +3 -2
  134. package/dist/core/tools/index.d.ts.map +1 -1
  135. package/dist/core/tools/index.js.map +1 -1
  136. package/dist/core/tools/ls.d.ts.map +1 -1
  137. package/dist/core/tools/ls.js +3 -2
  138. package/dist/core/tools/ls.js.map +1 -1
  139. package/dist/core/tools/read.d.ts.map +1 -1
  140. package/dist/core/tools/read.js +2 -2
  141. package/dist/core/tools/read.js.map +1 -1
  142. package/dist/core/tools/render-utils.d.ts +2 -1
  143. package/dist/core/tools/render-utils.d.ts.map +1 -1
  144. package/dist/core/tools/render-utils.js.map +1 -1
  145. package/dist/core/tools/todos.d.ts.map +1 -1
  146. package/dist/core/tools/todos.js +1 -1
  147. package/dist/core/tools/todos.js.map +1 -1
  148. package/dist/core/tools/tool-definition-wrapper.d.ts +4 -3
  149. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  150. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  151. package/dist/core/tools/write.d.ts.map +1 -1
  152. package/dist/core/tools/write.js +1 -1
  153. package/dist/core/tools/write.js.map +1 -1
  154. package/dist/index.d.ts +2 -1
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js +2 -1
  157. package/dist/index.js.map +1 -1
  158. package/dist/main.d.ts.map +1 -1
  159. package/dist/main.js +2 -2
  160. package/dist/main.js.map +1 -1
  161. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/assistant-message.js +3 -3
  163. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  164. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  165. package/dist/modes/interactive/components/bash-execution.js +3 -3
  166. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  167. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  169. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  170. package/dist/modes/interactive/components/chat-message-renderer.d.ts +2 -1
  171. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  172. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  173. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  175. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  176. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/config-selector.js +1 -1
  178. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/custom-editor.d.ts +3 -0
  180. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/custom-editor.js +13 -3
  182. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  183. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/footer.js +1 -1
  185. package/dist/modes/interactive/components/footer.js.map +1 -1
  186. package/dist/modes/interactive/components/index.d.ts +2 -1
  187. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/index.js +2 -1
  189. package/dist/modes/interactive/components/index.js.map +1 -1
  190. package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
  191. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  192. package/dist/modes/interactive/components/keybinding-hints.js +47 -5
  193. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  194. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/login-dialog.js +5 -5
  196. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  197. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  198. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  200. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  201. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/scoped-models-selector.js +7 -7
  203. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  204. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  205. package/dist/modes/interactive/components/session-selector.js +8 -8
  206. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  207. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  208. package/dist/modes/interactive/components/settings-selector.js +3 -3
  209. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  210. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  212. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  213. package/dist/modes/interactive/components/tool-execution.d.ts +10 -12
  214. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  215. package/dist/modes/interactive/components/tool-execution.js +3 -3
  216. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  217. package/dist/modes/interactive/components/working-status.d.ts +25 -0
  218. package/dist/modes/interactive/components/working-status.d.ts.map +1 -0
  219. package/dist/modes/interactive/components/working-status.js +28 -0
  220. package/dist/modes/interactive/components/working-status.js.map +1 -0
  221. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  222. package/dist/modes/interactive/interactive-mode.js +8 -7
  223. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  225. package/dist/modes/rpc/rpc-mode.js +8 -0
  226. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  227. package/dist/modes/rpc/rpc-types.d.ts +5 -5
  228. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-types.js.map +1 -1
  230. package/dist/utils/tools-manager.d.ts.map +1 -1
  231. package/dist/utils/tools-manager.js.map +1 -1
  232. package/docs/development.md +2 -2
  233. package/docs/extensions.md +7 -7
  234. package/docs/packages.md +11 -8
  235. package/docs/quickstart.md +2 -2
  236. package/docs/rpc.md +1 -1
  237. package/docs/sdk.md +14 -11
  238. package/docs/session-format.md +1 -1
  239. package/docs/sessions.md +10 -10
  240. package/docs/settings.md +1 -1
  241. package/docs/terminal-setup.md +9 -9
  242. package/docs/tmux.md +10 -10
  243. package/docs/tui.md +2 -2
  244. package/docs/usage.md +9 -9
  245. package/package.json +6 -1
@@ -25,10 +25,19 @@
25
25
  * src/tui/graph-view.ts overlay integration + key routing
26
26
  */
27
27
 
28
+ import { keyHint, keyText, rawKeyHint } from "@bastani/atomic";
29
+ import {
30
+ SelectList,
31
+ truncateToWidth,
32
+ visibleWidth,
33
+ wrapTextWithAnsi,
34
+ type SelectItem,
35
+ type SelectListTheme,
36
+ } from "@earendil-works/pi-tui";
28
37
  import type { PendingPrompt } from "../shared/store-types.js";
29
38
  import type { GraphTheme } from "./graph-theme.js";
30
39
  import { hexToAnsi, hexBg, paint, RESET, BOLD } from "./color-utils.js";
31
- import { matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
40
+ import { matchesKey } from "./text-helpers.js";
32
41
 
33
42
  // ---------------------------------------------------------------------------
34
43
  // State
@@ -49,6 +58,8 @@ export interface PromptCardState {
49
58
  selectedIndex: number;
50
59
  /** Boolean selection for `confirm` prompts (true = yes, false = no). */
51
60
  confirmValue: boolean;
61
+ /** For multi-line editor prompts, Tab moves focus to a visible Submit action. */
62
+ editorSubmitFocused: boolean;
52
63
  }
53
64
 
54
65
  export function createPromptCardState(prompt: PendingPrompt): PromptCardState {
@@ -59,6 +70,7 @@ export function createPromptCardState(prompt: PendingPrompt): PromptCardState {
59
70
  caret: initial.length,
60
71
  selectedIndex: 0,
61
72
  confirmValue: false,
73
+ editorSubmitFocused: false,
62
74
  };
63
75
  }
64
76
 
@@ -87,7 +99,7 @@ export function handlePromptCardInput(
87
99
  data: string,
88
100
  state: PromptCardState,
89
101
  ): PromptCardAction {
90
- if (data === "\x1b") {
102
+ if (data === "\x03" || matchesKey(data, "escape")) {
91
103
  return { kind: "cancel" };
92
104
  }
93
105
 
@@ -131,18 +143,15 @@ function handleSelect(data: string, state: PromptCardState): PromptCardAction {
131
143
  }
132
144
  return { kind: "noop" };
133
145
  }
134
- if (matchesKey(data, "down") || data === "\x1b[B" || matchesKey(data, "right") || data === "\x1b[C") {
135
- state.selectedIndex = (state.selectedIndex + 1) % choices.length;
136
- return { kind: "noop" };
137
- }
138
- if (matchesKey(data, "up") || data === "\x1b[A" || matchesKey(data, "left") || data === "\x1b[D") {
139
- state.selectedIndex = (state.selectedIndex - 1 + choices.length) % choices.length;
140
- return { kind: "noop" };
141
- }
142
- if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
143
- return { kind: "submit", response: choices[state.selectedIndex] ?? choices[0] };
144
- }
145
- return { kind: "noop" };
146
+
147
+ let action: PromptCardAction = { kind: "noop" };
148
+ const list = createPromptSelectList(state);
149
+ list.onSelect = (item) => {
150
+ const idx = Number(item.value);
151
+ action = { kind: "submit", response: choices[idx] ?? choices[0] };
152
+ };
153
+ list.handleInput(normalizeSelectKeyData(data));
154
+ return action;
146
155
  }
147
156
 
148
157
  function handleInput(data: string, state: PromptCardState): PromptCardAction {
@@ -153,11 +162,15 @@ function handleInput(data: string, state: PromptCardState): PromptCardAction {
153
162
  }
154
163
 
155
164
  function handleEditor(data: string, state: PromptCardState): PromptCardAction {
156
- // ctrl+s submits multi-line editor content; bare enter inserts a newline
157
- // (mirrors pi.ui.editor's "save with ctrl+s" affordance documented on the
158
- // chat editor's status hints).
159
- if (data === "\x13") {
160
- return { kind: "submit", response: state.rawText };
165
+ if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
166
+ state.editorSubmitFocused = !state.editorSubmitFocused;
167
+ return { kind: "noop" };
168
+ }
169
+ if (state.editorSubmitFocused) {
170
+ if (matchesKey(data, "enter")) {
171
+ return { kind: "submit", response: state.rawText };
172
+ }
173
+ return { kind: "noop" };
161
174
  }
162
175
  if (data === "\r" || data === "\n") {
163
176
  state.rawText = state.rawText.slice(0, state.caret) + "\n" + state.rawText.slice(state.caret);
@@ -172,30 +185,75 @@ function applyTextEdit(
172
185
  state: PromptCardState,
173
186
  ): PromptCardAction {
174
187
  if (data === "\x1b[D") {
175
- state.caret = Math.max(0, state.caret - 1);
188
+ state.caret = previousGraphemeBoundary(state.rawText, state.caret);
176
189
  return { kind: "noop" };
177
190
  }
178
191
  if (data === "\x1b[C") {
179
- state.caret = Math.min(state.rawText.length, state.caret + 1);
192
+ state.caret = nextGraphemeBoundary(state.rawText, state.caret);
180
193
  return { kind: "noop" };
181
194
  }
182
195
  if (data === "\x7f" || data === "\b") {
183
196
  if (state.caret > 0) {
184
- state.rawText =
185
- state.rawText.slice(0, state.caret - 1) + state.rawText.slice(state.caret);
186
- state.caret -= 1;
197
+ const prev = previousGraphemeBoundary(state.rawText, state.caret);
198
+ state.rawText = state.rawText.slice(0, prev) + state.rawText.slice(state.caret);
199
+ state.caret = prev;
187
200
  }
188
201
  return { kind: "noop" };
189
202
  }
190
- if (data.length === 1 && data >= " " && data <= "~") {
203
+ if (isPrintableText(data)) {
191
204
  state.rawText =
192
205
  state.rawText.slice(0, state.caret) + data + state.rawText.slice(state.caret);
193
- state.caret += 1;
206
+ state.caret += data.length;
194
207
  return { kind: "noop" };
195
208
  }
196
209
  return { kind: "noop" };
197
210
  }
198
211
 
212
+ interface GraphemePart {
213
+ text: string;
214
+ start: number;
215
+ end: number;
216
+ width: number;
217
+ }
218
+
219
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
220
+
221
+ function graphemeParts(value: string): GraphemePart[] {
222
+ const parts: GraphemePart[] = [];
223
+ for (const segment of segmenter.segment(value)) {
224
+ const text = segment.segment;
225
+ parts.push({
226
+ text,
227
+ start: segment.index,
228
+ end: segment.index + text.length,
229
+ width: visibleWidth(text),
230
+ });
231
+ }
232
+ return parts;
233
+ }
234
+
235
+ function previousGraphemeBoundary(value: string, caret: number): number {
236
+ const safeCaret = Math.max(0, Math.min(caret, value.length));
237
+ let prev = 0;
238
+ for (const part of graphemeParts(value)) {
239
+ if (part.start >= safeCaret) break;
240
+ prev = part.start;
241
+ }
242
+ return prev;
243
+ }
244
+
245
+ function nextGraphemeBoundary(value: string, caret: number): number {
246
+ const safeCaret = Math.max(0, Math.min(caret, value.length));
247
+ for (const part of graphemeParts(value)) {
248
+ if (part.end > safeCaret) return part.end;
249
+ }
250
+ return value.length;
251
+ }
252
+
253
+ function isPrintableText(data: string): boolean {
254
+ return data.length > 0 && !data.startsWith("\x1b") && !/[\x00-\x1f\x7f]/.test(data);
255
+ }
256
+
199
257
  /**
200
258
  * Compute the safe default response when the user dismisses the prompt.
201
259
  * Used by the overlay to keep the workflow body unblocked even on cancel.
@@ -212,6 +270,76 @@ export function defaultResponseFor(prompt: PendingPrompt): unknown {
212
270
  }
213
271
  }
214
272
 
273
+ // ---------------------------------------------------------------------------
274
+ // Select-list bridge
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function createPromptSelectList(
278
+ state: PromptCardState,
279
+ theme?: GraphTheme,
280
+ maxVisible = 5,
281
+ ): SelectList {
282
+ const choices = state.prompt.choices ?? [];
283
+ const items: SelectItem[] = choices.map((choice, idx) => ({
284
+ value: String(idx),
285
+ label: choice,
286
+ }));
287
+ const list = new SelectList(
288
+ items,
289
+ Math.max(1, Math.min(maxVisible, choices.length || 1)),
290
+ createSelectListTheme(theme),
291
+ {
292
+ minPrimaryColumnWidth: 1,
293
+ maxPrimaryColumnWidth: 80,
294
+ truncatePrimary: ({ text, maxWidth, isSelected }) => {
295
+ const clipped = truncateToWidth(text, maxWidth, "");
296
+ if (!theme) return clipped;
297
+ return paint(clipped, isSelected ? theme.text : theme.dim, { bold: isSelected });
298
+ },
299
+ },
300
+ );
301
+ const selectedIndex = normalizeSelectIndex(state.selectedIndex, choices.length);
302
+ list.setSelectedIndex(selectedIndex);
303
+ list.onSelectionChange = (item) => {
304
+ state.selectedIndex = normalizeSelectIndex(Number(item.value), choices.length);
305
+ };
306
+ return list;
307
+ }
308
+
309
+ function createSelectListTheme(theme?: GraphTheme): SelectListTheme {
310
+ if (!theme) {
311
+ return {
312
+ selectedPrefix: (text) => text,
313
+ selectedText: (text) => text,
314
+ description: (text) => text,
315
+ scrollInfo: (text) => text,
316
+ noMatch: (text) => text,
317
+ };
318
+ }
319
+ return {
320
+ selectedPrefix: (text) => paint(text, theme.accent, { bold: true }),
321
+ selectedText: (text) => paint(text, theme.text, { bold: true }),
322
+ description: (text) => paint(text, theme.textMuted),
323
+ scrollInfo: (text) => paint(text, theme.dim),
324
+ noMatch: (text) => paint(text, theme.dim),
325
+ };
326
+ }
327
+
328
+ function normalizeSelectIndex(index: number, length: number): number {
329
+ if (length <= 0) return 0;
330
+ const n = Number.isFinite(index) ? Math.trunc(index) : 0;
331
+ return ((n % length) + length) % length;
332
+ }
333
+
334
+ function normalizeSelectKeyData(data: string): string {
335
+ // The historical prompt card accepted left/right as select aliases; feed the
336
+ // corresponding vertical key into pi-tui's SelectList so it owns the actual
337
+ // wrap/clamp/selection update behavior.
338
+ if (matchesKey(data, "right") || data === "\x1b[C") return "\x1b[B";
339
+ if (matchesKey(data, "left") || data === "\x1b[D") return "\x1b[A";
340
+ return data;
341
+ }
342
+
215
343
  // ---------------------------------------------------------------------------
216
344
  // Rendering
217
345
  // ---------------------------------------------------------------------------
@@ -293,25 +421,7 @@ function makePaddedRow(
293
421
 
294
422
  function wrapText(text: string, width: number): string[] {
295
423
  if (width <= 0) return [text];
296
- const out: string[] = [];
297
- for (const paragraph of text.split("\n")) {
298
- if (paragraph.length === 0) {
299
- out.push("");
300
- continue;
301
- }
302
- let remaining = paragraph;
303
- while (visibleWidth(remaining) > width) {
304
- // Break on the last whitespace within `width` cells; fall back to a
305
- // hard cut so glyphs longer than `width` don't run off the card.
306
- const slice = remaining.slice(0, width);
307
- const lastSpace = slice.lastIndexOf(" ");
308
- const cut = lastSpace > 0 ? lastSpace : width;
309
- out.push(remaining.slice(0, cut));
310
- remaining = remaining.slice(cut).replace(/^\s+/, "");
311
- }
312
- out.push(remaining);
313
- }
314
- return out;
424
+ return wrapTextWithAnsi(text, width);
315
425
  }
316
426
 
317
427
  function renderResponseField(
@@ -324,7 +434,7 @@ function renderResponseField(
324
434
  case "confirm":
325
435
  return [renderConfirmRow(state, theme, usable)];
326
436
  case "select":
327
- return [renderSelectRow(state, theme, usable)];
437
+ return renderSelectRows(state, theme, usable);
328
438
  case "input":
329
439
  return [renderInputRow(state, theme, usable, cursorOn)];
330
440
  case "editor":
@@ -350,23 +460,18 @@ function renderConfirmRow(
350
460
  return padToUsable(row, usable);
351
461
  }
352
462
 
353
- function renderSelectRow(
463
+ function renderSelectRows(
354
464
  state: PromptCardState,
355
465
  theme: GraphTheme,
356
466
  usable: number,
357
- ): string {
467
+ ): string[] {
358
468
  const choices = state.prompt.choices ?? [];
359
469
  if (choices.length === 0) {
360
- return padToUsable(paint("(no choices)", theme.dim), usable);
470
+ return [padToUsable(paint("(no choices)", theme.dim), usable)];
361
471
  }
362
- const cells = choices.map((choice, idx) => {
363
- const sel = idx === state.selectedIndex;
364
- const marker = sel ? "●" : "○";
365
- const markerColor = sel ? theme.accent : theme.dim;
366
- const textColor = sel ? theme.text : theme.dim;
367
- return paint(marker, markerColor) + " " + paint(choice, textColor, { bold: sel });
368
- });
369
- return padToUsable(cells.join(" "), usable);
472
+ const maxVisible = Math.min(5, choices.length);
473
+ const list = createPromptSelectList(state, theme, maxVisible);
474
+ return list.render(usable).map((line) => padToUsable(line, usable));
370
475
  }
371
476
 
372
477
  function renderInputRow(
@@ -411,7 +516,7 @@ function renderEditorRows(
411
516
  for (let i = 0; i < ROWS; i++) {
412
517
  const lineIdx = safeStart + i;
413
518
  const lineText = allLines[lineIdx] ?? "";
414
- const isCaretLine = lineIdx === caretLine;
519
+ const isCaretLine = !state.editorSubmitFocused && lineIdx === caretLine;
415
520
  const inner = usable - 2;
416
521
  const clipped = clipToCaretWindow(lineText, isCaretLine ? caretCol : Math.min(caretCol, lineText.length), inner);
417
522
  const withCursor = isCaretLine
@@ -420,19 +525,67 @@ function renderEditorRows(
420
525
  const prefix = paint(isCaretLine ? "❯ " : " ", isCaretLine ? theme.accent : theme.dim);
421
526
  rows.push(padToUsable(prefix + withCursor, usable));
422
527
  }
528
+ rows.push(padToUsable(renderEditorSubmitAction(state.editorSubmitFocused, theme), usable));
423
529
  return rows;
424
530
  }
425
531
 
532
+ function renderEditorSubmitAction(focused: boolean, theme: GraphTheme): string {
533
+ const marker = focused ? "❯" : "○";
534
+ return (
535
+ paint(marker, focused ? theme.accent : theme.dim, { bold: focused }) +
536
+ " " +
537
+ paint("Submit response", focused ? theme.text : theme.textMuted, { bold: focused }) +
538
+ paint(" · ", theme.dim) +
539
+ graphKeyHint("tui.input.submit", "submit", theme)
540
+ );
541
+ }
542
+
426
543
  function clipToCaretWindow(
427
544
  value: string,
428
545
  caret: number,
429
546
  windowWidth: number,
430
547
  ): { text: string; caret: number } {
431
- if (value.length <= windowWidth) return { text: value, caret };
432
- // Keep the caret inside the visible window; bias toward showing the tail.
433
- const right = Math.max(caret + 4, windowWidth);
434
- const left = Math.max(0, right - windowWidth);
435
- return { text: value.slice(left, left + windowWidth), caret: caret - left };
548
+ if (windowWidth <= 0) return { text: "", caret: 0 };
549
+ if (visibleWidth(value) <= windowWidth) {
550
+ return { text: value, caret: Math.max(0, Math.min(caret, value.length)) };
551
+ }
552
+
553
+ const parts = graphemeParts(value);
554
+ const safeCaret = Math.max(0, Math.min(caret, value.length));
555
+ const caretPartIndex = parts.findIndex((part) => part.end > safeCaret);
556
+ const caretIndex = caretPartIndex === -1 ? parts.length : caretPartIndex;
557
+
558
+ // Keep the caret visible and bias toward a few cells of look-ahead, matching
559
+ // the old tail-biased input field while slicing on grapheme/cell boundaries.
560
+ let start = caretIndex;
561
+ let end = caretIndex;
562
+ let cells = 0;
563
+ const lookAheadCells = Math.min(4, windowWidth);
564
+ while (end < parts.length && (cells < lookAheadCells || start === end)) {
565
+ const width = Math.max(1, parts[end]!.width);
566
+ if (cells > 0 && cells + width > windowWidth) break;
567
+ cells += width;
568
+ end += 1;
569
+ }
570
+ while (start > 0) {
571
+ const width = Math.max(1, parts[start - 1]!.width);
572
+ if (cells > 0 && cells + width > windowWidth) break;
573
+ cells += width;
574
+ start -= 1;
575
+ }
576
+ while (end < parts.length) {
577
+ const width = Math.max(1, parts[end]!.width);
578
+ if (cells > 0 && cells + width > windowWidth) break;
579
+ cells += width;
580
+ end += 1;
581
+ }
582
+
583
+ const textStart = parts[start]?.start ?? 0;
584
+ const textEnd = parts[end - 1]?.end ?? textStart;
585
+ return {
586
+ text: value.slice(textStart, textEnd),
587
+ caret: Math.max(0, Math.min(safeCaret - textStart, textEnd - textStart)),
588
+ };
436
589
  }
437
590
 
438
591
  function drawCursor(
@@ -441,10 +594,15 @@ function drawCursor(
441
594
  cursorOn: boolean,
442
595
  theme: GraphTheme,
443
596
  ): string {
597
+ const parts = graphemeParts(text);
444
598
  const safeCaret = Math.max(0, Math.min(caret, text.length));
445
- const before = text.slice(0, safeCaret);
446
- const at = text[safeCaret] ?? " ";
447
- const after = text.slice(safeCaret + 1);
599
+ const caretPartIndex = parts.findIndex((part) => part.end > safeCaret);
600
+ const cursorPart = caretPartIndex === -1 ? undefined : parts[caretPartIndex];
601
+ const cursorStart = cursorPart?.start ?? text.length;
602
+ const cursorEnd = cursorPart?.end ?? text.length;
603
+ const before = text.slice(0, cursorStart);
604
+ const at = cursorPart?.text ?? " ";
605
+ const after = text.slice(cursorEnd);
448
606
  const beforeFx = paint(before, theme.text);
449
607
  const afterFx = paint(after, theme.text);
450
608
  if (!cursorOn) return beforeFx + paint(at, theme.text) + afterFx;
@@ -459,43 +617,62 @@ function padToUsable(content: string, usable: number): string {
459
617
  return content + " ".repeat(usable - w);
460
618
  }
461
619
 
620
+ type CodingAgentKeybinding = Parameters<typeof keyHint>[0];
621
+
622
+ function graphKeyHint(
623
+ keybinding: CodingAgentKeybinding,
624
+ description: string,
625
+ theme: GraphTheme,
626
+ ): string {
627
+ try {
628
+ return keyHint(keybinding, description);
629
+ } catch {
630
+ return localKeyHint(keyText(keybinding), description, theme);
631
+ }
632
+ }
633
+
634
+ function graphRawKeyHint(key: string, description: string, theme: GraphTheme): string {
635
+ try {
636
+ return rawKeyHint(key, description);
637
+ } catch {
638
+ return localKeyHint(key, description, theme);
639
+ }
640
+ }
641
+
642
+ function localKeyHint(key: string, description: string, theme: GraphTheme): string {
643
+ return paint(key, theme.text) + paint(` ${description}`, theme.textMuted);
644
+ }
645
+
462
646
  function renderHints(kind: PendingPrompt["kind"], theme: GraphTheme): string {
463
- const accent = hexToAnsi(theme.text);
464
- const muted = hexToAnsi(theme.textMuted);
465
- const dim = hexToAnsi(theme.dim);
466
- const sep = `${dim} · ${RESET}`;
647
+ const sep = paint(" · ", theme.dim);
467
648
  if (kind === "editor") {
468
649
  return (
469
- `${accent}ctrl+s${RESET} ${muted}submit${RESET}` +
650
+ graphRawKeyHint("tab", "Submit Action", theme) +
470
651
  sep +
471
- `${accent}enter${RESET} ${muted}newline${RESET}` +
652
+ graphKeyHint("tui.input.submit", "Newline/Submit", theme) +
472
653
  sep +
473
- `${accent}esc${RESET} ${muted}skip${RESET}`
654
+ graphKeyHint("tui.select.cancel", "Skip", theme)
474
655
  );
475
656
  }
476
657
  if (kind === "confirm") {
477
658
  return (
478
- `${accent}y${RESET} ${muted}yes${RESET}` +
659
+ graphRawKeyHint("y", "Yes", theme) +
479
660
  sep +
480
- `${accent}n${RESET} ${muted}no${RESET}` +
661
+ graphRawKeyHint("n", "No", theme) +
481
662
  sep +
482
- `${accent}↵${RESET} ${muted}submit${RESET}` +
663
+ graphKeyHint("tui.select.confirm", "Submit", theme) +
483
664
  sep +
484
- `${accent}esc${RESET} ${muted}skip${RESET}`
665
+ graphKeyHint("tui.select.cancel", "Skip", theme)
485
666
  );
486
667
  }
487
668
  if (kind === "select") {
488
669
  return (
489
- `${accent}↑↓${RESET} ${muted}choose${RESET}` +
670
+ graphRawKeyHint("↑↓", "Choose", theme) +
490
671
  sep +
491
- `${accent}↵${RESET} ${muted}submit${RESET}` +
672
+ graphKeyHint("tui.select.confirm", "Submit", theme) +
492
673
  sep +
493
- `${accent}esc${RESET} ${muted}skip${RESET}`
674
+ graphKeyHint("tui.select.cancel", "Skip", theme)
494
675
  );
495
676
  }
496
- return (
497
- `${accent}↵${RESET} ${muted}submit${RESET}` +
498
- sep +
499
- `${accent}esc${RESET} ${muted}skip${RESET}`
500
- );
677
+ return graphKeyHint("tui.input.submit", "Submit", theme) + sep + graphKeyHint("tui.select.cancel", "Skip", theme);
501
678
  }