@agi-fans/dsh-tui 0.22.0 → 0.23.1

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 (282) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +219 -1
  3. package/lib/chrome/box.d.ts +1 -18
  4. package/lib/chrome/box.js +2 -45
  5. package/lib/chrome/box.js.map +1 -1
  6. package/lib/chrome/code-highlight.js +4 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +0 -1
  9. package/lib/chrome/command-output.js +11 -5
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/goal-bar.js +1 -1
  12. package/lib/chrome/goal-bar.js.map +1 -1
  13. package/lib/chrome/main-screen-renderer.js +4 -3
  14. package/lib/chrome/main-screen-renderer.js.map +1 -1
  15. package/lib/chrome/markdown.d.ts +3 -3
  16. package/lib/chrome/markdown.js +85 -36
  17. package/lib/chrome/markdown.js.map +1 -1
  18. package/lib/chrome/md-style.d.ts +5 -2
  19. package/lib/chrome/md-style.js +13 -4
  20. package/lib/chrome/md-style.js.map +1 -1
  21. package/lib/chrome/renderer.d.ts +37 -1
  22. package/lib/chrome/renderer.js.map +1 -1
  23. package/lib/chrome/status-line.d.ts +3 -3
  24. package/lib/chrome/status-line.js +5 -5
  25. package/lib/chrome/status-line.js.map +1 -1
  26. package/lib/chrome/theme.d.ts +8 -3
  27. package/lib/chrome/theme.js +51 -153
  28. package/lib/chrome/theme.js.map +1 -1
  29. package/lib/commands/auth-account.d.ts +9 -0
  30. package/lib/commands/auth-account.js +202 -0
  31. package/lib/commands/auth-account.js.map +1 -0
  32. package/lib/commands/auth.js +238 -414
  33. package/lib/commands/auth.js.map +1 -1
  34. package/lib/commands/diff.js +22 -16
  35. package/lib/commands/diff.js.map +1 -1
  36. package/lib/commands/export.d.ts +0 -1
  37. package/lib/commands/export.js +99 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +11 -9
  40. package/lib/commands/files.js.map +1 -1
  41. package/lib/commands/integrations.d.ts +12 -1
  42. package/lib/commands/integrations.js +120 -23
  43. package/lib/commands/integrations.js.map +1 -1
  44. package/lib/commands/jobs-browser.d.ts +3 -0
  45. package/lib/commands/jobs-browser.js +152 -0
  46. package/lib/commands/jobs-browser.js.map +1 -0
  47. package/lib/commands/jobs.d.ts +3 -14
  48. package/lib/commands/jobs.js +26 -18
  49. package/lib/commands/jobs.js.map +1 -1
  50. package/lib/commands/model.d.ts +3 -0
  51. package/lib/commands/model.js +212 -128
  52. package/lib/commands/model.js.map +1 -1
  53. package/lib/commands/open-external.d.ts +2 -0
  54. package/lib/commands/open-external.js +19 -0
  55. package/lib/commands/open-external.js.map +1 -0
  56. package/lib/commands/permission.js +65 -54
  57. package/lib/commands/permission.js.map +1 -1
  58. package/lib/commands/plugins.js +381 -139
  59. package/lib/commands/plugins.js.map +1 -1
  60. package/lib/commands/schedule-catalog.d.ts +12 -0
  61. package/lib/commands/schedule-catalog.js +41 -0
  62. package/lib/commands/schedule-catalog.js.map +1 -0
  63. package/lib/commands/schedule-history.d.ts +4 -0
  64. package/lib/commands/schedule-history.js +62 -0
  65. package/lib/commands/schedule-history.js.map +1 -0
  66. package/lib/commands/schedule.d.ts +12 -15
  67. package/lib/commands/schedule.js +220 -187
  68. package/lib/commands/schedule.js.map +1 -1
  69. package/lib/commands/session-configuration.d.ts +3 -0
  70. package/lib/commands/session-configuration.js +62 -28
  71. package/lib/commands/session-configuration.js.map +1 -1
  72. package/lib/commands/session.js +116 -66
  73. package/lib/commands/session.js.map +1 -1
  74. package/lib/commands/terminal.js +14 -8
  75. package/lib/commands/terminal.js.map +1 -1
  76. package/lib/definition.d.ts +115 -15
  77. package/lib/index.d.ts +1 -0
  78. package/lib/index.js +1 -0
  79. package/lib/index.js.map +1 -1
  80. package/lib/input/actions.d.ts +1 -1
  81. package/lib/input/actions.js +5 -2
  82. package/lib/input/actions.js.map +1 -1
  83. package/lib/input/composer-files.d.ts +2 -0
  84. package/lib/input/composer-files.js +13 -3
  85. package/lib/input/composer-files.js.map +1 -1
  86. package/lib/input/image-paste.d.ts +5 -12
  87. package/lib/input/image-paste.js +32 -28
  88. package/lib/input/image-paste.js.map +1 -1
  89. package/lib/input/key-test.d.ts +5 -0
  90. package/lib/input/key-test.js +19 -0
  91. package/lib/input/key-test.js.map +1 -0
  92. package/lib/input/keys.d.ts +2 -0
  93. package/lib/input/keys.js +4 -1
  94. package/lib/input/keys.js.map +1 -1
  95. package/lib/locale/index.d.ts +6 -0
  96. package/lib/locale/index.js +32 -0
  97. package/lib/locale/index.js.map +1 -0
  98. package/lib/locale/prompt.d.ts +4 -0
  99. package/lib/locale/prompt.js +24 -0
  100. package/lib/locale/prompt.js.map +1 -0
  101. package/lib/locale/zh.d.ts +1114 -0
  102. package/lib/locale/zh.js +1115 -0
  103. package/lib/locale/zh.js.map +1 -0
  104. package/lib/prompt-intents.d.ts +4 -0
  105. package/lib/prompt-intents.js +4 -0
  106. package/lib/prompt-intents.js.map +1 -1
  107. package/lib/runtime/account-settings.d.ts +18 -0
  108. package/lib/runtime/account-settings.js +241 -0
  109. package/lib/runtime/account-settings.js.map +1 -0
  110. package/lib/runtime/agent-profile.d.ts +3 -3
  111. package/lib/runtime/agent-profile.js +4 -4
  112. package/lib/runtime/agent-profile.js.map +1 -1
  113. package/lib/runtime/btw-policy.js +2 -1
  114. package/lib/runtime/btw-policy.js.map +1 -1
  115. package/lib/runtime/btw.js +1 -2
  116. package/lib/runtime/btw.js.map +1 -1
  117. package/lib/runtime/composer-images.d.ts +2 -2
  118. package/lib/runtime/composer-images.js +4 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.js +27 -1
  121. package/lib/runtime/composer-shell.js.map +1 -1
  122. package/lib/runtime/file-browser.d.ts +19 -0
  123. package/lib/runtime/file-browser.js +291 -0
  124. package/lib/runtime/file-browser.js.map +1 -0
  125. package/lib/runtime/file-review.d.ts +6 -9
  126. package/lib/runtime/file-review.js +91 -7
  127. package/lib/runtime/file-review.js.map +1 -1
  128. package/lib/runtime/help-reader.d.ts +12 -0
  129. package/lib/runtime/help-reader.js +50 -0
  130. package/lib/runtime/help-reader.js.map +1 -0
  131. package/lib/runtime/human-interaction.js +1 -1
  132. package/lib/runtime/human-interaction.js.map +1 -1
  133. package/lib/runtime/model-catalog-editor.d.ts +12 -0
  134. package/lib/runtime/model-catalog-editor.js +215 -0
  135. package/lib/runtime/model-catalog-editor.js.map +1 -0
  136. package/lib/runtime/model-settings.d.ts +11 -0
  137. package/lib/runtime/model-settings.js +404 -0
  138. package/lib/runtime/model-settings.js.map +1 -0
  139. package/lib/runtime/plugin-settings.d.ts +4 -1
  140. package/lib/runtime/plugin-settings.js +164 -88
  141. package/lib/runtime/plugin-settings.js.map +1 -1
  142. package/lib/runtime/prompt-browser.d.ts +9 -0
  143. package/lib/runtime/prompt-browser.js +19 -0
  144. package/lib/runtime/prompt-browser.js.map +1 -0
  145. package/lib/runtime/provider-local.d.ts +27 -27
  146. package/lib/runtime/provider-local.js +1288 -308
  147. package/lib/runtime/provider-local.js.map +1 -1
  148. package/lib/runtime/reference-reader.d.ts +15 -0
  149. package/lib/runtime/reference-reader.js +78 -0
  150. package/lib/runtime/reference-reader.js.map +1 -0
  151. package/lib/runtime/runner.js +38 -29
  152. package/lib/runtime/runner.js.map +1 -1
  153. package/lib/runtime/session-runtime.js +1 -1
  154. package/lib/runtime/session-runtime.js.map +1 -1
  155. package/lib/runtime/settings-draft.d.ts +21 -0
  156. package/lib/runtime/settings-draft.js +166 -0
  157. package/lib/runtime/settings-draft.js.map +1 -0
  158. package/lib/runtime/settings-preferences.js +1 -1
  159. package/lib/runtime/settings-preferences.js.map +1 -1
  160. package/lib/runtime/startup-notices.d.ts +0 -1
  161. package/lib/runtime/startup-notices.js +35 -6
  162. package/lib/runtime/startup-notices.js.map +1 -1
  163. package/lib/runtime/subagent-model-selection.js +2 -1
  164. package/lib/runtime/subagent-model-selection.js.map +1 -1
  165. package/lib/runtime/subagent-settings.d.ts +14 -0
  166. package/lib/runtime/subagent-settings.js +288 -0
  167. package/lib/runtime/subagent-settings.js.map +1 -0
  168. package/lib/runtime/terminal-notifications.d.ts +3 -0
  169. package/lib/runtime/terminal-notifications.js +13 -8
  170. package/lib/runtime/terminal-notifications.js.map +1 -1
  171. package/lib/runtime/terminal-probe.d.ts +2 -0
  172. package/lib/runtime/terminal-probe.js +9 -0
  173. package/lib/runtime/terminal-probe.js.map +1 -1
  174. package/lib/runtime/tool-presentation.js.map +1 -1
  175. package/lib/runtime/tool-reader.d.ts +5 -0
  176. package/lib/runtime/tool-reader.js +27 -0
  177. package/lib/runtime/tool-reader.js.map +1 -0
  178. package/lib/runtime/web-settings.d.ts +14 -0
  179. package/lib/runtime/web-settings.js +339 -0
  180. package/lib/runtime/web-settings.js.map +1 -0
  181. package/lib/session/command-attachments.d.ts +17 -0
  182. package/lib/session/command-attachments.js +50 -0
  183. package/lib/session/command-attachments.js.map +1 -0
  184. package/lib/session/context-diagnostics.d.ts +16 -0
  185. package/lib/session/context-diagnostics.js +15 -0
  186. package/lib/session/context-diagnostics.js.map +1 -1
  187. package/lib/session/external-agents.d.ts +3 -1
  188. package/lib/session/external-agents.js +35 -10
  189. package/lib/session/external-agents.js.map +1 -1
  190. package/lib/session/generation-continuation.d.ts +11 -0
  191. package/lib/session/generation-continuation.js +34 -0
  192. package/lib/session/generation-continuation.js.map +1 -0
  193. package/lib/session/goal-controls.d.ts +5 -0
  194. package/lib/session/goal-controls.js +81 -0
  195. package/lib/session/goal-controls.js.map +1 -0
  196. package/lib/session/interaction-adapter.d.ts +7 -3
  197. package/lib/session/interaction-adapter.js +121 -49
  198. package/lib/session/interaction-adapter.js.map +1 -1
  199. package/lib/session/message-queue.d.ts +2 -0
  200. package/lib/session/message-queue.js +102 -17
  201. package/lib/session/message-queue.js.map +1 -1
  202. package/lib/session/pending-inputs.d.ts +30 -0
  203. package/lib/session/pending-inputs.js +183 -0
  204. package/lib/session/pending-inputs.js.map +1 -0
  205. package/lib/session/session-configuration.js +1 -1
  206. package/lib/session/session-configuration.js.map +1 -1
  207. package/lib/session/session-controller.d.ts +33 -20
  208. package/lib/session/session-controller.js +500 -129
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/session-library.d.ts +3 -1
  211. package/lib/session/session-library.js +15 -0
  212. package/lib/session/session-library.js.map +1 -1
  213. package/lib/session/subagent-roster.d.ts +1 -0
  214. package/lib/session/subagent-roster.js +16 -5
  215. package/lib/session/subagent-roster.js.map +1 -1
  216. package/lib/session/subagent-tracker.d.ts +2 -0
  217. package/lib/session/subagent-tracker.js +18 -0
  218. package/lib/session/subagent-tracker.js.map +1 -1
  219. package/lib/session/tui-settings.d.ts +9 -5
  220. package/lib/session/tui-settings.js +3 -1
  221. package/lib/session/tui-settings.js.map +1 -1
  222. package/lib/session/workflow-projection.d.ts +2 -1
  223. package/lib/session/workflow-projection.js +34 -4
  224. package/lib/session/workflow-projection.js.map +1 -1
  225. package/lib/views/autocomplete.d.ts +12 -12
  226. package/lib/views/autocomplete.js +54 -45
  227. package/lib/views/autocomplete.js.map +1 -1
  228. package/lib/views/context-overview.d.ts +4 -0
  229. package/lib/views/context-overview.js +43 -0
  230. package/lib/views/context-overview.js.map +1 -0
  231. package/lib/views/copy-targets.d.ts +6 -2
  232. package/lib/views/copy-targets.js +19 -14
  233. package/lib/views/copy-targets.js.map +1 -1
  234. package/lib/views/document-reader.d.ts +10 -1
  235. package/lib/views/document-reader.js +63 -6
  236. package/lib/views/document-reader.js.map +1 -1
  237. package/lib/views/event-views.js +29 -11
  238. package/lib/views/event-views.js.map +1 -1
  239. package/lib/views/file-actions.js +21 -0
  240. package/lib/views/file-actions.js.map +1 -1
  241. package/lib/views/file-browser.d.ts +98 -0
  242. package/lib/views/file-browser.js +120 -0
  243. package/lib/views/file-browser.js.map +1 -0
  244. package/lib/views/history-search.js +4 -4
  245. package/lib/views/history-search.js.map +1 -1
  246. package/lib/views/hotkey-format.d.ts +3 -2
  247. package/lib/views/hotkey-format.js +4 -3
  248. package/lib/views/hotkey-format.js.map +1 -1
  249. package/lib/views/hotkeys.js +10 -12
  250. package/lib/views/hotkeys.js.map +1 -1
  251. package/lib/views/list-view.js +16 -11
  252. package/lib/views/list-view.js.map +1 -1
  253. package/lib/views/prompt-selector.d.ts +23 -1
  254. package/lib/views/prompt-selector.js +123 -60
  255. package/lib/views/prompt-selector.js.map +1 -1
  256. package/lib/views/settings-list.d.ts +6 -1
  257. package/lib/views/settings-list.js +69 -46
  258. package/lib/views/settings-list.js.map +1 -1
  259. package/lib/views/tool-actions.d.ts +16 -0
  260. package/lib/views/tool-actions.js +29 -0
  261. package/lib/views/tool-actions.js.map +1 -0
  262. package/lib/views/trajectory-timeline.d.ts +5 -6
  263. package/lib/views/trajectory-timeline.js +25 -15
  264. package/lib/views/trajectory-timeline.js.map +1 -1
  265. package/lib/views/trajectory.d.ts +23 -2
  266. package/lib/views/trajectory.js +404 -186
  267. package/lib/views/trajectory.js.map +1 -1
  268. package/lib/views/transcript-render.d.ts +39 -15
  269. package/lib/views/transcript-render.js +406 -208
  270. package/lib/views/transcript-render.js.map +1 -1
  271. package/lib/views/transcript-types.d.ts +3 -2
  272. package/lib/views/transcript-types.js.map +1 -1
  273. package/lib/views/workflow.d.ts +16 -0
  274. package/lib/views/workflow.js +42 -0
  275. package/lib/views/workflow.js.map +1 -0
  276. package/package.json +61 -58
  277. package/lib/chrome/welcome-tips.d.ts +0 -23
  278. package/lib/chrome/welcome-tips.js +0 -24
  279. package/lib/chrome/welcome-tips.js.map +0 -1
  280. package/lib/session/steering.d.ts +0 -3
  281. package/lib/session/steering.js +0 -9
  282. package/lib/session/steering.js.map +0 -1
@@ -1,3 +1,11 @@
1
+ import { renderContextOverview } from "../views/context-overview.js";
2
+ import { contextOverviewDetails } from "../session/context-diagnostics.js";
3
+ import { browseTools } from "./tool-reader.js";
4
+ import { promptBrowserState } from "./prompt-browser.js";
5
+ import { browseHelp } from "./help-reader.js";
6
+ import { readReference } from "./reference-reader.js";
7
+ import { FileBrowser } from "./file-browser.js";
8
+ import { reasoningKey } from "../views/transcript-types.js";
1
9
  /**
2
10
  * TUI capability seam — local terminal provider.
3
11
 
@@ -10,6 +18,10 @@
10
18
  * printing of settled blocks.
11
19
  * @module @agi-fans/dsh-tui
12
20
  */
21
+ import { rosterItems } from "../views/workflow.js";
22
+ import { localizePrompt } from "../locale/prompt.js";
23
+ import { resolveUiLanguage, uiText } from "../locale/index.js";
24
+ import { formatAgentPreset } from "../session/session-configuration.js";
13
25
  import { TerminalNotificationController, TerminalNotificationQueue, terminalNotificationSequence, } from "./terminal-notifications.js";
14
26
  import { PROGRAM_STATUS_QUERY, programStatusSequence, terminalTitle } from "./terminal-state.js";
15
27
  import { TerminalSession } from "./terminal-session.js";
@@ -20,6 +32,7 @@ import { imageProtocol } from "../chrome/terminal-images.js";
20
32
  import { previewImages } from "./image-preview.js";
21
33
  import { rmSync } from 'node:fs';
22
34
  import { ComposerImages } from "./composer-images.js";
35
+ import { PendingInputs } from "../session/pending-inputs.js";
23
36
  import { ComposerDraftStore } from "../session/composer-drafts.js";
24
37
  import { mergeJobSettlement } from "../session/job-notice.js";
25
38
  import { PlainTui } from "./plain-tui.js";
@@ -39,14 +52,16 @@ import { matchesAtShortcutQuery, nextSelectableAutocompleteIndex, searchAtSugges
39
52
  import { readTmuxMouseEnabled } from "../input/mouse-policy.js";
40
53
  import { TextSelection } from "../input/text-selection.js";
41
54
  import { copyToClipboard, readFromClipboard } from "../input/clipboard.js";
42
- import { buildCopyTargets } from "../views/copy-targets.js";
55
+ import { toolActionContext, toolSource, sourceQuote } from "../views/tool-actions.js";
56
+ import { keyTestDescription } from "../input/key-test.js";
57
+ import { buildCopyTargets, toolCommand } from "../views/copy-targets.js";
43
58
  import { applyHistorySearchEvent, createHistorySearch, } from "../views/history-search.js";
44
59
  import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
45
60
  import { applySettingsEvent, createSettings, refreshSettings, tuiSettingItems, } from "../views/settings-list.js";
46
61
  import { editSettingsEntry, pluginSettingsEntries, resetSettingsControl, setSettingsEntry, showSettingsError } from "./plugin-settings.js";
47
62
  import { preferenceChanges } from "./settings-preferences.js";
48
- import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryListMetrics, } from "../views/trajectory.js";
49
- import { applyEvent, applyStreamChunk, renderedBlocks, formatAgentElapsed, subagentPreviewStart, initialTranscript, replayEvents, renderView, turnGroups, transcriptSeam, TRANSCRIPT_FAST_SCROLL, withWorkspaceSummary, } from "../views/event-views.js";
63
+ import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryDetailScrollLimit, trajectoryClick, trajectoryListMetrics, } from "../views/trajectory.js";
64
+ import { applyEvent, applyStreamChunk, renderedBlocks, formatAgentElapsed, rosterPreviewStart, initialTranscript, replayEvents, renderView, turnGroups, transcriptSeam, TRANSCRIPT_FAST_SCROLL, withWorkspaceSummary, } from "../views/event-views.js";
50
65
  import { applyTranscriptSearchEvent, createTranscriptSearch, searchMatches, } from "../views/transcript-search.js";
51
66
  import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
52
67
  import { flushPending, parseKeys } from "../input/keys.js";
@@ -55,6 +70,7 @@ import { blockFileAction } from "../views/file-actions.js";
55
70
  import { fileDocument, reviewFiles } from "./file-review.js";
56
71
  import { stripAnsi } from "../chrome/width.js";
57
72
  import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
73
+ import { supportsHyperlinks } from "./terminal-probe.js";
58
74
  import { colorDisabledByEnv, createTheme, detectColorDepth } from "../chrome/theme.js";
59
75
  import { formatToolsText } from "../chrome/tools-list.js";
60
76
  import { renderTool } from "../chrome/tool-renderers.js";
@@ -66,12 +82,12 @@ import { DEFAULT_KEYBINDINGS, loadKeybindings, saveKeybindings, TUI_ACTION_LABEL
66
82
  import { discoverEditors } from "../input/editor-discovery.js";
67
83
  import { editExternally, editFileExternally } from "../input/external-editor.js";
68
84
  import z from '@deepseek-ai/schemastery';
69
- import { movePromptSelection, movePromptTree, searchPromptSelection, filteredPromptOptions, refreshPromptOptions, promptTreePageSize, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, documentRows, findInRows, } from "../views/prompt-selector.js";
85
+ import { movePromptSelection, movePromptTree, searchPromptSelection, filteredPromptOptions, refreshPromptOptions, promptTreePageSize, selectedPromptAnswer, structuredPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, documentRows, findInRows, } from "../views/prompt-selector.js";
70
86
  import { edgeIndex, listNavigation, navigateIndex } from "../views/list-navigation.js";
71
87
  import { refreshProjectContext, resolveGitStatusFiles, resolveProjectContext } from "../session/project-context.js";
72
88
  import { modelRouteReady } from "../session/model-readiness.js";
73
- import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount, keysForAction } from "../views/hotkeys.js";
74
- import { imagePathCandidates, splitComposerImageMarkers, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
89
+ import { formatEssentialHotkeysText, formatHotkeysText, keysForAction } from "../views/hotkeys.js";
90
+ import { imagePathCandidates, splitComposerAttachmentMarkers, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
75
91
  import { APP_DISPLAY_NAME, APP_NAME, APP_VERSION } from "../session/package-metadata.js";
76
92
  const DOUBLE_CTRL_C_MS = 500;
77
93
  /**
@@ -188,6 +204,8 @@ export class LocalTui {
188
204
  #imageCellSize = { width: 9, height: 18 };
189
205
  #imagePreviewAbort;
190
206
  #fileReviewAbort;
207
+ #keyTest;
208
+ #mouseActionPress;
191
209
  #fileActionFrame;
192
210
  #imageTemporary = new Set();
193
211
  #terminalProfile;
@@ -255,12 +273,18 @@ export class LocalTui {
255
273
  #features;
256
274
  /** Profile patch the Tools feature controls write; absent hides those controls. */
257
275
  #featurePatch;
276
+ #fileBrowser = null;
258
277
  #trajectory = null;
278
+ #trajectoryPress;
259
279
  #pending = null;
260
- #queuedSubmissions = [];
280
+ #pendingInputs;
281
+ #pendingScope() { return this.#draftScope ?? '__default__'; }
282
+ get #queuedSubmissions() { return this.#pendingInputs.session(this.#pendingScope()).queued; }
261
283
  /** Newer queue entries temporarily detached while Up browses backward. */
262
- #queueEditNewer = null;
284
+ get #queueEditNewer() { return this.#pendingInputs.session(this.#pendingScope()).newer; }
285
+ set #queueEditNewer(value) { this.#pendingInputs.session(this.#pendingScope()).newer = value; }
263
286
  #queueEditPending = false;
287
+ #queueRecall;
264
288
  #quitRequested = false;
265
289
  #resumeHintRequested = false;
266
290
  #lastSigintTime = 0;
@@ -268,13 +292,15 @@ export class LocalTui {
268
292
  #inspectEscapeFenceUntil = 0;
269
293
  #interrupts = new Set();
270
294
  #queueEdits = new Set();
295
+ #continueHandler;
296
+ #continuing = false;
297
+ #goalHandler;
298
+ #goalAbort;
271
299
  #queueHandler;
272
300
  #queueHotkeyAbort;
273
- #queuedInputIds = new WeakMap();
274
- #queuedInputSerial = 0;
275
- #steerHandler;
276
301
  #rewinds = new Set();
277
302
  #inspects = new Set();
303
+ #workflowInspects = new Set();
278
304
  #inspectCloses = new Set();
279
305
  #inspectSubmits = new Set();
280
306
  #disposed = false;
@@ -285,6 +311,7 @@ export class LocalTui {
285
311
  #autocompleteRequestId = 0;
286
312
  #pasteBurst = new PasteBurst();
287
313
  #pasteBurstTimer;
314
+ #busyEnter = 'queue';
288
315
  #pasteProtection = true;
289
316
  #paste = false;
290
317
  #pasteBuf = '';
@@ -316,6 +343,9 @@ export class LocalTui {
316
343
  #follow = true;
317
344
  #focusBlock;
318
345
  #focusBlockEdge;
346
+ #focusBlockOffset;
347
+ /** Reading positions last only for this inspection and this detail layout. */
348
+ #toolReadingPositions = new Map();
319
349
  #motion = 'full';
320
350
  #externalEditor = 'auto';
321
351
  #terminalTitle = 'status';
@@ -383,9 +413,10 @@ export class LocalTui {
383
413
  #mathMode = 'auto';
384
414
  #mermaidMode = 'auto';
385
415
  /** Transcript collapses every thought to one line; Ctrl+T flips it. */
386
- #hideThinking = false;
416
+ #hideThinking = true;
387
417
  #mouseInteraction = 'auto';
388
418
  #tmuxMouseEnabled;
419
+ #disclosurePress;
389
420
  #jumpPress;
390
421
  #jumpToLatest;
391
422
  #openedGroups = new Set();
@@ -395,6 +426,9 @@ export class LocalTui {
395
426
  #asideAbort;
396
427
  /** Thoughts a reader opened individually, addressed by reasoning identity. */
397
428
  #expandedReasoning = new Set();
429
+ #expandedToolRows = new Set();
430
+ #processSelection;
431
+ #closedReasoning = new Set();
398
432
  /**
399
433
  * The last action's confirmation, drawn on the composer's bottom border.
400
434
  *
@@ -405,6 +439,8 @@ export class LocalTui {
405
439
  */
406
440
  #transientStatus;
407
441
  #transientTimer = null;
442
+ /** Idle submissions also pass through the inbox, but are not queue gestures. */
443
+ #directInputIds = new Set();
408
444
  /** Aborts the live Shell card, so Ctrl+C interrupts the command it belongs to. */
409
445
  #shellAbort;
410
446
  /** Turn scopes whose tools stay detailed as new calls arrive during inspection. */
@@ -437,6 +473,10 @@ export class LocalTui {
437
473
  */
438
474
  #subagentSelection;
439
475
  #inspected;
476
+ #promptActions = [];
477
+ #promptActionPress;
478
+ #promptChoices = [];
479
+ #promptChoicePress;
440
480
  #promptDocument;
441
481
  /**
442
482
  * 24-bit switch for the current color preference, re-derived whenever that
@@ -444,6 +484,7 @@ export class LocalTui {
444
484
  * hexadecimal palettes without a restart.
445
485
  */
446
486
  #colorDepth = 'ansi16';
487
+ #compactLinks = supportsHyperlinks(process.env);
447
488
  /** Hold the first frame until the capability probe settles; scrollback rows are immutable, so the first real paint must use the reported colors. */
448
489
  #probeGate = false;
449
490
  #copy;
@@ -468,6 +509,7 @@ export class LocalTui {
468
509
  #prefsSaveTask;
469
510
  #disposeTask;
470
511
  #prefsVersion = 0;
512
+ #uiLanguage = 'auto';
471
513
  #agentBehavior;
472
514
  #updateAgentBehavior;
473
515
  #offAgentBehaviorWatch;
@@ -527,6 +569,7 @@ export class LocalTui {
527
569
  this.#syncColorDepth();
528
570
  this.#tty = term.input.isTTY === true && term.output.isTTY === true;
529
571
  this.#draftStore = this.#tty && paths.draftsPath !== undefined ? new ComposerDraftStore(paths.draftsPath) : undefined;
572
+ this.#pendingInputs = new PendingInputs(this.#tty && paths.draftsPath !== undefined ? join(paths.draftsPath, 'pending') : undefined, (action, error) => this.notice(`Could not ${action} pending input: ${error instanceof Error ? error.message : String(error)}`, { level: 'error' }));
530
573
  this.#plain = new PlainTui({
531
574
  term,
532
575
  blocks: () => this.#state.blocks,
@@ -554,7 +597,9 @@ export class LocalTui {
554
597
  // A terminal resize changes the committed/live seam; re-anchor the
555
598
  // live window so it stays anchored at the bottom.
556
599
  this.#selection.clear();
600
+ this.#mouseActionPress = undefined;
557
601
  this.#jumpPress = undefined;
602
+ this.#disclosurePress = undefined;
558
603
  const repaint = () => {
559
604
  this.#resizeTimer = null;
560
605
  this.#renderer.resize(this.#term.width(), this.#term.height());
@@ -637,7 +682,27 @@ export class LocalTui {
637
682
  this.#retryDeadline = Date.now() + event.data.delayMs;
638
683
  if (event.type === 'llm/retry-started' || event.type === 'turn/end')
639
684
  this.#retryDeadline = undefined;
685
+ let claimedHumanInput = false;
686
+ if (event.type === 'agent/inbox/spliced') {
687
+ const inbox = event.data.target === 'next-step' ? this.#state.nextStepInbox : this.#state.nextTurnInbox;
688
+ const removed = inbox.slice(event.data.start, event.data.start + (event.data.removedCount ?? 0));
689
+ claimedHumanInput = event.data.outcome !== 'canceled' && event.data.inserted.length === 0
690
+ && removed.some(message => message.source.kind === 'user' && !this.#directInputIds.has(message.id));
691
+ for (const message of removed)
692
+ this.#directInputIds.delete(message.id);
693
+ if (event.data.target === 'next-turn' && this.#state.status === 'idle'
694
+ && !inbox.some(message => message.source.kind === 'user')) {
695
+ const first = event.data.inserted.find(message => message.source.kind === 'user');
696
+ if (first !== undefined)
697
+ this.#directInputIds.add(first.id);
698
+ }
699
+ }
700
+ else if (event.type === 'session/end-seed')
701
+ this.#directInputIds.clear();
640
702
  this.#state = applyEvent(this.#state, event, presentation, workspace);
703
+ if (claimedHumanInput && this.#tty && event.type === 'agent/inbox/spliced') {
704
+ this.#showTransientStatus(event.data.target === 'next-step' ? 'Steer taken for the next step' : 'Queue taken for the next turn');
705
+ }
641
706
  const compacting = this.#state.status === 'compacting';
642
707
  if (compacting && this.#compactionStartedAt === undefined)
643
708
  this.#compactionStartedAt = Date.now();
@@ -749,12 +814,24 @@ export class LocalTui {
749
814
  const statusChanged = previous.length !== incoming.length || incoming.some((agent, index) => {
750
815
  const before = previous[index];
751
816
  return before === undefined || before.id !== agent.id || before.label !== agent.label
752
- || before.phase !== agent.phase || before.depth !== agent.depth || before.mode !== agent.mode;
817
+ || before.phase !== agent.phase || before.needsApproval !== agent.needsApproval || before.depth !== agent.depth || before.mode !== agent.mode;
753
818
  });
819
+ const previousItems = rosterItems(this.#subagents);
820
+ const incomingItems = rosterItems(roster);
821
+ const workflowsChanged = (this.#subagents?.workflows?.length ?? 0) !== (roster?.workflows?.length ?? 0)
822
+ || (roster?.workflows ?? []).some((run, index) => {
823
+ const before = this.#subagents?.workflows?.[index];
824
+ return before?.id !== run.id || before.name !== run.name || before.status !== run.status || before.phase !== run.phase || before.members !== run.members;
825
+ });
826
+ const selectedId = this.#subagentSelection === undefined ? undefined : previousItems[this.#subagentSelection]?.key;
754
827
  this.#subagents = roster === undefined
755
828
  ? undefined
756
- : { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })) };
757
- if ((this.#subagents?.agents.length ?? 0) === 0)
829
+ : { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })), ...(roster.workflows === undefined ? {} : { workflows: [...roster.workflows] }) };
830
+ if (selectedId !== undefined) {
831
+ const index = incomingItems.findIndex(item => item.key === selectedId);
832
+ this.#subagentSelection = index < 0 ? undefined : index;
833
+ }
834
+ if (incomingItems.length === 0)
758
835
  this.#subagentSelection = undefined;
759
836
  const inspected = this.#inspected;
760
837
  if (inspected !== undefined) {
@@ -764,12 +841,13 @@ export class LocalTui {
764
841
  id: current.id,
765
842
  label: current.label,
766
843
  phase: current.phase,
844
+ needsApproval: current.needsApproval === true,
767
845
  ...(current.mode === undefined ? {} : { mode: current.mode }),
768
846
  writable: current.mode === 'continuable',
769
847
  };
770
848
  }
771
849
  }
772
- if (!statusChanged)
850
+ if (!statusChanged && !workflowsChanged)
773
851
  return;
774
852
  this.#syncTick();
775
853
  if (this.#tty)
@@ -777,11 +855,16 @@ export class LocalTui {
777
855
  }
778
856
  setInspectedSubagent(inspected) {
779
857
  if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
780
- && this.#inspected?.phase === inspected?.phase && this.#inspected?.mode === inspected?.mode
858
+ && this.#inspected?.phase === inspected?.phase && this.#inspected?.needsApproval === inspected?.needsApproval && this.#inspected?.mode === inspected?.mode
781
859
  && this.#inspected?.writable === inspected?.writable)
782
860
  return;
861
+ if (this.#inspected?.id !== inspected?.id) {
862
+ this.#fileReviewAbort?.abort();
863
+ this.#closeFileBrowser();
864
+ }
783
865
  this.#switchDraft(inspected?.id ?? this.#sessionId);
784
866
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
867
+ this.#drainPendingInput();
785
868
  if (inspected !== undefined) {
786
869
  this.#inspectEscapeFenceUntil = 0;
787
870
  this.#subagentSelection = undefined;
@@ -794,20 +877,54 @@ export class LocalTui {
794
877
  this.#tools = tools.map((tool) => ({ name: tool.name, description: tool.description }));
795
878
  }
796
879
  setCommands(commands) {
797
- this.#runtimeCommands = commands.map((command) => ({
798
- name: command.name,
799
- description: command.description,
800
- ...(command.inputHint === undefined ? {} : { inputHint: command.inputHint }),
801
- }));
880
+ this.#runtimeCommands = commands.map(command => ({ ...command }));
802
881
  this.#syncSettingsActions();
803
882
  this.#refreshAutocomplete();
804
883
  if (this.#tty)
805
884
  this.#render();
806
885
  }
886
+ async reviewFiles(request) {
887
+ if (!this.#tty || this.#disposed || request.signal.aborted)
888
+ return;
889
+ this.#flushPasteBurst(true);
890
+ this.#pasteBurst.clear();
891
+ this.#finishPrompt(null);
892
+ this.#closeFileBrowser();
893
+ this.#selection.clear();
894
+ this.#trajectory = null;
895
+ this.#settings = null;
896
+ this.#search = null;
897
+ this.#ac = null;
898
+ const browser = new FileBrowser(request, () => { if (!this.#disposed)
899
+ this.#render(); }, () => {
900
+ if (this.#fileBrowser === browser)
901
+ this.#fileBrowser = null;
902
+ if (this.#promptDisplaced?.kind === 'fileBrowser' && this.#promptDisplaced.browser === browser)
903
+ this.#promptDisplaced = null;
904
+ this.#selection.clear();
905
+ this.#trajectoryPress = undefined;
906
+ if (!this.#disposed)
907
+ this.#render();
908
+ });
909
+ this.#fileBrowser = browser;
910
+ browser.start();
911
+ await browser.done;
912
+ }
913
+ #closeFileBrowser() {
914
+ this.#fileBrowser?.close();
915
+ if (this.#promptDisplaced?.kind === 'fileBrowser')
916
+ this.#promptDisplaced.browser.close();
917
+ }
918
+ #applyFileBrowserInput(event) {
919
+ this.#fileBrowser?.input(event, createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks), this.#term.width(), this.#term.height());
920
+ }
807
921
  openTrajectory(events) {
808
922
  if (!this.#tty)
809
923
  return false;
810
924
  this.#finishPrompt(null);
925
+ this.#selection.clear();
926
+ this.#trajectoryPress = undefined;
927
+ this.#closeFileBrowser();
811
928
  this.#trajectory = createTrajectory(events);
812
929
  this.#settings = null;
813
930
  this.#search = null;
@@ -848,6 +965,11 @@ export class LocalTui {
848
965
  .join('\n');
849
966
  }
850
967
  notice(text, options = {}) {
968
+ if (options.transient) {
969
+ if (this.#tty && !this.#disposed)
970
+ this.#showTransientStatus(text);
971
+ return;
972
+ }
851
973
  const merged = options.job === undefined ? undefined : mergeJobSettlement(this.#state, options.job);
852
974
  if (merged !== undefined && this.#tty) {
853
975
  const origin = merged.blocks.find((block, index) => block !== this.#state.blocks[index]);
@@ -946,7 +1068,6 @@ export class LocalTui {
946
1068
  output: result.output === '' ? current.output : result.output,
947
1069
  status: failed ? 'error' : 'ok',
948
1070
  ...(result.code === undefined ? {} : { code: result.code }),
949
- ...(request.share === true ? { shared: true } : {}),
950
1071
  }));
951
1072
  paint();
952
1073
  return result;
@@ -1080,7 +1201,24 @@ export class LocalTui {
1080
1201
  next[at] = update(next[at]);
1081
1202
  this.#state = { ...this.#state, blocks: next };
1082
1203
  }
1204
+ openContext(request) {
1205
+ let snapshot = request.read();
1206
+ return this.#promptWithDocument({
1207
+ localize: ['title', 'options'], title: 'Context', question: '',
1208
+ detail: contextOverviewDetails(snapshot), presentation: 'document', allowCustom: false, notify: false,
1209
+ options: [{ label: 'Details', value: 'details' }, { label: 'Copy session ID', value: 'copy' },
1210
+ ...(request.canCompact ? [{ label: 'Compact', value: 'compact' }] : [])],
1211
+ refreshDocument: () => { snapshot = request.read(); return contextOverviewDetails(snapshot); },
1212
+ signal: request.signal, onSuperseded: request.onSuperseded,
1213
+ }, (theme, width) => renderContextOverview(snapshot, theme, width));
1214
+ }
1083
1215
  prompt(request) {
1216
+ return this.#promptWithDocument(request);
1217
+ }
1218
+ #promptWithDocument(request, renderDocument) {
1219
+ const sourceRequest = request;
1220
+ const browserState = promptBrowserState(request);
1221
+ request = localizePrompt(request, resolveUiLanguage(this.#uiLanguage));
1084
1222
  // Background approvals take the terminal from a user-opened file reader.
1085
1223
  if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal)
1086
1224
  this.#fileReviewAbort.abort();
@@ -1108,13 +1246,22 @@ export class LocalTui {
1108
1246
  }
1109
1247
  if (this.#tty && request.documentImage !== undefined && this.#imageProtocol !== undefined)
1110
1248
  this.#term.output.write('\x1b[16t');
1249
+ this.#selection.clear();
1250
+ this.#promptActionPress = undefined;
1251
+ this.#promptChoicePress = undefined;
1252
+ this.#disclosurePress = undefined;
1253
+ this.#mouseActionPress = undefined;
1254
+ this.#jumpPress = undefined;
1255
+ this.#wheelGesture = IDLE_WHEEL;
1111
1256
  const savedInput = this.#editor.snapshot();
1112
- this.#editor.setText(request.allowCustom === false || request.filterable === true || (request.options?.length && request.initialInput === undefined) ? '' : request.initialInput ?? '');
1257
+ this.#editor.setText(request.filterable === true ? browserState?.query ?? ''
1258
+ : request.allowCustom === false || (request.options?.length && request.initialInput === undefined) ? '' : request.initialInput ?? '');
1113
1259
  this.#ac = null;
1114
1260
  const displaced = this.#displaceSurface();
1115
1261
  return new Promise((resolve) => {
1116
- const selected = Math.max(0, filteredPromptOptions(request, '').findIndex(option => (option.value ?? option.label) === request.initialValue) ?? 0);
1117
- const pending = { request, selected, checked: new Set((request.options ?? []).flatMap((option, index) => request.initialValues?.includes(option.value ?? option.label) ? [index] : [])), resolve, savedInput,
1262
+ const selected = Math.max(0, filteredPromptOptions(request, this.#editor.text).findIndex(option => (option.value ?? option.label) === (browserState?.selected ?? request.initialValue)));
1263
+ const pending = { sourceRequest, request, ...(renderDocument === undefined ? {} : { renderDocument }), selected, checked: new Set((request.options ?? []).flatMap((option, index) => request.initialValues?.includes(option.value ?? option.label) ? [index] : [])), resolve, savedInput,
1264
+ ...(browserState === undefined ? {} : { browserState }),
1118
1265
  ...(request.armMs !== undefined && request.armMs > 0 ? { armedUntil: Date.now() + request.armMs } : {}),
1119
1266
  ...(request.documentPosition === undefined ? {} : { documentAnchor: request.documentPosition, documentQuery: request.documentPosition.query,
1120
1267
  documentWordWrap: request.documentPosition.wordWrap, documentColumn: request.documentPosition.column }),
@@ -1137,7 +1284,7 @@ export class LocalTui {
1137
1284
  }
1138
1285
  if (this.#tty && (request.refreshDocument !== undefined || request.refreshDocumentSource !== undefined || request.refreshOptions !== undefined)) {
1139
1286
  const tick = setInterval(() => {
1140
- if (this.#disposed || this.#prompt === null)
1287
+ if (this.#disposed || this.#prompt === null || this.#selection.active)
1141
1288
  return;
1142
1289
  try {
1143
1290
  const detail = request.refreshDocument?.();
@@ -1147,16 +1294,17 @@ export class LocalTui {
1147
1294
  if (source !== undefined && source !== this.#prompt.request.documentSource) {
1148
1295
  const shift = (this.#prompt.request.documentSource?.firstLine ?? 1) - (source.firstLine ?? 1);
1149
1296
  const anchor = this.#prompt.documentAnchor;
1150
- this.#prompt = { ...this.#prompt, request: { ...this.#prompt.request, documentSource: source },
1297
+ this.#prompt = { ...this.#prompt, sourceRequest: { ...this.#prompt.sourceRequest, documentSource: source }, request: { ...this.#prompt.request, documentSource: source },
1151
1298
  ...(anchor === undefined ? {} : { documentAnchor: { ...anchor, row: Math.max(0, anchor.row + shift) } }) };
1152
1299
  changed = true;
1153
1300
  }
1154
- if (options !== undefined && options !== this.#prompt.request.options) {
1155
- this.#prompt = refreshPromptOptions(this.#prompt, options, this.#editor.text);
1301
+ if (options !== undefined && options !== this.#prompt.sourceRequest.options) {
1302
+ this.#prompt.sourceRequest = { ...this.#prompt.sourceRequest, options };
1303
+ this.#prompt = refreshPromptOptions(this.#prompt, localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)).options, this.#editor.text);
1156
1304
  changed = true;
1157
1305
  }
1158
1306
  if (detail !== undefined && detail !== this.#prompt.request.detail) {
1159
- this.#prompt = { ...this.#prompt, request: { ...this.#prompt.request, detail } };
1307
+ this.#prompt = { ...this.#prompt, sourceRequest: { ...this.#prompt.sourceRequest, detail }, request: { ...this.#prompt.request, detail } };
1160
1308
  changed = true;
1161
1309
  }
1162
1310
  if (changed)
@@ -1193,10 +1341,21 @@ export class LocalTui {
1193
1341
  });
1194
1342
  }
1195
1343
  replaceSession(events, presentations, status = 'idle', reason = 'open') {
1196
- if (reason !== 'refresh')
1344
+ if (reason !== 'refresh') {
1197
1345
  this.#fileReviewAbort?.abort();
1346
+ this.#closeFileBrowser();
1347
+ this.#clearTransientStatus();
1348
+ }
1198
1349
  this.#trajectory = null;
1199
1350
  const state = replayEvents(events, presentations);
1351
+ if (reason === 'refresh') {
1352
+ const pendingIds = new Set(state.nextTurnInbox.map(message => message.id));
1353
+ for (const id of this.#directInputIds)
1354
+ if (!pendingIds.has(id))
1355
+ this.#directInputIds.delete(id);
1356
+ }
1357
+ else
1358
+ this.#directInputIds.clear();
1200
1359
  // A notice does not turn a blank session into a document that /new needs
1201
1360
  // to separate. Refreshes are the same document.
1202
1361
  const boundary = reason === 'open' || (reason === 'new' && this.#state.blocks.some(block => block.kind !== 'notice'));
@@ -1212,10 +1371,13 @@ export class LocalTui {
1212
1371
  this.#plain.print();
1213
1372
  }
1214
1373
  setSession(info) {
1215
- if (info.id !== this.#sessionId)
1374
+ if (info.id !== this.#sessionId) {
1216
1375
  this.#fileReviewAbort?.abort();
1376
+ this.#closeFileBrowser();
1377
+ }
1217
1378
  this.#switchDraft(this.#inspected?.id ?? info.id);
1218
1379
  this.#sessionId = info.id;
1380
+ this.#drainPendingInput();
1219
1381
  this.#herdr.setSession(info.id);
1220
1382
  const title = info.title?.trim();
1221
1383
  this.#sessionTitle = title === undefined || title === '' ? undefined : title;
@@ -1287,6 +1449,7 @@ export class LocalTui {
1287
1449
  /** Currently applied preferences; sibling rows read these instead of the config references. */
1288
1450
  prefs() {
1289
1451
  return {
1452
+ uiLanguage: this.#uiLanguage,
1290
1453
  theme: this.#themeName,
1291
1454
  colors: this.#colors,
1292
1455
  motion: this.#motion,
@@ -1298,6 +1461,7 @@ export class LocalTui {
1298
1461
  terminalState: this.#terminalState,
1299
1462
  copyOnSelect: this.#copyOnSelect,
1300
1463
  mouseInteraction: this.#mouseInteraction,
1464
+ busyEnter: this.#busyEnter,
1301
1465
  pasteProtection: this.#pasteProtection,
1302
1466
  checkUpdates: this.#checkUpdates,
1303
1467
  startupChangelog: this.#startupChangelog,
@@ -1350,6 +1514,26 @@ export class LocalTui {
1350
1514
  this.#render();
1351
1515
  };
1352
1516
  }
1517
+ #takeQueuedInput() {
1518
+ const scope = this.#sessionId ?? '__default__';
1519
+ const inputs = this.#pendingInputs.session(scope).queued;
1520
+ const index = inputs.findIndex(item => !this.#pendingInputs.receipt(item)?.recovered);
1521
+ const queued = index < 0 ? undefined : inputs.splice(index, 1)[0];
1522
+ if (queued !== undefined)
1523
+ this.#pendingInputs.take(queued, scope);
1524
+ return queued;
1525
+ }
1526
+ #drainPendingInput() {
1527
+ if (this.#pending === null || this.#disposed)
1528
+ return;
1529
+ const queued = this.#takeQueuedInput();
1530
+ if (queued === undefined)
1531
+ return;
1532
+ const pending = this.#pending;
1533
+ this.#pending = null;
1534
+ pending.offAbort?.();
1535
+ pending.resolve(queued);
1536
+ }
1353
1537
  readInput(signal) {
1354
1538
  if (this.#pending !== null)
1355
1539
  return Promise.reject(new Error('omdsh-tui: input read already in flight'));
@@ -1365,7 +1549,7 @@ export class LocalTui {
1365
1549
  return this.#plain.readline(signal);
1366
1550
  // Lines submitted while a turn was still running were queued instead of
1367
1551
  // dropped; serve the oldest before waiting for fresh input.
1368
- const queued = this.#queuedSubmissions.shift();
1552
+ const queued = this.#takeQueuedInput();
1369
1553
  if (queued !== undefined) {
1370
1554
  this.#render();
1371
1555
  return Promise.resolve(queued);
@@ -1387,20 +1571,83 @@ export class LocalTui {
1387
1571
  }
1388
1572
  /** Compatibility helper for provider-level text editing tests and embedders. */
1389
1573
  async readline() {
1390
- return (await this.readInput())?.text ?? null;
1391
- }
1392
- restoreInput(submission) {
1574
+ const submission = await this.readInput();
1575
+ if (submission !== null)
1576
+ this.#pendingInputs.finish(submission);
1577
+ return submission?.text ?? null;
1578
+ }
1579
+ restoreInput(submission, sessionId) {
1580
+ const origin = this.#pendingInputs.receipt(submission)?.sessionId;
1581
+ if (origin !== '__default__')
1582
+ sessionId ??= origin;
1583
+ if (sessionId !== undefined && sessionId !== this.#draftScope || this.#prompt !== null || this.#disposed) {
1584
+ const target = sessionId ?? this.#draftScope;
1585
+ const current = target === this.#draftScope;
1586
+ const editor = new InputEditor();
1587
+ const images = new ComposerImages({ editor, notice: text => this.notice(text) });
1588
+ const files = new ComposerFiles({ text: () => editor.text,
1589
+ append: marker => { editor.setCursor(editor.text.length); editor.handle({ type: 'text', value: (editor.text === '' ? '' : ' ') + marker }); } });
1590
+ let existing = current ? this.#captureDraft() : target === undefined ? undefined : this.#unsavedDrafts.get(target);
1591
+ try {
1592
+ if (target !== undefined)
1593
+ existing ??= this.#draftStore?.load(target);
1594
+ }
1595
+ catch (error) {
1596
+ this.#reportDraftError('restore', error);
1597
+ }
1598
+ if (existing !== undefined) {
1599
+ images.replace({ text: existing.editor.text, images: existing.images });
1600
+ editor.restore(existing.editor);
1601
+ files.restoreSnapshot(existing.files);
1602
+ }
1603
+ images.restore(submission, files.adopt(submission.files ?? []));
1604
+ const draft = { editor: editor.snapshot(), images: images.copies(), files: files.snapshot(editor.text), literal: existing?.literal === true || submission.literal === true };
1605
+ if (current && this.#prompt !== null) {
1606
+ const answer = this.#editor.snapshot();
1607
+ this.#restoreDraft(draft);
1608
+ this.#prompt.savedInput = draft.editor;
1609
+ this.#editor.restore(answer);
1610
+ this.#draftDirty = true;
1611
+ this.#flushDraft();
1612
+ if (this.#draftStore === undefined || !this.#unsavedDrafts.has(this.#pendingScope()))
1613
+ this.#pendingInputs.finish(submission);
1614
+ return;
1615
+ }
1616
+ if (target === undefined)
1617
+ return;
1618
+ this.#unsavedDrafts.set(target, draft);
1619
+ try {
1620
+ if (this.#draftStore !== undefined) {
1621
+ this.#draftStore.save(target, draft);
1622
+ this.#unsavedDrafts.delete(target);
1623
+ }
1624
+ }
1625
+ catch (error) {
1626
+ this.#reportDraftError('save', error);
1627
+ return;
1628
+ }
1629
+ this.#pendingInputs.finish(submission);
1630
+ return;
1631
+ }
1393
1632
  this.#literalInput ||= submission.literal === true;
1394
- this.#images.restore(submission);
1395
- for (const file of submission.files ?? [])
1396
- this.#files.add(file);
1633
+ this.#images.restore(submission, this.#files.adopt(submission.files ?? []));
1634
+ this.#draftDirty = true;
1635
+ this.#flushDraft();
1636
+ if (this.#draftStore === undefined || !this.#unsavedDrafts.has(this.#pendingScope()))
1637
+ this.#pendingInputs.finish(submission);
1397
1638
  this.#refreshAutocomplete();
1398
1639
  if (this.#tty)
1399
1640
  this.#render();
1400
1641
  }
1401
- resolveQueueEdit(submission) {
1642
+ resolveQueueEdit(submission, request) {
1643
+ if (request !== undefined && (request !== this.#queueRecall?.request || request.signal.aborted)) {
1644
+ if (submission !== null)
1645
+ this.restoreInput(submission, request.sessionId);
1646
+ return;
1647
+ }
1402
1648
  if (!this.#queueEditPending)
1403
1649
  return;
1650
+ this.#queueRecall = undefined;
1404
1651
  this.#queueEditPending = false;
1405
1652
  if (submission === null) {
1406
1653
  if (this.#editor.text === '' && this.#images.count === 0 && this.#queueEditNewer?.length === 0) {
@@ -1410,25 +1657,21 @@ export class LocalTui {
1410
1657
  }
1411
1658
  if (this.#queueEditNewer === null)
1412
1659
  this.#queueEditNewer = [];
1413
- if (this.#editor.text !== '' || this.#images.count > 0) {
1660
+ if (this.#hasComposerContent()) {
1414
1661
  this.#queueEditNewer.unshift(this.#currentSubmission());
1415
1662
  }
1416
1663
  this.#replaceInput(submission);
1417
1664
  }
1418
1665
  #currentSubmission() {
1419
- const files = this.#files.copies();
1420
- return {
1421
- text: this.#files.text(this.#editor.expandedText),
1422
- ...(this.#literalInput || this.#editor.startsWithPaste ? { literal: true } : {}),
1423
- images: this.#images.copies(),
1424
- ...(files.length === 0 ? {} : { files }),
1425
- };
1666
+ return this.#buildSubmission(this.#editor.expandedText).submission;
1426
1667
  }
1427
1668
  #replaceInput(submission) {
1428
1669
  this.#literalInput = submission.literal === true;
1429
- this.#images.replace(submission);
1430
- this.#files.restore(submission.files);
1670
+ this.#files.clear();
1671
+ this.#images.replace(submission, this.#files.adopt(submission.files ?? []));
1431
1672
  this.#historyIndex = 0;
1673
+ this.#draftDirty = true;
1674
+ this.#flushDraft();
1432
1675
  this.#refreshAutocomplete();
1433
1676
  if (this.#tty)
1434
1677
  this.#render();
@@ -1438,30 +1681,39 @@ export class LocalTui {
1438
1681
  return () => { this.#interrupts.delete(listener); };
1439
1682
  }
1440
1683
  pendingInputs = {
1684
+ receipt: submission => this.#pendingInputs.receipt(submission),
1685
+ prepare: (submission, messageId) => this.#pendingInputs.prepare(submission, messageId),
1686
+ acknowledge: submission => { this.#pendingInputs.finish(submission); if (this.#tty && !this.#disposed)
1687
+ this.#render(); },
1688
+ reconcile: (sessionId, accepted) => this.#pendingInputs.reconcile(sessionId, accepted),
1441
1689
  list: () => {
1442
1690
  if (this.#disposed)
1443
1691
  return [];
1444
1692
  if (this.#queueEditNewer !== null || this.#queueEditPending)
1445
1693
  throw new Error('Finish editing the queued follow-up with Enter before opening Queue.');
1446
1694
  return this.#queuedSubmissions.map(submission => {
1447
- let id = this.#queuedInputIds.get(submission);
1448
- if (id === undefined) {
1449
- id = `local-${++this.#queuedInputSerial}`;
1450
- this.#queuedInputIds.set(submission, id);
1451
- }
1452
- return { id, submission };
1695
+ const receipt = this.#pendingInputs.identify(submission, this.#pendingScope());
1696
+ return { id: receipt.id, submission, ...(receipt.recovered ? { recovered: true } : {}) };
1453
1697
  });
1454
1698
  },
1455
1699
  update: (id, change) => {
1456
1700
  if (this.#disposed)
1457
1701
  return false;
1458
- const index = this.#queuedSubmissions.findIndex(item => this.#queuedInputIds.get(item) === id);
1702
+ const index = this.#queuedSubmissions.findIndex(item => this.#pendingInputs.receipt(item)?.id === id);
1459
1703
  if (index < 0)
1460
1704
  return false;
1461
- if (change.kind === 'remove')
1705
+ if (change.kind === 'release') {
1706
+ const submission = this.#queuedSubmissions[index];
1707
+ this.#queuedSubmissions.splice(index, 1);
1708
+ this.#dispatchInput(submission);
1709
+ }
1710
+ else if (change.kind === 'remove')
1462
1711
  this.#queuedSubmissions.splice(index, 1);
1463
1712
  else if (change.kind === 'replace') {
1464
- this.#queuedInputIds.set(change.submission, id);
1713
+ const old = this.#pendingInputs.receipt(this.#queuedSubmissions[index]);
1714
+ const receipt = this.#pendingInputs.identify(change.submission, this.#pendingScope());
1715
+ receipt.id = id;
1716
+ receipt.recovered = old.recovered ?? false;
1465
1717
  this.#queuedSubmissions[index] = change.submission;
1466
1718
  }
1467
1719
  else {
@@ -1477,6 +1729,46 @@ export class LocalTui {
1477
1729
  return true;
1478
1730
  },
1479
1731
  };
1732
+ setContinueHandler(handler) {
1733
+ this.#continueHandler = handler;
1734
+ return () => { if (this.#continueHandler === handler)
1735
+ this.#continueHandler = undefined; };
1736
+ }
1737
+ #canContinue() {
1738
+ if (this.#continueHandler === undefined || this.#continuing || this.#disposed || this.#sessionId === undefined
1739
+ || this.#sessionControls?.continuation === undefined || this.#inspected !== undefined || this.#state.status !== 'idle'
1740
+ || this.#prompt !== null || this.#hasComposerContent() || this.#pasteInFlight > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
1741
+ return false;
1742
+ const pending = this.#pendingInputs.session(this.#sessionId);
1743
+ return pending.queued.length === 0 && pending.sending.size === 0
1744
+ && this.#state.nextTurnInbox.length === 0 && !this.#state.nextStepInbox.some(message => message.source.kind === 'user');
1745
+ }
1746
+ setGoalHandler(handler) {
1747
+ this.#goalHandler = handler;
1748
+ this.#syncSettingsActions();
1749
+ return () => { if (this.#goalHandler === handler) {
1750
+ this.#goalAbort?.abort();
1751
+ this.#goalHandler = undefined;
1752
+ this.#syncSettingsActions();
1753
+ } };
1754
+ }
1755
+ #openGoal() {
1756
+ if (this.#inspected !== undefined)
1757
+ return;
1758
+ if (this.#goalHandler === undefined) {
1759
+ this.notice('Goal controls are unavailable in this profile.');
1760
+ return;
1761
+ }
1762
+ if (this.#goalAbort !== undefined)
1763
+ return;
1764
+ const abort = new AbortController();
1765
+ this.#goalAbort = abort;
1766
+ void this.#goalHandler(abort.signal).catch(error => {
1767
+ if (!abort.signal.aborted && !this.#disposed)
1768
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1769
+ }).finally(() => { if (this.#goalAbort === abort)
1770
+ this.#goalAbort = undefined; });
1771
+ }
1480
1772
  setQueueHandler(handler) {
1481
1773
  this.#queueHandler = handler;
1482
1774
  return () => { if (this.#queueHandler === handler) {
@@ -1496,6 +1788,10 @@ export class LocalTui {
1496
1788
  this.#inspects.add(listener);
1497
1789
  return () => { this.#inspects.delete(listener); };
1498
1790
  }
1791
+ onInspectWorkflow(listener) {
1792
+ this.#workflowInspects.add(listener);
1793
+ return () => { this.#workflowInspects.delete(listener); };
1794
+ }
1499
1795
  onInspectClose(listener) {
1500
1796
  this.#inspectCloses.add(listener);
1501
1797
  return () => { this.#inspectCloses.delete(listener); };
@@ -1504,11 +1800,6 @@ export class LocalTui {
1504
1800
  this.#inspectSubmits.add(listener);
1505
1801
  return () => { this.#inspectSubmits.delete(listener); };
1506
1802
  }
1507
- setSteerHandler(handler) {
1508
- this.#steerHandler = handler;
1509
- return () => { if (this.#steerHandler === handler)
1510
- this.#steerHandler = undefined; };
1511
- }
1512
1803
  /**
1513
1804
  * Last-resort restore for a process that exits without disposing: leave the
1514
1805
  * alternate screen, give every mode back and show the cursor. Writes to a TTY
@@ -1529,10 +1820,11 @@ export class LocalTui {
1529
1820
  return this.#disposeTask ?? this.#herdr.release();
1530
1821
  this.#flushPasteBurst(true);
1531
1822
  this.#flushDraft();
1823
+ this.#pendingInputs.flush(this.#pendingScope());
1532
1824
  this.#disposed = true;
1533
- this.#steerHandler = undefined;
1534
1825
  this.#imagePreviewAbort?.abort();
1535
1826
  this.#fileReviewAbort?.abort();
1827
+ this.#closeFileBrowser();
1536
1828
  this.#asideAbort?.abort();
1537
1829
  for (const directory of this.#imageTemporary) {
1538
1830
  try {
@@ -1541,8 +1833,13 @@ export class LocalTui {
1541
1833
  catch { /* OS may still hold the opened file. */ }
1542
1834
  }
1543
1835
  this.#imageTemporary.clear();
1836
+ this.#goalAbort?.abort();
1837
+ this.#goalAbort = undefined;
1838
+ this.#goalHandler = undefined;
1544
1839
  this.#queueHotkeyAbort?.abort();
1545
1840
  this.#queueHandler = undefined;
1841
+ this.#continueHandler = undefined;
1842
+ this.#cancelQueueRecall();
1546
1843
  if (this.#notificationTimer !== undefined)
1547
1844
  clearTimeout(this.#notificationTimer);
1548
1845
  this.#notificationQueue.clear();
@@ -1590,7 +1887,7 @@ export class LocalTui {
1590
1887
  this.#session.dispose();
1591
1888
  this.#term.output.write('\x1b[?25h\r\n');
1592
1889
  if (this.#resumeHintRequested && this.#sessionId !== undefined) {
1593
- this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
1890
+ this.#term.output.write(`\r\n${uiText(resolveUiLanguage(this.#uiLanguage), 'Resume this session with {command}', { command: `${APP_NAME} --resume ${this.#sessionId}` })}\r\n`);
1594
1891
  }
1595
1892
  // A tty stdin keeps the event loop alive after the tree disposes;
1596
1893
  // release the descriptor so natural completion can exit the process.
@@ -1638,10 +1935,10 @@ export class LocalTui {
1638
1935
  /** Countdown shown on the working row while a retry or a compaction is pending. */
1639
1936
  #workingLabel() {
1640
1937
  if (this.#retryDeadline !== undefined) {
1641
- return `Retrying in ${Math.max(0, Math.ceil((this.#retryDeadline - Date.now()) / 1000))}s`;
1938
+ return uiText(resolveUiLanguage(this.#uiLanguage), 'Retrying in {seconds}s', { seconds: Math.max(0, Math.ceil((this.#retryDeadline - Date.now()) / 1000)) });
1642
1939
  }
1643
1940
  if (this.#state.status === 'compacting' && this.#compactionStartedAt !== undefined) {
1644
- return `Compacting ${Math.max(0, Math.floor((Date.now() - this.#compactionStartedAt) / 1000))}s`;
1941
+ return uiText(resolveUiLanguage(this.#uiLanguage), 'Compacting {seconds}s', { seconds: Math.max(0, Math.floor((Date.now() - this.#compactionStartedAt) / 1000)) });
1645
1942
  }
1646
1943
  return undefined;
1647
1944
  }
@@ -1762,7 +2059,13 @@ export class LocalTui {
1762
2059
  clearTimeout(this.#draftTimer);
1763
2060
  this.#draftTimer = undefined;
1764
2061
  const id = this.#draftScope;
1765
- if (id === undefined || this.#draftStore === undefined || !this.#draftDirty && !this.#unsavedDrafts.has(id))
2062
+ if (id === undefined)
2063
+ return;
2064
+ if (this.#draftStore === undefined) {
2065
+ this.#unsavedDrafts.set(id, this.#captureDraft());
2066
+ return;
2067
+ }
2068
+ if (!this.#draftDirty && !this.#unsavedDrafts.has(id))
1766
2069
  return;
1767
2070
  const draft = this.#captureDraft();
1768
2071
  this.#draftDirty = false;
@@ -1784,7 +2087,7 @@ export class LocalTui {
1784
2087
  }
1785
2088
  #clearDraft() {
1786
2089
  this.#historyDraft = undefined;
1787
- if (this.#draftStore === undefined || this.#draftScope === undefined)
2090
+ if (this.#draftScope === undefined)
1788
2091
  return;
1789
2092
  // A sent or explicitly discarded input must not return after a quick exit.
1790
2093
  this.#draftFingerprint = this.#draftKey();
@@ -1792,12 +2095,20 @@ export class LocalTui {
1792
2095
  this.#flushDraft();
1793
2096
  }
1794
2097
  #switchDraft(id) {
1795
- if (this.#draftStore === undefined || id === this.#draftScope)
2098
+ if (id === this.#draftScope)
1796
2099
  return;
2100
+ this.#goalAbort?.abort();
2101
+ this.#goalAbort = undefined;
2102
+ this.#queueHotkeyAbort?.abort();
2103
+ this.#queueHotkeyAbort = undefined;
2104
+ this.#cancelQueueRecall();
1797
2105
  this.#flushPasteBurst(true);
1798
2106
  this.#finishPrompt(null);
1799
2107
  this.#flushDraft();
2108
+ this.#pendingInputs.flush(this.#pendingScope());
1800
2109
  const initialInput = this.#draftScope === undefined && this.#editor.text !== '' ? this.#captureDraft() : undefined;
2110
+ if (this.#draftScope === undefined && id !== undefined)
2111
+ this.#pendingInputs.bindInitial(id);
1801
2112
  this.#draftScope = id;
1802
2113
  this.#historyIndex = 0;
1803
2114
  this.#historyDraft = undefined;
@@ -1810,7 +2121,7 @@ export class LocalTui {
1810
2121
  let restoreError;
1811
2122
  if (id !== undefined) {
1812
2123
  try {
1813
- const draft = this.#unsavedDrafts.get(id) ?? this.#draftStore.load(id);
2124
+ const draft = this.#unsavedDrafts.get(id) ?? this.#draftStore?.load(id);
1814
2125
  if (draft !== undefined)
1815
2126
  this.#restoreDraft(draft);
1816
2127
  }
@@ -1826,6 +2137,7 @@ export class LocalTui {
1826
2137
  this.#reportDraftError('restore', restoreError);
1827
2138
  }
1828
2139
  #render() {
2140
+ this.#pendingInputs.flush(this.#pendingScope());
1829
2141
  this.#watchDraft();
1830
2142
  if (this.#streamRenderTimer !== null) {
1831
2143
  clearTimeout(this.#streamRenderTimer);
@@ -1848,16 +2160,16 @@ export class LocalTui {
1848
2160
  imageCellSize: this.#imageCellSize,
1849
2161
  mathMode: this.#mathMode,
1850
2162
  mermaidMode: this.#mermaidMode,
1851
- hideThinking: this.#hideThinking,
2163
+ hideThinking: true,
1852
2164
  height: this.#term.height(),
1853
2165
  model: this.#model,
1854
2166
  ...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
1855
2167
  input: this.#editor.text,
1856
2168
  inputCursor: this.#editor.cursor,
1857
2169
  inputImages: this.#images.count,
1858
- queuedSubmissions: this.#queueEditNewer === null
1859
- ? this.#queuedSubmissions
1860
- : [...this.#queuedSubmissions, ...this.#queueEditNewer],
2170
+ canContinue: this.#canContinue(),
2171
+ queuedSubmissionScope: this.#pendingScope(),
2172
+ queuedSubmissions: [...this.#queuedSubmissions, ...(this.#queueEditNewer ?? [])].map(submission => this.#pendingInputs.receipt(submission)?.recovered ? { ...submission, recovered: true } : submission),
1861
2173
  colors: this.#colors,
1862
2174
  pwd: this.#pwd,
1863
2175
  ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
@@ -1868,12 +2180,16 @@ export class LocalTui {
1868
2180
  spinnerFrame: this.#spinner,
1869
2181
  motion: this.#motion,
1870
2182
  colorDepth: this.#colorDepth,
2183
+ compactLinks: this.#compactLinks,
2184
+ language: resolveUiLanguage(this.#uiLanguage),
1871
2185
  themeName: this.#themeName,
1872
2186
  terminal: this.#terminalColors,
1873
2187
  ...(workingLabel === undefined ? {} : { workingLabel }),
1874
- ...(this.#transientStatus === undefined ? {} : { transientStatus: this.#transientStatus }),
2188
+ ...(this.#transientStatus !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), this.#transientStatus) }
2189
+ : this.#selection.active ? { selectionActions: this.#canQuoteSelection() ? 'copy-quote' : 'copy' }
2190
+ : this.#processSelection !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), 'Process · ↑↓ select · Enter toggle · {key} actions · Home title · Esc return', { key: keysForAction(this.#keybindings, 'tool-actions') }) } : {}),
1875
2191
  scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
1876
- ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start' }),
2192
+ ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start', ...(this.#focusBlockOffset === undefined ? {} : { focusBlockOffset: this.#focusBlockOffset }) }),
1877
2193
  ...(this.#transcriptSearch === null ? {} : {
1878
2194
  transcriptSearch: {
1879
2195
  query: this.#transcriptSearch.query,
@@ -1884,9 +2200,14 @@ export class LocalTui {
1884
2200
  toolsExpanded: this.#toolsExpanded,
1885
2201
  expandedOutputs: this.#expandedOutputs,
1886
2202
  expandedTools: this.#detailedToolIds(),
2203
+ rawTools: this.#detailedToolIds(false),
2204
+ expandedReasoning: this.#visibleReasoning(),
2205
+ ...(this.#processSelection === undefined ? {} : { selectedProcess: this.#processSelection }),
1887
2206
  toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
1888
2207
  turnDetailsKey: keysForAction(this.#keybindings, 'toggle-tools'),
1889
- ...(this.#steerHandler === undefined ? {} : { steerKey: keysForAction(this.#keybindings, 'steer-turn') }),
2208
+ busyEnter: this.#queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#busyEnter,
2209
+ steerKey: keysForAction(this.#keybindings, 'steer-turn'),
2210
+ queueKey: keysForAction(this.#keybindings, 'follow-up'),
1890
2211
  openedGroups: this.#openedGroups,
1891
2212
  commands: this.#commands(),
1892
2213
  modelReady: this.#modelReady,
@@ -1897,6 +2218,7 @@ export class LocalTui {
1897
2218
  ...(this.#subagentSelection === undefined ? {} : { subagentSelection: this.#subagentSelection }),
1898
2219
  ...(this.#inspected === undefined ? {} : { inspected: this.#inspected }),
1899
2220
  ...(this.#trajectory === null ? {} : { trajectory: this.#trajectory }),
2221
+ ...(this.#fileBrowser === null ? {} : { fileBrowser: this.#fileBrowser.state }),
1900
2222
  ...(this.#aside === undefined ? {} : { aside: this.#aside }),
1901
2223
  statusBar: this.#statusBar,
1902
2224
  ...(this.#prompt === null ? {} : { promptSelector: this.#prompt }),
@@ -1909,6 +2231,9 @@ export class LocalTui {
1909
2231
  : { lines: [] };
1910
2232
  this.#focusBlock = undefined;
1911
2233
  this.#focusBlockEdge = undefined;
2234
+ this.#focusBlockOffset = undefined;
2235
+ this.#promptActions = frame.promptActions ?? [];
2236
+ this.#promptChoices = frame.promptChoices ?? [];
1912
2237
  this.#promptDocument = frame.promptDocument;
1913
2238
  if (this.#prompt !== null && this.#prompt.documentScroll !== Number.POSITIVE_INFINITY && frame.promptDocument?.position !== undefined) {
1914
2239
  this.#prompt = { ...this.#prompt, documentAnchor: frame.promptDocument.position };
@@ -1916,8 +2241,8 @@ export class LocalTui {
1916
2241
  this.#syncScroll(frame.transcript);
1917
2242
  this.#jumpToLatest = frame.jumpToLatest;
1918
2243
  this.#fileActionFrame = frame.transcript === undefined || frame.transientSurface === 'overlay' ? undefined : frame;
1919
- this.#setMouseTracking(frame.transcript !== undefined && frame.transientSurface !== 'overlay'
1920
- && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0));
2244
+ this.#setMouseTracking(this.#fileBrowser !== null || this.#trajectory !== null || this.#documentMouseEnabled() || this.#promptChoices.length > 0 || this.#promptActions.length > 0 || (frame.transcript !== undefined && frame.transientSurface !== 'overlay'
2245
+ && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0 || (frame.transcript.disclosures?.length ?? 0) > 0)));
1921
2246
  // A run opening or folding reshapes rows above the screen in place; the
1922
2247
  // renderer's frozen boundary is a row index and has to be told, or a fold
1923
2248
  // blanks the screen and a large one replays the transcript into history.
@@ -1996,14 +2321,38 @@ export class LocalTui {
1996
2321
  this.#toolsExpanded = false;
1997
2322
  this.#openedGroups.clear();
1998
2323
  this.#detailedTurns.clear();
2324
+ this.#expandedOutputs.clear();
2325
+ this.#expandedToolRows.clear();
2326
+ this.#toolReadingPositions.clear();
2327
+ this.#expandedReasoning.clear();
2328
+ this.#closedReasoning.clear();
2329
+ this.#processSelection = undefined;
2330
+ }
2331
+ #documentMouseEnabled() {
2332
+ const prompt = this.#prompt;
2333
+ return prompt !== null && prompt.feedback !== true && this.#promptDocument !== undefined
2334
+ && (prompt.request.presentation === 'document' || prompt.request.presentation === 'plan-review');
2335
+ }
2336
+ #scrollPromptDocument(row) {
2337
+ if (this.#prompt === null)
2338
+ return;
2339
+ this.#prompt = {
2340
+ ...this.#prompt,
2341
+ documentAnchor: undefined,
2342
+ documentTarget: undefined,
2343
+ documentScroll: row === Number.POSITIVE_INFINITY ? row : Math.max(0, Math.min(row, this.#promptDocument?.maxStart ?? 0)),
2344
+ };
2345
+ this.#render();
1999
2346
  }
2000
2347
  /** Capture transcript navigation; overlays and disposal restore terminal ownership. */
2001
2348
  #setMouseTracking(enabled) {
2002
2349
  enabled = enabled && this.#mouseInteraction !== 'native'
2003
2350
  && (this.#mouseInteraction === 'tui' || this.#tmuxMouseEnabled !== false);
2004
2351
  if (!enabled) {
2352
+ this.#mouseActionPress = undefined;
2005
2353
  this.#selection.clear();
2006
2354
  this.#jumpPress = undefined;
2355
+ this.#disclosurePress = undefined;
2007
2356
  }
2008
2357
  if (!this.#tty || enabled === this.#mouseTracking)
2009
2358
  return;
@@ -2035,7 +2384,7 @@ export class LocalTui {
2035
2384
  * or go back to the live tail once nothing is open any more.
2036
2385
  */
2037
2386
  #inspect(target) {
2038
- const open = this.#toolsExpanded || this.#openedGroups.size > 0;
2387
+ const open = this.#toolsExpanded || this.#openedGroups.size > 0 || this.#expandedOutputs.size > 0;
2039
2388
  if (!open) {
2040
2389
  this.#inspecting = false;
2041
2390
  this.#followTail();
@@ -2079,9 +2428,9 @@ export class LocalTui {
2079
2428
  }
2080
2429
  }
2081
2430
  #routeInputBatch(events) {
2082
- if (this.#pasteProtection && this.#state.status !== 'compacting' && !this.#paste && this.#pasteInFlight === 0
2431
+ if (this.#pasteProtection && !this.#paste && this.#pasteInFlight === 0
2083
2432
  && this.#prompt === null && this.#settings === null
2084
- && this.#search === null && this.#trajectory === null && this.#transcriptSearch === null
2433
+ && this.#search === null && this.#trajectory === null && this.#fileBrowser === null && this.#transcriptSearch === null
2085
2434
  && events.every(event => event.type === 'text' || (event.type === 'key' && ['enter', 'ctrl+j', 'tab'].includes(event.id)))) {
2086
2435
  if (this.#pasteBurstTimer !== undefined)
2087
2436
  clearTimeout(this.#pasteBurstTimer);
@@ -2104,8 +2453,8 @@ export class LocalTui {
2104
2453
  this.#dispatch(event);
2105
2454
  return;
2106
2455
  }
2107
- const composer = this.#state.status !== 'compacting' && this.#prompt === null && this.#settings === null
2108
- && this.#search === null && this.#trajectory === null && this.#transcriptSearch === null;
2456
+ const composer = this.#prompt === null && this.#settings === null
2457
+ && this.#search === null && this.#trajectory === null && this.#fileBrowser === null && this.#transcriptSearch === null;
2109
2458
  if (this.#paste || this.#pasteInFlight > 0 || !this.#pasteProtection || !composer
2110
2459
  || event.type === 'paste-start' || event.type === 'paste-end') {
2111
2460
  this.#flushPasteBurst(true);
@@ -2156,6 +2505,10 @@ export class LocalTui {
2156
2505
  });
2157
2506
  }
2158
2507
  async #acceptPastedText(text) {
2508
+ if (this.#fileBrowser !== null) {
2509
+ this.#applyFileBrowserInput({ type: 'text', value: text });
2510
+ return;
2511
+ }
2159
2512
  if (this.#prompt !== null) {
2160
2513
  const prompt = this.#prompt;
2161
2514
  if (prompt.documentInput !== undefined) {
@@ -2254,14 +2607,198 @@ export class LocalTui {
2254
2607
  if (this.#tty)
2255
2608
  this.#render();
2256
2609
  }
2610
+ /** Resolve only current painted rows, never reinterpret a frozen native-history snapshot. */
2611
+ #disclosureAt(row, column, action = false) {
2612
+ const frame = this.#fileActionFrame;
2613
+ const document = this.#renderer.documentRowAt(row);
2614
+ const source = frame?.documentRows;
2615
+ if (document === undefined || source == null || document < source.documentStart || document >= source.documentEnd)
2616
+ return;
2617
+ const target = frame?.transcript?.disclosures?.find(item => item.row === document && (action
2618
+ ? item.actionColumn !== undefined && column >= item.actionColumn && column < item.actionColumn + (item.key.startsWith('output:') ? 6 : 5)
2619
+ : column >= item.column && column < item.column + item.width));
2620
+ const expected = frame?.lines[source.frameStart + document - source.documentStart];
2621
+ const actual = this.#renderer.viewport()[row];
2622
+ if (target === undefined || expected === undefined || actual === undefined
2623
+ || stripAnsi(sanitizeDisplayLine(expected)).trimEnd() !== stripAnsi(actual).trimEnd())
2624
+ return;
2625
+ return target;
2626
+ }
2627
+ /** Preview text and spinner frames may change while a click stays on the same item. */
2628
+ #pressedDisclosureAt(row, column) {
2629
+ const pressed = this.#disclosurePress;
2630
+ if (pressed === undefined || pressed.row !== row)
2631
+ return;
2632
+ if (pressed.target.key.startsWith('output:') && Math.abs(pressed.column - column) > 1)
2633
+ return;
2634
+ const target = this.#disclosureAt(row, column);
2635
+ if (target?.key === pressed.target.key && target.row === pressed.target.row
2636
+ && target.column === pressed.target.column && target.width === pressed.target.width)
2637
+ return target;
2638
+ }
2639
+ #activateDisclosure(target) {
2640
+ const output = this.#state.blocks[target.block];
2641
+ if (output?.kind === 'commandOutput' && target.key === `output:${target.block}`) {
2642
+ if (this.#expandedOutputs.has(output))
2643
+ this.#expandedOutputs.delete(output);
2644
+ else
2645
+ this.#expandedOutputs.add(output);
2646
+ this.#inspect(target.block);
2647
+ this.#focusBlock = target.block;
2648
+ this.#focusBlockEdge = 'start';
2649
+ this.#render();
2650
+ return;
2651
+ }
2652
+ const groups = turnGroups(this.#state);
2653
+ const group = groups.find(item => target.group === undefined
2654
+ ? target.block >= item.start && target.block < item.until : item.key === target.group);
2655
+ if (group === undefined)
2656
+ return;
2657
+ const block = this.#state.blocks[target.block];
2658
+ if (target.group === undefined && target.key !== (block?.kind === 'tool' ? `call:${block.callId}`
2659
+ : block?.kind === 'assistant' ? `thought:${reasoningKey(block)}` : undefined))
2660
+ return;
2661
+ const scope = this.#detailScope(group);
2662
+ for (const member of groups)
2663
+ if (this.#detailScope(member) === scope)
2664
+ this.#openedGroups.add(member.key);
2665
+ this.#processSelection = this.#processRows().includes(target.block) ? target.block : this.#processRows()[0];
2666
+ this.#ac = null;
2667
+ this.#inspect(target.block);
2668
+ if (target.group === undefined)
2669
+ this.#toggleProcessItem(target.block, 'enter');
2670
+ else {
2671
+ this.#focusBlock = target.block;
2672
+ this.#focusBlockEdge = 'start';
2673
+ this.#render();
2674
+ }
2675
+ }
2676
+ #mouseActionAt(row, column) {
2677
+ const frame = this.#fileActionFrame;
2678
+ const viewport = this.#renderer.viewport();
2679
+ for (const target of frame?.selectionActions ?? []) {
2680
+ const physical = viewport.length - (frame.lines.length - target.row);
2681
+ if (row === physical && column >= target.column && column < target.column + target.width
2682
+ && this.#selection.active && this.#selection.valid(viewport)
2683
+ && stripAnsi(viewport[row] ?? '').trimEnd() === stripAnsi(frame.lines[target.row] ?? '').trimEnd()) {
2684
+ return { ...target, row: physical, key: target.action };
2685
+ }
2686
+ }
2687
+ const tool = this.#disclosureAt(row, column, true);
2688
+ if (tool?.actionColumn !== undefined)
2689
+ return { row, column: tool.actionColumn, width: tool.key.startsWith('output:') ? 6 : 5, key: tool.key, action: 'tool', block: tool.block };
2690
+ return undefined;
2691
+ }
2257
2692
  #handleMouse(event) {
2258
2693
  if (!this.#mouseTracking || this.#paste || this.#pasteInFlight > 0)
2259
2694
  return;
2695
+ if (this.#trajectory !== null || this.#fileBrowser !== null) {
2696
+ this.#handleTrajectoryMouse(event);
2697
+ return;
2698
+ }
2699
+ const promptAction = this.#promptActions.find(action => event.row === action.row && event.column >= action.column && event.column < action.column + action.width);
2700
+ if (this.#promptActionPress !== undefined) {
2701
+ const pressed = this.#promptActionPress;
2702
+ if (event.action === 'release') {
2703
+ this.#promptActionPress = undefined;
2704
+ this.#promptChoicePress = undefined;
2705
+ if (event.button === 'left' && !event.shift && promptAction?.index === pressed.index && promptAction.key === pressed.key
2706
+ && promptAction.row === pressed.row && promptAction.column === pressed.column && this.#prompt !== null) {
2707
+ if (pressed.key === undefined) {
2708
+ this.#prompt = { ...this.#prompt, selected: pressed.index, documentInput: undefined };
2709
+ this.#handlePlanReview({ type: 'key', id: 'enter' }, this.#prompt);
2710
+ }
2711
+ else if (pressed.selectedValue === (filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected]?.value
2712
+ ?? filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected]?.label)) {
2713
+ this.#handlePrompt({ type: 'key', id: pressed.key });
2714
+ }
2715
+ }
2716
+ }
2717
+ else if (event.action === 'drag' && (promptAction?.index !== pressed.index || promptAction.key !== pressed.key
2718
+ || promptAction.row !== pressed.row || promptAction.column !== pressed.column || event.shift)) {
2719
+ this.#promptActionPress = { ...pressed, index: -2 };
2720
+ }
2721
+ return;
2722
+ }
2723
+ if (event.button === 'left' && event.action === 'press' && !event.shift && promptAction !== undefined) {
2724
+ this.#selection.clear();
2725
+ const selected = this.#prompt === null ? undefined : filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected];
2726
+ this.#promptActionPress = { ...promptAction, selectedValue: selected?.value ?? selected?.label };
2727
+ return;
2728
+ }
2729
+ if (this.#promptChoices.length > 0 && event.button === 'left') {
2730
+ const choice = this.#promptChoices.find(item => item.row === event.row && event.column >= item.column && event.column < item.column + item.width);
2731
+ if (event.action === 'press') {
2732
+ const prompt = this.#prompt;
2733
+ if (prompt?.request.wait !== undefined && prompt.waitHeld !== true) {
2734
+ prompt.request.wait.hold();
2735
+ prompt.offTick?.();
2736
+ this.#prompt = { ...prompt, waitHeld: true };
2737
+ }
2738
+ this.#promptChoicePress = choice === undefined ? undefined : { value: choice.value, row: choice.row, dragged: event.shift };
2739
+ this.#selection.begin(this.#renderer.viewport(), event, Date.now(), event.shift);
2740
+ }
2741
+ else if (event.action === 'drag') {
2742
+ if (this.#promptChoicePress !== undefined)
2743
+ this.#promptChoicePress.dragged = true;
2744
+ this.#selection.move(event);
2745
+ }
2746
+ else {
2747
+ const pressed = this.#promptChoicePress;
2748
+ this.#promptChoicePress = undefined;
2749
+ const text = this.#selection.release(event);
2750
+ if (text !== undefined) {
2751
+ if (this.#copyOnSelect)
2752
+ void this.#copySelection();
2753
+ }
2754
+ else if (pressed !== undefined && !pressed.dragged && !event.shift && choice?.value === pressed.value && choice.row === pressed.row && this.#prompt !== null) {
2755
+ this.#prompt = { ...this.#prompt, selected: choice.index, previewScroll: 0 };
2756
+ if (this.#prompt.request.multiSelect === true && filteredPromptOptions(this.#prompt.request, this.#editor.text)[choice.index]?.disabled !== true)
2757
+ this.#prompt = togglePromptSelection(this.#prompt, this.#editor.text);
2758
+ }
2759
+ }
2760
+ this.#render();
2761
+ return;
2762
+ }
2763
+ const pressedAction = this.#mouseActionPress;
2764
+ if (pressedAction !== undefined) {
2765
+ const target = this.#mouseActionAt(event.row, event.column);
2766
+ if (event.action === 'release') {
2767
+ this.#mouseActionPress = undefined;
2768
+ if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
2769
+ if (target.action === 'copy')
2770
+ void this.#copySelection(true);
2771
+ else if (target.action === 'quote')
2772
+ this.#quoteSelection();
2773
+ else {
2774
+ const block = this.#state.blocks[target.block];
2775
+ if (block?.kind === 'tool')
2776
+ void this.#openToolActions(block);
2777
+ else if (block?.kind === 'commandOutput')
2778
+ void this.#readCommandOutput(block);
2779
+ }
2780
+ }
2781
+ }
2782
+ else if (event.action === 'drag' && (target?.key !== pressedAction.key || event.shift)) {
2783
+ // Keep consuming the gesture, but never activate after leaving the button.
2784
+ this.#mouseActionPress = { ...pressedAction, key: '' };
2785
+ }
2786
+ return;
2787
+ }
2788
+ if (event.button === 'left' && event.action === 'press' && !event.shift) {
2789
+ const target = this.#mouseActionAt(event.row, event.column);
2790
+ if (target !== undefined) {
2791
+ this.#mouseActionPress = target;
2792
+ this.#disclosurePress = undefined;
2793
+ return;
2794
+ }
2795
+ }
2260
2796
  const inside = (target) => event.row === target.row && event.column >= target.column && event.column < target.column + target.width;
2261
2797
  if (event.button === 'right') {
2798
+ this.#disclosurePress = undefined;
2262
2799
  if (event.action === 'press') {
2263
2800
  if (this.#selection.active)
2264
- void this.#copySelection();
2801
+ void this.#copySelection(true);
2265
2802
  else {
2266
2803
  const document = this.#renderer.documentRowAt(event.row);
2267
2804
  const frame = this.#fileActionFrame;
@@ -2283,6 +2820,8 @@ export class LocalTui {
2283
2820
  return;
2284
2821
  }
2285
2822
  if (event.action === 'press') {
2823
+ const disclosure = event.shift ? undefined : this.#disclosureAt(event.row, event.column);
2824
+ this.#disclosurePress = disclosure === undefined ? undefined : { target: disclosure, row: event.row, column: event.column };
2286
2825
  const target = this.#jumpToLatest;
2287
2826
  this.#jumpPress = target !== undefined && inside(target) ? target : undefined;
2288
2827
  if (this.#jumpPress !== undefined)
@@ -2301,15 +2840,93 @@ export class LocalTui {
2301
2840
  this.#followTail();
2302
2841
  }
2303
2842
  }
2304
- else if (event.action === 'drag')
2305
- this.#selection.move(event);
2843
+ else if (event.action === 'drag') {
2844
+ // Motion confined to the empty gutter is click jitter. Once it leaves,
2845
+ // selection owns the gesture even if the pointer returns to the arrow.
2846
+ if (event.shift || this.#pressedDisclosureAt(event.row, event.column) === undefined) {
2847
+ this.#disclosurePress = undefined;
2848
+ this.#selection.move(event);
2849
+ }
2850
+ }
2306
2851
  else {
2852
+ const target = this.#pressedDisclosureAt(event.row, event.column);
2853
+ this.#disclosurePress = undefined;
2854
+ if (!event.shift && target !== undefined) {
2855
+ this.#selection.clear();
2856
+ this.#activateDisclosure(target);
2857
+ return;
2858
+ }
2307
2859
  const text = this.#selection.release(event);
2308
2860
  if (text !== undefined && this.#copyOnSelect)
2309
2861
  void this.#copySelection();
2310
2862
  }
2311
2863
  this.#render();
2312
2864
  }
2865
+ #handleTrajectoryMouse(event) {
2866
+ if (event.button === 'right') {
2867
+ if (event.action === 'press' && this.#selection.active)
2868
+ void this.#copySelection(true);
2869
+ return;
2870
+ }
2871
+ if (event.action === 'press') {
2872
+ this.#trajectoryPress = { row: event.row, column: event.column,
2873
+ text: stripAnsi(this.#renderer.viewport()[event.row] ?? ''), dragged: event.shift };
2874
+ this.#selection.begin(this.#renderer.viewport(), event, Date.now(), event.shift);
2875
+ }
2876
+ else if (event.action === 'drag') {
2877
+ if (this.#trajectoryPress !== undefined)
2878
+ this.#trajectoryPress.dragged = true;
2879
+ this.#selection.move(event);
2880
+ }
2881
+ else {
2882
+ const press = this.#trajectoryPress;
2883
+ this.#trajectoryPress = undefined;
2884
+ const text = this.#selection.release(event);
2885
+ if (text !== undefined) {
2886
+ if (this.#copyOnSelect)
2887
+ void this.#copySelection();
2888
+ }
2889
+ else if (press !== undefined && !press.dragged && !event.shift && press.row === event.row
2890
+ && Math.abs(press.column - event.column) <= 1
2891
+ && stripAnsi(this.#renderer.viewport()[event.row] ?? '') === press.text) {
2892
+ if (this.#fileBrowser !== null)
2893
+ this.#fileBrowser.click(event.row, event.column, this.#term.width(), this.#term.height());
2894
+ else if (this.#trajectory !== null)
2895
+ this.#trajectory = trajectoryClick(this.#trajectory, this.#term.width(), this.#term.height(), event.row, event.column);
2896
+ }
2897
+ }
2898
+ this.#render();
2899
+ }
2900
+ #applyTrajectoryInput(event) {
2901
+ const state = this.#trajectory;
2902
+ if (state === null)
2903
+ return;
2904
+ const { pageSize } = trajectoryListMetrics(state, this.#term.height(), this.#term.width());
2905
+ const { pageLines } = trajectoryDetailMetrics(state, this.#term.height());
2906
+ const theme = createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks);
2907
+ const maxDetailScroll = trajectoryDetailScrollLimit(state, theme, this.#term.width(), this.#term.height());
2908
+ const command = applyTrajectoryEvent(state, event, { pageSize, detailPageLines: pageLines, width: this.#term.width(), maxDetailScroll });
2909
+ if ('close' in command) {
2910
+ this.#trajectory = null;
2911
+ this.#trajectoryPress = undefined;
2912
+ }
2913
+ else if ('copy' in command) {
2914
+ void this.#deliverCopy(command.copy).then(delivery => {
2915
+ if (this.#trajectory === state) {
2916
+ this.#trajectory = { ...state, notice: delivery === 'requested' ? 'Sent to terminal clipboard' : `Copied ${command.label}` };
2917
+ this.#render();
2918
+ }
2919
+ }, () => {
2920
+ if (this.#trajectory === state) {
2921
+ this.#trajectory = { ...state, notice: 'Copy failed' };
2922
+ this.#render();
2923
+ }
2924
+ });
2925
+ }
2926
+ else
2927
+ this.#trajectory = command.state;
2928
+ this.#render();
2929
+ }
2313
2930
  /** Keep older clipboard operations from finishing after a newer user copy. */
2314
2931
  #deliverCopy(text) {
2315
2932
  const task = this.#copyTask.then(() => {
@@ -2320,7 +2937,7 @@ export class LocalTui {
2320
2937
  this.#copyTask = task.catch(() => { });
2321
2938
  return task;
2322
2939
  }
2323
- async #copySelection() {
2940
+ async #copySelection(explicit = false) {
2324
2941
  if (!this.#selection.valid(this.#renderer.viewport())) {
2325
2942
  this.#selection.clear();
2326
2943
  return;
@@ -2329,11 +2946,99 @@ export class LocalTui {
2329
2946
  if (text === undefined)
2330
2947
  return;
2331
2948
  try {
2332
- await this.#deliverCopy(text);
2949
+ const delivery = await this.#deliverCopy(text);
2950
+ if (explicit && !this.#disposed) {
2951
+ this.#showTransientStatus(delivery === 'requested' ? 'Selection sent to terminal clipboard' : 'Copied selection');
2952
+ this.#render();
2953
+ }
2333
2954
  }
2334
2955
  catch {
2335
- if (!this.#disposed)
2336
- this.notice('Could not copy selection to the clipboard.', { level: 'error' });
2956
+ if (!this.#disposed) {
2957
+ this.#showTransientStatus('Copy failed');
2958
+ this.#render();
2959
+ }
2960
+ }
2961
+ }
2962
+ #canQuoteSelection() {
2963
+ return this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null && this.#search === null
2964
+ && this.#transcriptSearch === null && (this.#inspected === undefined || this.#inspected.writable === true);
2965
+ }
2966
+ #quoteSelection() {
2967
+ if (!this.#canQuoteSelection())
2968
+ return;
2969
+ const text = this.#selection.valid(this.#renderer.viewport()) ? this.#selection.text() : undefined;
2970
+ this.#selection.clear();
2971
+ if (text !== undefined) {
2972
+ this.#appendQuote(text.split('\n').map(line => '> ' + line).join('\n'));
2973
+ }
2974
+ this.#render();
2975
+ }
2976
+ #appendQuote(text) {
2977
+ this.#editor.setCursor(this.#editor.text.length);
2978
+ this.#editor.paste((this.#editor.text === '' ? '' : '\n\n') + text + '\n\n');
2979
+ this.#processSelection = undefined;
2980
+ this.#refreshAutocomplete();
2981
+ this.#showTransientStatus('Quoted in composer');
2982
+ this.#render();
2983
+ }
2984
+ /** Explicit process selection wins; scrolling through an open detail follows that tool. */
2985
+ #currentTool() {
2986
+ const detail = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
2987
+ const selected = this.#processSelection;
2988
+ const row = selected === undefined ? undefined : this.#blockStarts[selected];
2989
+ const selectedVisible = row !== undefined && row >= this.#scrollStart && row < this.#scrollStart + this.#scrollBudget - 1;
2990
+ const index = selectedVisible ? selected : detail?.block ?? selected ?? this.#viewportBlock();
2991
+ const block = index === undefined ? undefined : this.#state.blocks[index];
2992
+ return block?.kind === 'tool' ? block : undefined;
2993
+ }
2994
+ async #openToolActions(block = this.#currentTool()) {
2995
+ if (this.#fileReviewAbort !== undefined || this.#search !== null || this.#prompt !== null || this.#settings !== null)
2996
+ return;
2997
+ if (block === undefined) {
2998
+ this.#showTransientStatus(uiText(resolveUiLanguage(this.#uiLanguage), 'No tool selected · use {key}', { key: keysForAction(this.#keybindings, 'toggle-tools') }), 4_000);
2999
+ return;
3000
+ }
3001
+ const command = toolCommand(block);
3002
+ const files = blockFileAction(block);
3003
+ const source = toolSource(block);
3004
+ const options = [
3005
+ ...(command === undefined ? [] : [{ label: 'Copy command', value: 'command', description: command.split('\n')[0] ?? '' }]),
3006
+ ...(block.output.trim() === '' ? [] : [{ label: 'Copy result', value: 'result' }]),
3007
+ ...(files === undefined ? [] : [{ label: 'View file', value: 'files' }]),
3008
+ ...(source === undefined ? [] : [{ label: 'Copy source', value: 'source-copy', description: source.label },
3009
+ ...(this.#canQuoteSelection() ? [{ label: 'Quote source', value: 'source-quote', description: source.label }] : [])]),
3010
+ ];
3011
+ if (options.length === 0) {
3012
+ this.#showTransientStatus('No content available for this tool.', 4_000);
3013
+ return;
3014
+ }
3015
+ const abort = new AbortController();
3016
+ this.#fileReviewAbort = abort;
3017
+ try {
3018
+ const answer = await this.prompt({ localize: ['title'], title: 'Tool actions', question: toolActionContext(block),
3019
+ options: options.map(option => ({ ...option, label: uiText(resolveUiLanguage(this.#uiLanguage), option.label) })), allowCustom: false, notify: false, signal: abort.signal, onSuperseded: () => { abort.abort(); } });
3020
+ if (abort.signal.aborted || this.#disposed)
3021
+ return;
3022
+ if (answer === 'command' && command !== undefined)
3023
+ await this.#copyPicked(command, 'command');
3024
+ else if (answer === 'result')
3025
+ await this.#copyPicked(block.output, 'tool result');
3026
+ else if (answer === 'source-copy' && source !== undefined)
3027
+ await this.#copyPicked(source.text, 'source');
3028
+ else if (answer === 'source-quote' && source !== undefined && this.#canQuoteSelection())
3029
+ this.#appendQuote(sourceQuote(source));
3030
+ else if (answer === 'files' && files !== undefined) {
3031
+ this.#fileReviewAbort = undefined;
3032
+ this.#openFileAction(files);
3033
+ }
3034
+ }
3035
+ catch (error) {
3036
+ if (!abort.signal.aborted && !this.#disposed)
3037
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
3038
+ }
3039
+ finally {
3040
+ if (this.#fileReviewAbort === abort)
3041
+ this.#fileReviewAbort = undefined;
2337
3042
  }
2338
3043
  }
2339
3044
  #openFileAction(action, initial) {
@@ -2382,10 +3087,23 @@ export class LocalTui {
2382
3087
  this.#handleMouse(event);
2383
3088
  return;
2384
3089
  }
3090
+ this.#promptActionPress = undefined;
3091
+ this.#promptChoicePress = undefined;
2385
3092
  this.#jumpPress = undefined;
3093
+ this.#disclosurePress = undefined;
3094
+ this.#mouseActionPress = undefined;
3095
+ if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3096
+ && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3097
+ this.#keyTest.observe(event);
3098
+ return;
3099
+ }
2386
3100
  if (this.#selection.active) {
3101
+ if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection') {
3102
+ this.#quoteSelection();
3103
+ return;
3104
+ }
2387
3105
  if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
2388
- void this.#copySelection();
3106
+ void this.#copySelection(true);
2389
3107
  return;
2390
3108
  }
2391
3109
  this.#selection.clear();
@@ -2396,32 +3114,37 @@ export class LocalTui {
2396
3114
  }
2397
3115
  }
2398
3116
  if (event.type === 'wheel') {
3117
+ if (this.#fileBrowser !== null) {
3118
+ this.#applyFileBrowserInput(event);
3119
+ return;
3120
+ }
3121
+ if (this.#trajectory !== null) {
3122
+ this.#applyTrajectoryInput(event);
3123
+ return;
3124
+ }
2399
3125
  this.#jumpPress = undefined;
2400
3126
  if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
2401
3127
  const step = wheelStep(this.#wheelGesture, Date.now(), event.direction === 'up' ? -1 : 1, !this.#wheelAccelerates);
2402
3128
  this.#wheelGesture = step.gesture;
2403
- if (step.lines !== 0)
2404
- this.#scrollBy(step.lines);
3129
+ if (step.lines !== 0) {
3130
+ if (this.#prompt !== null && this.#prompt.request.presentation !== 'document' && this.#prompt.request.presentation !== 'plan-review') {
3131
+ if (this.#prompt.request.wait !== undefined && this.#prompt.waitHeld !== true) {
3132
+ this.#prompt.request.wait.hold();
3133
+ this.#prompt.offTick?.();
3134
+ this.#prompt = { ...this.#prompt, waitHeld: true };
3135
+ }
3136
+ const count = filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed).length;
3137
+ this.#prompt = movePromptSelection(this.#prompt, Math.max(0, Math.min(count - 1, this.#prompt.selected + step.lines)), count);
3138
+ this.#render();
3139
+ }
3140
+ else if (this.#documentMouseEnabled())
3141
+ this.#scrollPromptDocument(this.#promptDocument.start + step.lines);
3142
+ else
3143
+ this.#scrollBy(step.lines);
3144
+ }
2405
3145
  }
2406
3146
  return;
2407
3147
  }
2408
- if (this.#state.status === 'compacting') {
2409
- if (event.type === 'key' && event.id === 'end' && this.#jumpToLatest !== undefined) {
2410
- this.#followTail();
2411
- this.#render();
2412
- return;
2413
- }
2414
- if (event.type === 'key' && event.id === 'pageUp') {
2415
- this.#scrollBy(-this.#pageSize());
2416
- return;
2417
- }
2418
- if (event.type === 'key' && event.id === 'pageDown') {
2419
- this.#scrollBy(this.#pageSize());
2420
- return;
2421
- }
2422
- if (event.type !== 'key' || event.id !== 'ctrl+c')
2423
- return;
2424
- }
2425
3148
  // Clipboard image inspection is asynchronous. Preserve the exact key
2426
3149
  // order so a fast Ctrl+V, Enter submits the finished image draft rather
2427
3150
  // than an empty prompt.
@@ -2449,6 +3172,8 @@ export class LocalTui {
2449
3172
  return;
2450
3173
  }
2451
3174
  if (event.type === 'paste-start') {
3175
+ if (this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null)
3176
+ this.#processSelection = undefined;
2452
3177
  this.#paste = true;
2453
3178
  this.#pasteBuf = '';
2454
3179
  return;
@@ -2466,15 +3191,12 @@ export class LocalTui {
2466
3191
  }
2467
3192
  if (this.#handlePrompt(event))
2468
3193
  return;
3194
+ if (this.#fileBrowser !== null) {
3195
+ this.#applyFileBrowserInput(event);
3196
+ return;
3197
+ }
2469
3198
  if (this.#trajectory !== null) {
2470
- const { pageSize } = trajectoryListMetrics(this.#trajectory, this.#term.height(), this.#term.width());
2471
- const { pageLines } = trajectoryDetailMetrics(this.#trajectory, this.#term.height());
2472
- const command = applyTrajectoryEvent(this.#trajectory, event, { pageSize, detailPageLines: pageLines });
2473
- if ('close' in command)
2474
- this.#trajectory = null;
2475
- else
2476
- this.#trajectory = command.state;
2477
- this.#render();
3199
+ this.#applyTrajectoryInput(event);
2478
3200
  return;
2479
3201
  }
2480
3202
  if (this.#transcriptSearch !== null) {
@@ -2500,6 +3222,8 @@ export class LocalTui {
2500
3222
  this.#applySettings(event);
2501
3223
  return;
2502
3224
  }
3225
+ if (this.#search === null && this.#handleProcessNavigation(event))
3226
+ return;
2503
3227
  /**
2504
3228
  * The side pane is a companion, not a mode: it answers where the reader is
2505
3229
  * already working, so it must not take keys the composer is using. It reads
@@ -2588,10 +3312,7 @@ export class LocalTui {
2588
3312
  return true;
2589
3313
  }
2590
3314
  if (this.#state.status !== 'idle') {
2591
- // Aborting a turn would strand whatever the user queued behind it. Pi
2592
- // hands those drafts back first, so nothing typed during the turn is lost
2593
- // to the abort that ends it.
2594
- this.#restoreQueuedBeforeAbort();
3315
+ // Harness retains accepted input; stopping a turn does not withdraw messages.
2595
3316
  for (const listener of this.#interrupts)
2596
3317
  listener();
2597
3318
  return true;
@@ -2629,7 +3350,7 @@ export class LocalTui {
2629
3350
  }
2630
3351
  // A running Shell line owns Ctrl+C before the composer does: the reader is
2631
3352
  // watching a command, and the key they press to stop it is the same one.
2632
- if (this.#shellAbort !== undefined && !this.#hasComposerContent()) {
3353
+ if (this.#inspected === undefined && this.#shellAbort !== undefined && !this.#hasComposerContent()) {
2633
3354
  this.#shellAbort();
2634
3355
  return true;
2635
3356
  }
@@ -2658,7 +3379,6 @@ export class LocalTui {
2658
3379
  // A running turn keeps Ctrl+C as the familiar abort while the composer is
2659
3380
  // empty; Escape already covers it, and refusing the reflex would be worse.
2660
3381
  if (this.#state.status !== 'idle' && !this.#hasComposerContent()) {
2661
- this.#restoreQueuedBeforeAbort();
2662
3382
  for (const listener of this.#interrupts)
2663
3383
  listener();
2664
3384
  return true;
@@ -2666,20 +3386,6 @@ export class LocalTui {
2666
3386
  this.#clearComposerWithUndo();
2667
3387
  return true;
2668
3388
  }
2669
- /**
2670
- * Hand durable follow-ups back to the composer before a turn is abandoned.
2671
- *
2672
- * The queued messages would otherwise be delivered to a turn that will never
2673
- * produce the step they were aimed at, which silently loses them.
2674
- */
2675
- #restoreQueuedBeforeAbort() {
2676
- if (!this.#state.nextTurnInbox.some(message => message.source.kind === 'user'))
2677
- return;
2678
- if (this.#hasComposerContent())
2679
- return;
2680
- for (const listener of this.#queueEdits)
2681
- listener();
2682
- }
2683
3389
  #handlePrompt(event) {
2684
3390
  let prompt = this.#prompt;
2685
3391
  if (prompt === null)
@@ -2727,13 +3433,15 @@ export class LocalTui {
2727
3433
  const selected = filteredPromptOptions(prompt.request, '', prompt.collapsed)[prompt.selected];
2728
3434
  if (action !== undefined && (action.scope === 'list' || selected !== undefined)) {
2729
3435
  if (!disarmed) {
2730
- this.#finishPrompt(action.valuePrefix + (action.scope === 'list' ? '' : selected.value ?? selected.label));
3436
+ this.#finishPrompt(action.valuePrefix + (action.scope === 'list' ? '' : selected.value ?? selected.label), 'action');
2731
3437
  this.#render();
2732
3438
  }
2733
3439
  return true;
2734
3440
  }
2735
3441
  }
2736
- if (event.type === 'text' && event.value === ' ' && prompt.request.multiSelect === true) {
3442
+ const toggleAnswer = prompt.request.answerFormat === 'json' && event.type === 'key' && event.id === 'ctrl+g';
3443
+ const toggleSpace = event.type === 'text' && event.value === ' ' && (prompt.request.answerFormat !== 'json' || this.#editor.text === '');
3444
+ if (prompt.request.multiSelect === true && (toggleAnswer || toggleSpace)) {
2737
3445
  this.#prompt = togglePromptSelection(prompt, this.#editor.text);
2738
3446
  this.#render();
2739
3447
  return true;
@@ -2755,14 +3463,11 @@ export class LocalTui {
2755
3463
  const filtered = filteredPromptOptions(prompt.request, this.#editor.text, prompt.collapsed);
2756
3464
  const count = filtered.length;
2757
3465
  const keyAction = prompt.request.actions?.find(item => item.key.toLowerCase() === event.id.toLowerCase());
2758
- if (keyAction !== undefined && (keyAction.scope === 'list'
2759
- || prompt.request.presentation === 'tree'
2760
- || prompt.request.presentation === 'card')) {
3466
+ if (keyAction !== undefined && (keyAction.scope === 'list' || filtered[prompt.selected] !== undefined)) {
2761
3467
  if (!disarmed) {
2762
3468
  const selected = filtered[prompt.selected];
2763
3469
  if (keyAction.scope === 'list' || selected !== undefined) {
2764
- this.#editor.setText('');
2765
- this.#finishPrompt(keyAction.valuePrefix + (keyAction.scope === 'list' ? '' : selected.value ?? selected.label));
3470
+ this.#finishPrompt(keyAction.valuePrefix + (keyAction.scope === 'list' ? '' : selected.value ?? selected.label), 'action');
2766
3471
  this.#render();
2767
3472
  }
2768
3473
  }
@@ -2821,7 +3526,7 @@ export class LocalTui {
2821
3526
  // the other lists about what `tab` or `pageUp` means. `enter` stays below
2822
3527
  // because what it submits depends on the editor and on multi-select.
2823
3528
  const page = prompt.request.presentation === 'tree'
2824
- ? promptTreePageSize(prompt.request, this.#term.width(), this.#term.height()) : 10;
3529
+ ? promptTreePageSize(prompt.request, this.#term.width(), this.#term.height(), resolveUiLanguage(this.#uiLanguage)) : 10;
2825
3530
  const navigation = listNavigation(event);
2826
3531
  if (navigation.kind === 'move' || navigation.kind === 'page') {
2827
3532
  // The tree clamps: its rows are a structure, and wrapping past the last
@@ -2838,11 +3543,10 @@ export class LocalTui {
2838
3543
  }
2839
3544
  if (event.id === 'enter' && (this.#editor.text === '' || prompt.request.multiSelect === true)) {
2840
3545
  const answer = selectedPromptAnswer(prompt);
2841
- // A disabled row and an empty multi-select both swallow Enter: nothing is
2842
- // being answered, so the surface stays where the reader is.
2843
- if (answer === null)
3546
+ // Supplementary text can answer a question even with no checked options.
3547
+ if (answer === null && !(prompt.request.answerFormat === 'json' && this.#editor.expandedText.trim()))
2844
3548
  return true;
2845
- this.#finishPrompt(answer);
3549
+ this.#finishPrompt(answer ?? '', 'selection');
2846
3550
  this.#render();
2847
3551
  return true;
2848
3552
  }
@@ -2852,7 +3556,6 @@ export class LocalTui {
2852
3556
  // closing on it would lose both the list and the filter.
2853
3557
  if (answer === null)
2854
3558
  return true;
2855
- this.#editor.setText('');
2856
3559
  this.#finishPrompt(answer);
2857
3560
  this.#render();
2858
3561
  return true;
@@ -2882,7 +3585,7 @@ export class LocalTui {
2882
3585
  }
2883
3586
  return true;
2884
3587
  }
2885
- if (!document && event.type === 'key' && (event.id === 'ctrl+x' || this.#boundAction(event.id) === 'external-editor')) {
3588
+ if (!document && prompt.request.allowCustom !== false && event.type === 'key' && (event.id === 'ctrl+x' || this.#boundAction(event.id) === 'external-editor')) {
2886
3589
  this.#openPlanFeedbackEditor(prompt);
2887
3590
  return true;
2888
3591
  }
@@ -2945,13 +3648,7 @@ export class LocalTui {
2945
3648
  else if (event.id === 'end')
2946
3649
  documentScroll = prompt.request.documentTail === true ? Number.POSITIVE_INFINITY : scroll?.maxStart ?? prompt.documentScroll ?? 0;
2947
3650
  if (documentScroll !== undefined) {
2948
- this.#prompt = {
2949
- ...prompt,
2950
- documentAnchor: undefined,
2951
- documentTarget: undefined,
2952
- documentScroll: documentScroll === Number.POSITIVE_INFINITY ? documentScroll : Math.max(0, Math.min(documentScroll, scroll?.maxStart ?? Number.POSITIVE_INFINITY)),
2953
- };
2954
- this.#render();
3651
+ this.#scrollPromptDocument(documentScroll);
2955
3652
  return true;
2956
3653
  }
2957
3654
  if (event.id === 'tab' || event.id === 'right') {
@@ -2972,7 +3669,7 @@ export class LocalTui {
2972
3669
  return true;
2973
3670
  const approve = prompt.request.approveValue ?? prompt.request.options?.[0]?.value
2974
3671
  ?? prompt.request.options?.[0]?.label;
2975
- if (document || answer === approve) {
3672
+ if (document || answer === approve || prompt.request.allowCustom === false) {
2976
3673
  this.#finishPrompt(answer);
2977
3674
  }
2978
3675
  else {
@@ -2993,7 +3690,11 @@ export class LocalTui {
2993
3690
  * something up in it.
2994
3691
  */
2995
3692
  #handleRenderedDocument(event, prompt) {
2996
- const rows = () => documentRows(prompt.request, createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors), Math.max(1, this.#term.width() - 6));
3693
+ const rows = () => {
3694
+ const theme = createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks);
3695
+ const width = Math.max(1, this.#term.width() - 6);
3696
+ return prompt.renderDocument?.(theme, width) ?? documentRows(prompt.request, theme, width);
3697
+ };
2997
3698
  // Always build on the newest state: typing updates the query while the find
2998
3699
  // is open, and jumping from a stale copy would drop it again.
2999
3700
  const jumpTo = (row) => {
@@ -3055,7 +3756,7 @@ export class LocalTui {
3055
3756
  }
3056
3757
  #handleDocumentReader(event, prompt) {
3057
3758
  const source = prompt.request.documentSource;
3058
- const layout = documentLayout(source, createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors), Math.max(1, this.#term.width() - 6), {
3759
+ const layout = documentLayout(source, createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks), Math.max(1, this.#term.width() - 6), {
3059
3760
  wordWrap: prompt.documentWordWrap ?? true, column: prompt.documentColumn ?? 0,
3060
3761
  });
3061
3762
  const start = this.#promptDocument?.start ?? 0;
@@ -3137,7 +3838,7 @@ export class LocalTui {
3137
3838
  listener(id);
3138
3839
  }
3139
3840
  #handleSubagentLauncher(event) {
3140
- const agents = this.#subagents?.agents ?? [];
3841
+ const agents = rosterItems(this.#subagents);
3141
3842
  if (this.#subagentSelection !== undefined) {
3142
3843
  if (agents.length === 0) {
3143
3844
  this.#subagentSelection = undefined;
@@ -3146,7 +3847,12 @@ export class LocalTui {
3146
3847
  const at = Math.min(this.#subagentSelection, agents.length - 1);
3147
3848
  if (event.type === 'key' && event.id === 'enter') {
3148
3849
  this.#subagentSelection = undefined;
3149
- this.#openSelectedSubagent(agents[at]);
3850
+ const item = agents[at];
3851
+ if (item.kind === 'agent')
3852
+ this.#openSelectedSubagent(item.agent);
3853
+ else
3854
+ for (const listener of this.#workflowInspects)
3855
+ listener(item.run.id);
3150
3856
  return true;
3151
3857
  }
3152
3858
  if (event.type === 'key' && event.id === 'escape') {
@@ -3172,7 +3878,7 @@ export class LocalTui {
3172
3878
  return false;
3173
3879
  if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
3174
3880
  return false;
3175
- this.#subagentSelection = Math.min(agents.length - 1, Math.max(0, subagentPreviewStart(agents)));
3881
+ this.#subagentSelection = Math.min(agents.length - 1, Math.max(0, rosterPreviewStart(this.#subagents)));
3176
3882
  this.#render();
3177
3883
  return true;
3178
3884
  }
@@ -3216,11 +3922,15 @@ export class LocalTui {
3216
3922
  const previous = options.persist ? this.prefs() : undefined;
3217
3923
  const version = this.#prefsVersion;
3218
3924
  const previousMotion = this.#motion;
3925
+ this.#uiLanguage = prefs.uiLanguage ?? 'auto';
3926
+ if (this.#prompt !== null)
3927
+ this.#prompt = { ...this.#prompt, request: localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)) };
3219
3928
  this.#themeName = prefs.theme;
3220
3929
  this.#colors = prefs.colors;
3221
3930
  this.#syncColorDepth();
3222
3931
  this.#motion = prefs.motion ?? 'full';
3223
3932
  this.#flushPasteBurst(true);
3933
+ this.#busyEnter = prefs.busyEnter ?? 'queue';
3224
3934
  this.#pasteProtection = prefs.pasteProtection !== false;
3225
3935
  this.#externalEditor = prefs.editor ?? 'auto';
3226
3936
  this.#terminalTitle = prefs.terminalTitle ?? 'status';
@@ -3229,7 +3939,7 @@ export class LocalTui {
3229
3939
  this.#copyOnSelect = prefs.copyOnSelect ?? true;
3230
3940
  this.#mathMode = prefs.mathMode ?? 'auto';
3231
3941
  this.#mermaidMode = prefs.mermaidMode ?? 'auto';
3232
- this.#hideThinking = prefs.hideThinking === true;
3942
+ this.#hideThinking = prefs.hideThinking !== false;
3233
3943
  this.#mouseInteraction = prefs.mouseInteraction ?? 'auto';
3234
3944
  this.#checkUpdates = prefs.checkUpdates ?? true;
3235
3945
  this.#startupChangelog = prefs.startupChangelog ?? 'summary';
@@ -3334,6 +4044,10 @@ export class LocalTui {
3334
4044
  if (command.kind === 'open-command') {
3335
4045
  if (this.#prefsSaveTask !== undefined)
3336
4046
  return;
4047
+ if (command.name === 'manage-goal') {
4048
+ this.#openGoal();
4049
+ return;
4050
+ }
3337
4051
  if (command.name === 'keybindings') {
3338
4052
  void this.#editKeybindings();
3339
4053
  return;
@@ -3514,7 +4228,7 @@ export class LocalTui {
3514
4228
  return;
3515
4229
  }
3516
4230
  const commands = this.#commands();
3517
- const slashResult = slashSuggestions(this.#editor.text, this.#editor.cursor, commands);
4231
+ const slashResult = slashSuggestions(this.#editor.text, this.#editor.cursor, commands, resolveUiLanguage(this.#uiLanguage));
3518
4232
  if (slashResult !== null) {
3519
4233
  this.#setAutocomplete(slashResult);
3520
4234
  return;
@@ -3653,11 +4367,6 @@ export class LocalTui {
3653
4367
  return;
3654
4368
  }
3655
4369
  if (command.kind === 'submit') {
3656
- if (this.#inspected !== undefined) {
3657
- if (this.#inspected.writable === true)
3658
- this.#submitInspect(command.text);
3659
- return;
3660
- }
3661
4370
  this.#submit(command.text);
3662
4371
  return;
3663
4372
  }
@@ -3713,8 +4422,8 @@ export class LocalTui {
3713
4422
  #toggleThinking() {
3714
4423
  const next = !this.#hideThinking;
3715
4424
  this.#hideThinking = next;
3716
- if (!next)
3717
- this.#expandedReasoning.clear();
4425
+ this.#expandedReasoning.clear();
4426
+ this.#closedReasoning.clear();
3718
4427
  const persist = this.#persistPrefs;
3719
4428
  if (persist !== null)
3720
4429
  void persist(this.prefs(), [{ op: 'set', path: ['hideThinking'], value: next }]);
@@ -3749,7 +4458,22 @@ export class LocalTui {
3749
4458
  this.#refreshAutocomplete();
3750
4459
  this.#render();
3751
4460
  }
4461
+ #cancelQueueRecall() {
4462
+ this.#queueRecall?.abort.abort();
4463
+ this.#queueRecall = undefined;
4464
+ this.#queueEditPending = false;
4465
+ }
4466
+ #requestQueueRecall() {
4467
+ const abort = new AbortController();
4468
+ const request = { signal: abort.signal, ...(this.#draftScope === undefined ? {} : { sessionId: this.#draftScope }) };
4469
+ this.#queueRecall = { abort, request };
4470
+ this.#queueEditPending = true;
4471
+ for (const listener of this.#queueEdits)
4472
+ listener(request);
4473
+ }
3752
4474
  #restoreLatestQueuedSubmission(allowNonEmpty = false) {
4475
+ if (this.#inspected !== undefined && !this.#inspected.writable)
4476
+ return false;
3753
4477
  if (this.#queueEditPending)
3754
4478
  return true;
3755
4479
  if (this.#queueEditNewer !== null) {
@@ -3762,9 +4486,7 @@ export class LocalTui {
3762
4486
  const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
3763
4487
  if (!hasDurableFollowup || this.#queueEdits.size === 0)
3764
4488
  return true;
3765
- this.#queueEditPending = true;
3766
- for (const listener of this.#queueEdits)
3767
- listener();
4489
+ this.#requestQueueRecall();
3768
4490
  return true;
3769
4491
  }
3770
4492
  if (this.#historyIndex !== 0)
@@ -3781,10 +4503,7 @@ export class LocalTui {
3781
4503
  const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
3782
4504
  if (!hasDurableFollowup || this.#queueEdits.size === 0)
3783
4505
  return false;
3784
- this.#queueEditNewer = [];
3785
- this.#queueEditPending = true;
3786
- for (const listener of this.#queueEdits)
3787
- listener();
4506
+ this.#requestQueueRecall();
3788
4507
  return true;
3789
4508
  }
3790
4509
  #historyNext() {
@@ -3826,7 +4545,8 @@ export class LocalTui {
3826
4545
  const images = this.#images.copies();
3827
4546
  const files = this.#files.copies();
3828
4547
  const typedText = this.#files.text(images.length > 0 ? text.trim() : text);
3829
- const segments = images.length === 0 ? [] : splitComposerImageMarkers(typedText, images);
4548
+ const segments = images.length === 0 && files.length === 0 ? []
4549
+ : splitComposerAttachmentMarkers(text.trim(), images, this.#files.snapshot(text).entries.map(item => item.marker));
3830
4550
  // Stripping a marker that sat on its own line leaves that line break behind.
3831
4551
  const submittedText = images.length > 0 ? stripComposerImageMarkers(typedText, images).trim() : typedText;
3832
4552
  const literal = text === this.#editor.expandedText && (this.#literalInput || this.#editor.startsWithPaste);
@@ -3842,19 +4562,48 @@ export class LocalTui {
3842
4562
  historyText,
3843
4563
  };
3844
4564
  }
3845
- #submit(text) {
4565
+ #dispatchInput(submission) {
4566
+ const receipt = this.#pendingInputs.identify(submission, this.#pendingScope());
4567
+ receipt.recovered = false;
4568
+ if (this.#inspected !== undefined) {
4569
+ this.#pendingInputs.take(submission, this.#pendingScope());
4570
+ for (const listener of this.#inspectSubmits)
4571
+ listener(submission);
4572
+ }
4573
+ else if (this.#pending !== null) {
4574
+ const pending = this.#pending;
4575
+ this.#pending = null;
4576
+ pending.offAbort?.();
4577
+ this.#pendingInputs.take(submission, this.#pendingScope());
4578
+ pending.resolve(submission);
4579
+ }
4580
+ else
4581
+ this.#queuedSubmissions.push(submission);
4582
+ this.#pendingInputs.flush(this.#pendingScope());
4583
+ }
4584
+ #submit(text, delivery) {
3846
4585
  if (this.#prompt !== null) {
3847
4586
  if (this.#prompt.request.presentation === 'progress')
3848
4587
  return;
3849
- this.#editor.setText('');
3850
4588
  const answer = this.#prompt.request.preserveWhitespace === true ? text : text.trim();
3851
4589
  this.#finishPrompt(answer.trim() === '' ? null : answer);
3852
4590
  this.#render();
3853
4591
  return;
3854
4592
  }
4593
+ if (this.#inspected !== undefined) {
4594
+ this.#submitInspect(text, delivery);
4595
+ return;
4596
+ }
3855
4597
  const { submission, historyText } = this.#buildSubmission(text);
3856
- const { images, files = [], literal = false, text: submittedText } = submission;
4598
+ const { images, files = [], text: submittedText } = submission;
3857
4599
  const queueEditNewer = this.#queueEditNewer;
4600
+ // Commands keep their own routing; recalled follow-ups remain next-turn messages.
4601
+ if (delivery === 'steer')
4602
+ submission.literal = true;
4603
+ const command = !submission.literal && (submittedText.trimStart().startsWith('/') || (images.length === 0 && files.length === 0 && submittedText.startsWith('!')));
4604
+ if (!command && this.#state.status === 'running') {
4605
+ submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : delivery ?? this.#busyEnter;
4606
+ }
3858
4607
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
3859
4608
  this.#history.push(historyText);
3860
4609
  this.#historyStore?.add(historyText);
@@ -3863,11 +4612,10 @@ export class LocalTui {
3863
4612
  this.#draft = '';
3864
4613
  this.#literalInput = false;
3865
4614
  this.#queueEditNewer = null;
3866
- this.#queueEditPending = false;
4615
+ this.#cancelQueueRecall();
3867
4616
  this.#editor.setText('');
3868
4617
  this.#images.clear();
3869
4618
  this.#files.clear();
3870
- this.#clearDraft();
3871
4619
  this.#ac = null;
3872
4620
  this.#search = null;
3873
4621
  this.#settings = null;
@@ -3875,63 +4623,69 @@ export class LocalTui {
3875
4623
  // Image placeholders are TUI-owned. Mixed image+slash drafts stay a
3876
4624
  // submission so restore can keep the original markers; the runner strips
3877
4625
  // them only to detect and execute Harness commands.
3878
- const slash = !literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
4626
+ const slash = !submission.literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
3879
4627
  if (slash !== null) {
3880
4628
  if (queueEditNewer !== null)
3881
4629
  this.#queuedSubmissions.push(...queueEditNewer);
3882
4630
  this.#runSlash(slash.name, slash.args);
4631
+ this.#clearDraft();
3883
4632
  return;
3884
4633
  }
3885
4634
  this.#followTail();
3886
- const pending = this.#pending;
3887
- if (pending !== null) {
3888
- pending.offAbort?.();
3889
- this.#pending = null;
3890
- pending.resolve(submission);
3891
- if (queueEditNewer !== null)
3892
- this.#queuedSubmissions.push(...queueEditNewer);
3893
- }
3894
- else if (submittedText !== '' || images.length > 0 || files.length > 0 || queueEditNewer !== null) {
3895
- if (submittedText !== '' || images.length > 0 || files.length > 0) {
3896
- this.#queuedSubmissions.push(submission);
3897
- }
3898
- if (queueEditNewer !== null)
3899
- this.#queuedSubmissions.push(...queueEditNewer);
3900
- }
4635
+ if (submittedText !== '' || images.length > 0 || files.length > 0 || this.#pending !== null)
4636
+ this.#dispatchInput(submission);
4637
+ if (queueEditNewer !== null)
4638
+ this.#queuedSubmissions.push(...queueEditNewer);
3901
4639
  if (submittedText !== '' || images.length > 0 || files.length > 0) {
3902
4640
  this.#popStashedDraft();
3903
4641
  }
4642
+ this.#pendingInputs.flush(this.#pendingScope());
4643
+ this.#clearDraft();
3904
4644
  this.#render();
3905
4645
  }
3906
- #submitInspect(text) {
4646
+ #submitInspect(text, delivery) {
4647
+ if (this.#inspected?.writable !== true)
4648
+ return;
3907
4649
  const { submission, historyText } = this.#buildSubmission(text);
4650
+ const newer = this.#queueEditNewer;
4651
+ if (delivery === 'steer')
4652
+ submission.literal = true;
4653
+ submission.delivery = newer !== null || this.#queueEditPending ? 'queue' : this.#state.status === 'running' ? delivery ?? this.#busyEnter : 'queue';
3908
4654
  const { images, files = [], literal = false, text: submittedText } = submission;
3909
4655
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
3910
4656
  this.#history.push(historyText);
3911
4657
  this.#historyStore?.add(historyText);
3912
4658
  }
4659
+ this.#queueEditNewer = null;
4660
+ this.#cancelQueueRecall();
3913
4661
  this.#historyIndex = 0;
3914
4662
  this.#draft = '';
3915
4663
  this.#literalInput = false;
3916
4664
  this.#editor.setText('');
3917
4665
  this.#images.clear();
3918
4666
  this.#files.clear();
3919
- this.#clearDraft();
3920
4667
  this.#ac = null;
3921
4668
  this.#search = null;
3922
4669
  const slash = !literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
3923
4670
  if (slash !== null) {
4671
+ this.#queueEditNewer = newer;
3924
4672
  this.#runSlash(slash.name, slash.args);
3925
4673
  return;
3926
4674
  }
3927
4675
  this.#followTail();
3928
- if (submittedText === '' && images.length === 0 && files.length === 0) {
4676
+ if (submittedText === '' && images.length === 0 && files.length === 0 && !newer?.length) {
3929
4677
  this.#render();
3930
4678
  return;
3931
4679
  }
3932
4680
  const { literal: _literal, ...inspectSubmission } = submission;
3933
- for (const listener of this.#inspectSubmits)
3934
- listener(inspectSubmission);
4681
+ if (submittedText !== '' || images.length > 0 || files.length > 0) {
4682
+ this.#dispatchInput(inspectSubmission);
4683
+ }
4684
+ for (const draft of newer ?? []) {
4685
+ this.#dispatchInput({ ...draft, delivery: 'queue' });
4686
+ }
4687
+ this.#pendingInputs.flush(this.#pendingScope());
4688
+ this.#clearDraft();
3935
4689
  this.#popStashedDraft();
3936
4690
  this.#render();
3937
4691
  }
@@ -3949,6 +4703,10 @@ export class LocalTui {
3949
4703
  }
3950
4704
  // Reading controls cover the current view; submitting a command that
3951
4705
  // opens one must preserve the same return position as a keyboard action.
4706
+ if (command.name === 'queue' && this.#inspected !== undefined) {
4707
+ this.#runAction('manage-queue');
4708
+ return;
4709
+ }
3952
4710
  if (command.name === 'settings') {
3953
4711
  this.#runSettings(args);
3954
4712
  return;
@@ -3966,18 +4724,17 @@ export class LocalTui {
3966
4724
  return;
3967
4725
  }
3968
4726
  const raw = '/' + name + (args === '' ? '' : ' ' + args);
3969
- const pending = this.#pending;
3970
- if (pending !== null) {
3971
- pending.offAbort?.();
3972
- this.#pending = null;
3973
- pending.resolve({ text: raw, images: [] });
3974
- }
3975
- else {
3976
- this.#queuedSubmissions.push({ text: raw, images: [] });
3977
- }
4727
+ this.#dispatchInput({ text: raw, images: [] });
3978
4728
  this.#render();
3979
4729
  return;
3980
4730
  }
4731
+ if (command.name === 'help') {
4732
+ if (args !== '' && args !== 'full')
4733
+ this.notice('Usage: /help [full]');
4734
+ else
4735
+ this.#runHelp(args === 'full');
4736
+ return;
4737
+ }
3981
4738
  this.#followTail();
3982
4739
  if (command.name === 'quit') {
3983
4740
  this.#render();
@@ -3998,30 +4755,39 @@ export class LocalTui {
3998
4755
  this.#runTools();
3999
4756
  return;
4000
4757
  }
4001
- if (args !== '' && args !== 'full') {
4002
- this.#notice('Usage: /help [full]');
4003
- this.#render();
4004
- return;
4005
- }
4006
- this.#runHelp(args === 'full');
4758
+ this.#render();
4007
4759
  }
4008
4760
  #commands() {
4009
4761
  const localNames = new Set(BUILTIN_SLASH_COMMANDS.flatMap((command) => [command.name, ...(command.aliases ?? [])]));
4010
4762
  const runtime = this.#runtimeCommands
4011
- .filter((command) => !localNames.has(command.name))
4012
- .map((command) => ({
4013
- name: command.name,
4014
- description: command.description,
4015
- ...(command.inputHint === undefined ? {} : { inputHint: command.inputHint }),
4016
- ...(command.complete === undefined ? {} : { complete: command.complete }),
4017
- }));
4763
+ .filter((command) => !localNames.has(command.name));
4018
4764
  return [...BUILTIN_SLASH_COMMANDS, ...runtime];
4019
4765
  }
4020
- #finishPrompt(answer) {
4766
+ #finishPrompt(answer, kind = 'text') {
4021
4767
  const pending = this.#prompt;
4022
4768
  if (pending === null)
4023
4769
  return;
4770
+ const draft = structuredPromptAnswer(pending, this.#editor.expandedText);
4771
+ try {
4772
+ pending.request.onAnswerDraft?.(draft);
4773
+ }
4774
+ catch (error) {
4775
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
4776
+ }
4777
+ if (answer !== null && pending.request.answerFormat === 'json' && kind !== 'action' && this.#tty) {
4778
+ answer = JSON.stringify(kind === 'selection' ? draft : { selected: [], ...(answer === '' ? {} : { custom: answer }) });
4779
+ }
4780
+ if (pending.browserState !== undefined) {
4781
+ const selected = filteredPromptOptions(pending.request, this.#editor.text, pending.collapsed)[pending.selected];
4782
+ pending.browserState.query = this.#editor.text;
4783
+ if (selected !== undefined)
4784
+ pending.browserState.selected = selected.value ?? selected.label;
4785
+ }
4024
4786
  this.#prompt = null;
4787
+ this.#promptActionPress = undefined;
4788
+ this.#promptChoicePress = undefined;
4789
+ this.#selection.clear();
4790
+ this.#wheelGesture = IDLE_WHEEL;
4025
4791
  if (pending.request.notify !== false) {
4026
4792
  this.#herdr.promptResolved();
4027
4793
  this.#syncWindowTitle();
@@ -4054,11 +4820,14 @@ export class LocalTui {
4054
4820
  }
4055
4821
  /** Save and close the visible overlay so a prompt owns both input and screen. */
4056
4822
  #displaceSurface() {
4057
- const displaced = this.#trajectory !== null
4058
- ? { kind: 'trajectory', state: this.#trajectory }
4059
- : this.#settings !== null
4060
- ? { kind: 'settings', state: this.#settings }
4061
- : this.#search !== null ? { kind: 'search', state: this.#search } : null;
4823
+ const displaced = this.#fileBrowser !== null
4824
+ ? { kind: 'fileBrowser', browser: this.#fileBrowser }
4825
+ : this.#trajectory !== null
4826
+ ? { kind: 'trajectory', state: this.#trajectory }
4827
+ : this.#settings !== null
4828
+ ? { kind: 'settings', state: this.#settings }
4829
+ : this.#search !== null ? { kind: 'search', state: this.#search } : null;
4830
+ this.#fileBrowser = null;
4062
4831
  this.#trajectory = null;
4063
4832
  this.#settings = null;
4064
4833
  this.#search = null;
@@ -4070,7 +4839,9 @@ export class LocalTui {
4070
4839
  this.#promptDisplaced = null;
4071
4840
  if (displaced === null)
4072
4841
  return;
4073
- if (displaced.kind === 'trajectory')
4842
+ if (displaced.kind === 'fileBrowser')
4843
+ this.#fileBrowser = displaced.browser;
4844
+ else if (displaced.kind === 'trajectory')
4074
4845
  this.#trajectory = displaced.state;
4075
4846
  else if (displaced.kind === 'settings') {
4076
4847
  this.#settings = displaced.state;
@@ -4116,7 +4887,7 @@ export class LocalTui {
4116
4887
  * that did not answer to the shared list vocabulary, which is exactly the
4117
4888
  * inconsistency the reader noticed.
4118
4889
  */
4119
- const answer = await this.prompt({
4890
+ const answer = await this.prompt({ localize: true,
4120
4891
  title: 'Copy',
4121
4892
  question: items.length === 1 ? 'One thing to copy' : 'Choose what to copy',
4122
4893
  filterable: true,
@@ -4152,13 +4923,17 @@ export class LocalTui {
4152
4923
  }
4153
4924
  #settingsActions() {
4154
4925
  const runtimeActions = [
4155
- { id: 'command:model', label: 'Model and reasoning', description: 'Open /model; changes the current selection and saves the default route.', value: [this.#model, this.#reasoningEffort].filter(Boolean).join(' · '), values: [], action: 'model' },
4156
- { id: 'command:permission', label: 'Access', description: 'Open /permission to choose access for this session.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
4157
- { id: 'command:agent', label: 'Agent preset', description: 'Open /agent to choose the session Agent preset.', value: this.#sessionControls?.agentPreset ?? 'Choose', values: [], action: 'agent' },
4926
+ { id: 'command:model', behavior: 'Session + Profile · Choose scope in Model', group: 'Models', label: 'Model and reasoning', description: 'Open /model; changes the current selection and saves the default route.', value: [this.#model, this.#reasoningEffort].filter(Boolean).join(' · '), values: [], action: 'model' },
4927
+ { id: 'command:permission', behavior: 'Current session · Applies now', group: 'General', label: 'Current session access', description: 'Changes only this session. Default access controls future sessions.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
4928
+ { id: 'command:agent', behavior: 'Current session · Before the first prompt', group: 'Agent presets', label: 'Current session preset', description: 'Choose before the first prompt; does not change the new-session default.', value: this.#sessionControls?.agentPreset === undefined ? 'Choose' : formatAgentPreset(this.#sessionControls.agentPreset), values: [], action: 'agent' },
4929
+ { id: 'command:login', behavior: 'Provider credentials', group: 'Models', label: 'Provider credentials', description: 'Sign in or configure a provider. Does not change the selected model.', value: 'Configure', values: [], action: 'login' },
4930
+ { id: 'command:logout', behavior: 'Provider credentials', group: 'Models', label: 'Sign out', description: 'Remove saved provider credentials; environment credentials remain available.', value: 'Choose provider', values: [], action: 'logout' },
4931
+ { id: 'command:plugins', group: 'Plugins', label: 'Manage plugins', description: 'Inspect installed entries, enable bundles, discover plugins or start Creator.', value: 'Open', values: [], action: 'plugins' },
4158
4932
  ].filter(item => this.#runtimeCommands.some(command => command.name === item.action));
4159
4933
  const customBindings = Object.entries(this.#keybindings).filter(([key, action]) => DEFAULT_KEYBINDINGS[key] !== action).length;
4160
4934
  return [
4161
4935
  ...runtimeActions,
4936
+ ...(this.#goalHandler === undefined ? [] : [{ id: 'action:goal', behavior: 'Current session · Applies now', group: 'General', label: 'Current goal', description: 'Edit, pause, resume or clear this session’s goal.', value: 'Open', values: [], action: 'manage-goal' }]),
4162
4937
  ...(this.#keybindingsPath === undefined ? [] : [{
4163
4938
  id: 'action:keybindings',
4164
4939
  label: 'Keybindings',
@@ -4208,6 +4983,7 @@ export class LocalTui {
4208
4983
  }
4209
4984
  #configureNotifications() {
4210
4985
  this.#notifications.configure({
4986
+ language: resolveUiLanguage(this.#uiLanguage),
4211
4987
  policy: this.#notificationPolicy,
4212
4988
  thresholdMs: NOTIFICATION_THRESHOLD_MS,
4213
4989
  });
@@ -4234,32 +5010,66 @@ export class LocalTui {
4234
5010
  this.#term.output.write(sequence);
4235
5011
  }, 25);
4236
5012
  }
4237
- /**
4238
- * Help is reference, not conversation: 60 rows of it pushed into immutable
4239
- * scrollback and vanished on resume, so the reader paid for it twice. It opens
4240
- * in the reading surface instead, where it can be searched and left with Esc.
4241
- */
4242
5013
  #runHelp(full) {
4243
- const text = [
4244
- formatHelpText(this.#commands()),
4245
- '',
4246
- full
4247
- ? `**Keyboard Shortcuts · ${hotkeyCount(this.#keybindings)} shortcuts**`
4248
- : '**Essential Shortcuts**',
4249
- '',
4250
- full ? formatHotkeysText(this.#keybindings) : formatEssentialHotkeysText(this.#keybindings),
4251
- ...(full ? [] : ['', 'Use `/help full` to view every keyboard shortcut.']),
4252
- ].join('\n');
4253
- this.commandOutput('help', text, { focus: true });
5014
+ if (!this.#tty) {
5015
+ this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings),
5016
+ '', formatHelpText(this.#commands(), resolveUiLanguage(this.#uiLanguage)),
5017
+ ...(full ? ['', formatHotkeysText(this.#keybindings)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
5018
+ return;
5019
+ }
5020
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
5021
+ return;
5022
+ const abort = new AbortController();
5023
+ this.#fileReviewAbort = abort;
5024
+ const surface = {
5025
+ prompt: (request) => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5026
+ copyText: (text, label) => this.copyText(text, label),
5027
+ insertText: (text) => this.insertText(text),
5028
+ };
5029
+ void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal)
5030
+ .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5031
+ this.notice(String(error), { level: 'error' }); })
5032
+ .finally(() => { if (this.#fileReviewAbort === abort)
5033
+ this.#fileReviewAbort = undefined; });
4254
5034
  }
4255
- /**
4256
- * Tools are reference, like the command list: a table of names and
4257
- * descriptions the reader consults and dismisses, not a step of the work.
4258
- */
5035
+ async #readCommandOutput(block) {
5036
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null || this.#disposed)
5037
+ return;
5038
+ const abort = new AbortController();
5039
+ this.#fileReviewAbort = abort;
5040
+ try {
5041
+ await readReference({
5042
+ prompt: request => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5043
+ copyText: (text, label) => this.copyText(text, label),
5044
+ }, `/${block.command}`, block.text, abort.signal);
5045
+ }
5046
+ catch (error) {
5047
+ if (!abort.signal.aborted && !this.#disposed)
5048
+ this.notice(String(error), { level: 'error' });
5049
+ }
5050
+ finally {
5051
+ if (this.#fileReviewAbort === abort)
5052
+ this.#fileReviewAbort = undefined;
5053
+ }
5054
+ }
5055
+ /** Open the searchable catalog, retaining plain output for line mode. */
4259
5056
  #runTools() {
4260
- // The first row of a command result is its heading, so a table needs one.
4261
- const heading = `Tools · ${this.#tools.length} active`;
4262
- this.commandOutput('tools', `${heading}\n\n${formatToolsText(this.#tools)}`, { focus: true });
5057
+ if (!this.#tty) {
5058
+ this.commandOutput('tools', `Tools · ${this.#tools.length} active\n\n${formatToolsText(this.#tools)}`);
5059
+ return;
5060
+ }
5061
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
5062
+ return;
5063
+ const abort = new AbortController();
5064
+ this.#fileReviewAbort = abort;
5065
+ void browseTools({
5066
+ prompt: request => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5067
+ copyText: (text, label) => this.copyText(text, label),
5068
+ }, this.#tools, resolveUiLanguage(this.#uiLanguage), abort.signal)
5069
+ .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5070
+ this.notice(String(error), { level: 'error' }); })
5071
+ .finally(() => { if (this.#fileReviewAbort === abort)
5072
+ this.#fileReviewAbort = undefined; });
4263
5073
  }
4264
5074
  /** Resolve a configured action for one key id, tolerating camelCase ids. */
4265
5075
  #boundAction(id) {
@@ -4363,10 +5173,12 @@ export class LocalTui {
4363
5173
  this.#render();
4364
5174
  return true;
4365
5175
  }
5176
+ if (action === 'manage-goal') {
5177
+ this.#openGoal();
5178
+ return true;
5179
+ }
4366
5180
  if (action === 'manage-queue') {
4367
- if (this.#inspected !== undefined)
4368
- this.notice('Return to the parent session to manage queued messages.');
4369
- else if (this.#queueHandler === undefined)
5181
+ if (this.#queueHandler === undefined)
4370
5182
  this.notice('Queue management is unavailable in this profile.');
4371
5183
  else if (this.#queueHotkeyAbort === undefined) {
4372
5184
  const abort = new AbortController();
@@ -4382,33 +5194,30 @@ export class LocalTui {
4382
5194
  if (action === 'steer-turn') {
4383
5195
  if (this.#search !== null)
4384
5196
  return false;
4385
- if (this.#inspected !== undefined || this.#state.status !== 'running') {
4386
- this.notice('Steering needs an active parent turn. Use Enter to send the next message.');
4387
- }
4388
- else if (this.#images.count > 0 || this.#files.copies().length > 0) {
4389
- this.notice('Use Enter to queue attachments; steering accepts text only.');
5197
+ if (this.#inspected !== undefined && !this.#inspected.writable)
5198
+ return true;
5199
+ if (this.#state.status !== 'running') {
5200
+ this.notice('Steering needs an active turn. Use Enter to send the next message.');
4390
5201
  }
4391
5202
  else if (this.#queueEditNewer !== null || this.#queueEditPending) {
4392
5203
  this.notice('Finish editing the queued follow-up with Enter before steering.');
4393
5204
  }
4394
- else if (this.#editor.expandedText.trim() !== '') {
4395
- try {
4396
- if (this.#steerHandler === undefined)
4397
- throw new Error('Steering is unavailable in this profile.');
4398
- this.#steerHandler(this.#editor.expandedText);
4399
- this.#editor.setText('');
4400
- this.#literalInput = false;
4401
- this.#historyIndex = 0;
4402
- this.#clearDraft();
4403
- this.#ac = null;
4404
- this.#render();
4405
- }
4406
- catch (error) {
4407
- this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
4408
- }
5205
+ else if (this.#editor.expandedText.trim() !== '' || this.#images.count > 0 || this.#files.copies().length > 0) {
5206
+ this.#submit(this.#editor.expandedText, 'steer');
4409
5207
  }
4410
5208
  return true;
4411
5209
  }
5210
+ if (action === 'quote-selection')
5211
+ return true;
5212
+ if (action === 'tool-actions') {
5213
+ const anchor = this.#viewportBlock();
5214
+ const block = anchor === undefined ? undefined : this.#state.blocks[anchor];
5215
+ if (block?.kind === 'commandOutput')
5216
+ void this.#readCommandOutput(block);
5217
+ else
5218
+ void this.#openToolActions();
5219
+ return true;
5220
+ }
4412
5221
  if (action === 'review-changes') {
4413
5222
  // The keyboard twin of right-clicking a tool's path: open the files the
4414
5223
  // tool under the viewport declared. It shares the mouse path's region
@@ -4424,7 +5233,7 @@ export class LocalTui {
4424
5233
  const regions = this.#fileActionFrame?.transcript?.fileRegions ?? [];
4425
5234
  const region = (top === undefined ? undefined : regions.find(region => top >= region.start && top < region.end))
4426
5235
  ?? (this.#inspecting ? regions.find(region => this.#state.blocks[region.block]?.kind === 'tool' && rows.some(row => row >= region.start && row < region.end)) : undefined);
4427
- const block = this.#state.blocks[region?.block ?? (this.#viewportBlock() ?? this.#state.blocks.length - 1)];
5236
+ const block = this.#currentTool() ?? this.#state.blocks[region?.block ?? (this.#viewportBlock() ?? this.#state.blocks.length - 1)];
4428
5237
  const files = block?.kind === 'tool' ? blockFileAction(block) : undefined;
4429
5238
  if (files === undefined) {
4430
5239
  this.#showTransientStatus('No tool files here · use /diff', 4_000);
@@ -4490,7 +5299,16 @@ export class LocalTui {
4490
5299
  return true;
4491
5300
  }
4492
5301
  if (action === 'retry') {
4493
- this.#submit('/retry');
5302
+ if (!this.#canContinue())
5303
+ return true;
5304
+ const request = { sessionId: this.#sessionId, failureSeq: this.#sessionControls.continuation };
5305
+ this.#continuing = true;
5306
+ void this.#continueHandler(request).catch(error => {
5307
+ if (!this.#disposed && this.#sessionId === request.sessionId)
5308
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
5309
+ }).finally(() => { this.#continuing = false; if (!this.#disposed)
5310
+ this.#render(); });
5311
+ this.#render();
4494
5312
  return true;
4495
5313
  }
4496
5314
  if (action === 'cycle-model-forward') {
@@ -4514,12 +5332,11 @@ export class LocalTui {
4514
5332
  this.#startAsyncPaste(this.#pasteClipboard());
4515
5333
  return true;
4516
5334
  }
4517
- // A queued follow-up is the one action that must still work while the turn
4518
- // it belongs to is running; Enter is which-turn, this is after-this-turn.
5335
+ // Explicit queue delivery is independent of the busy-Enter preference.
4519
5336
  if (action === 'follow-up') {
4520
- if (this.#editor.expandedText.trim() === '')
5337
+ if (this.#editor.expandedText.trim() === '' && this.#images.count === 0 && this.#files.copies().length === 0)
4521
5338
  return false;
4522
- this.#submit(this.#editor.expandedText);
5339
+ this.#submit(this.#editor.expandedText, 'queue');
4523
5340
  return true;
4524
5341
  }
4525
5342
  if (action !== 'external-editor')
@@ -4574,8 +5391,10 @@ export class LocalTui {
4574
5391
  this.#images.clear();
4575
5392
  this.#files.clear();
4576
5393
  if (options.clearQueueEdit) {
5394
+ if (this.#queueEditNewer !== null)
5395
+ this.#queuedSubmissions.push(...this.#queueEditNewer);
4577
5396
  this.#queueEditNewer = null;
4578
- this.#queueEditPending = false;
5397
+ this.#cancelQueueRecall();
4579
5398
  }
4580
5399
  if (options.closeSearch)
4581
5400
  this.#search = null;
@@ -4633,11 +5452,39 @@ export class LocalTui {
4633
5452
  this.#historyIndex = 0;
4634
5453
  this.#refreshAutocomplete();
4635
5454
  }
5455
+ async #testKeys() {
5456
+ const abort = new AbortController();
5457
+ // Reuse browsing cancellation: approvals and session switches own the terminal.
5458
+ this.#fileReviewAbort = abort;
5459
+ this.#keyTest = { signal: abort.signal, observe: event => {
5460
+ const pending = this.#prompt;
5461
+ const detail = keyTestDescription(event, this.#keybindings, resolveUiLanguage(this.#uiLanguage));
5462
+ if (pending?.request.signal !== abort.signal || detail === undefined)
5463
+ return;
5464
+ pending.sourceRequest = { ...pending.sourceRequest, detail };
5465
+ pending.request = localizePrompt(pending.sourceRequest, resolveUiLanguage(this.#uiLanguage));
5466
+ this.#render();
5467
+ } };
5468
+ try {
5469
+ await this.prompt({ localize: true, title: 'Key test', presentation: 'progress',
5470
+ question: 'Press a key. Esc or Ctrl+C returns without changing bindings.',
5471
+ detail: uiText(resolveUiLanguage(this.#uiLanguage), 'If nothing appears, the terminal or operating system may have intercepted the key.'),
5472
+ allowCustom: false, notify: false, signal: abort.signal, onSuperseded: () => { abort.abort(); } });
5473
+ }
5474
+ finally {
5475
+ if (this.#keyTest?.signal === abort.signal)
5476
+ this.#keyTest = undefined;
5477
+ if (this.#fileReviewAbort === abort)
5478
+ this.#fileReviewAbort = undefined;
5479
+ }
5480
+ return !abort.signal.aborted;
5481
+ }
4636
5482
  async #editKeybindings() {
4637
5483
  if (this.#keybindingsPath === undefined)
4638
5484
  return;
4639
5485
  while (!this.#disposed) {
4640
5486
  const options = [
5487
+ { label: 'Test keys', value: '__test_keys__', description: 'See what the terminal sends without running an action.' },
4641
5488
  ...TUI_ACTIONS.map(action => {
4642
5489
  const keys = keysForAction(this.#keybindings, action);
4643
5490
  const defaultKeys = keysForAction(DEFAULT_KEYBINDINGS, action);
@@ -4653,7 +5500,7 @@ export class LocalTui {
4653
5500
  description: 'Restore default shortcuts in keybindings.json',
4654
5501
  },
4655
5502
  ];
4656
- const choice = await this.prompt({
5503
+ const choice = await this.prompt({ localize: true,
4657
5504
  title: 'Keybindings',
4658
5505
  question: 'Choose an action to remap, or Esc to return to /settings.',
4659
5506
  options,
@@ -4663,6 +5510,11 @@ export class LocalTui {
4663
5510
  });
4664
5511
  if (choice === null || this.#disposed)
4665
5512
  break;
5513
+ if (choice === '__test_keys__') {
5514
+ if (!await this.#testKeys())
5515
+ break;
5516
+ continue;
5517
+ }
4666
5518
  if (choice === '__reset_all__') {
4667
5519
  try {
4668
5520
  saveKeybindings(this.#keybindingsPath, DEFAULT_KEYBINDINGS);
@@ -4681,7 +5533,7 @@ export class LocalTui {
4681
5533
  continue;
4682
5534
  const currentRawKeys = Object.entries(this.#keybindings).filter(([, bound]) => bound === action).map(([key]) => key);
4683
5535
  const defaultKeys = keysForAction(DEFAULT_KEYBINDINGS, action);
4684
- const entered = await this.prompt({
5536
+ const entered = await this.prompt({ localize: true,
4685
5537
  title: `Remap ${TUI_ACTION_LABELS[action]}`,
4686
5538
  question: `Enter key chord (e.g. ctrl+s, alt+k), "default" (${defaultKeys}), or "none" to unbind.`,
4687
5539
  initialInput: currentRawKeys[0] ?? '',
@@ -4756,16 +5608,18 @@ export class LocalTui {
4756
5608
  * above them.
4757
5609
  */
4758
5610
  const anchored = anchor === undefined ? undefined : this.#state.blocks[anchor];
4759
- if (anchor !== undefined && (anchored?.kind === 'commandOutput' || anchored?.kind === 'aside')) {
5611
+ if (anchor !== undefined && (anchored?.kind === 'commandOutput' || anchored?.kind === 'aside' || (anchored?.kind === 'notice' && anchored.detail !== undefined))) {
4760
5612
  if (this.#expandedOutputs.has(anchored))
4761
5613
  this.#expandedOutputs.delete(anchored);
4762
5614
  else
4763
5615
  this.#expandedOutputs.add(anchored);
4764
5616
  this.#inspect(anchor);
5617
+ this.#focusBlock = anchor;
5618
+ this.#focusBlockEdge = 'start';
4765
5619
  this.#render();
4766
5620
  return;
4767
5621
  }
4768
- const groups = turnGroups(this.#state).filter(item => !item.live);
5622
+ const groups = turnGroups(this.#state);
4769
5623
  // The answer, changed-file record, and following notices still address
4770
5624
  // their turn when the reader is following the tail.
4771
5625
  const reach = (group) => {
@@ -4788,18 +5642,132 @@ export class LocalTui {
4788
5642
  return anchor >= first && anchor <= reach(item);
4789
5643
  });
4790
5644
  if (group !== undefined) {
4791
- this.#toggleSet(this.#openedGroups, group.key);
4792
- this.#inspect((group.answer ?? group.until) - 1);
5645
+ const scope = this.#detailScope(group);
5646
+ for (const member of groups)
5647
+ if (this.#detailScope(member) === scope)
5648
+ this.#openedGroups.add(member.key);
5649
+ const rows = this.#processRows();
5650
+ this.#processSelection = rows.includes(anchor) ? anchor : rows[0];
5651
+ this.#ac = null;
5652
+ this.#inspect(this.#processSelection ?? group.start);
5653
+ this.#focusBlock = this.#processSelection ?? group.start;
5654
+ this.#focusBlockEdge = 'start';
4793
5655
  this.#render();
4794
5656
  return;
4795
5657
  }
4796
- // A live turn already has the process presentation; Alt+O owns tool details.
5658
+ }
5659
+ /** The process list follows durable block order and grows without moving selection. */
5660
+ #processRows() {
5661
+ return turnGroups(this.#state).filter(group => this.#openedGroups.has(group.key)).flatMap(group => this.#state.blocks.slice(group.start, group.until).flatMap((block, offset) => block.kind === 'tool' || block.kind === 'assistant' && block.reasoning.trim() !== ''
5662
+ ? [group.start + offset] : []));
5663
+ }
5664
+ #handleProcessNavigation(event) {
5665
+ const selected = this.#processSelection;
5666
+ if (selected === undefined)
5667
+ return false;
5668
+ if (event.type === 'text' || event.type === 'paste-start') {
5669
+ this.#processSelection = undefined;
5670
+ return false;
5671
+ }
5672
+ if (event.type !== 'key')
5673
+ return false;
5674
+ if (event.id === 'escape' || event.id === 'end') {
5675
+ this.#followTail();
5676
+ this.#render();
5677
+ return true;
5678
+ }
5679
+ if (event.id === 'home') {
5680
+ const current = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
5681
+ this.#processSelection = current?.block ?? selected;
5682
+ this.#focusBlock = this.#processSelection;
5683
+ this.#focusBlockEdge = 'start';
5684
+ this.#render();
5685
+ return true;
5686
+ }
5687
+ if (event.id === 'up' || event.id === 'down') {
5688
+ const rows = this.#processRows();
5689
+ const at = Math.max(0, rows.indexOf(selected));
5690
+ const next = rows[Math.max(0, Math.min(rows.length - 1, at + (event.id === 'up' ? -1 : 1)))];
5691
+ if (next !== undefined) {
5692
+ this.#processSelection = next;
5693
+ const row = this.#blockStarts[next];
5694
+ if (row === undefined || row < this.#scrollStart || row >= this.#scrollStart + this.#scrollBudget - 1) {
5695
+ this.#focusBlock = next;
5696
+ this.#focusBlockEdge = 'start';
5697
+ }
5698
+ }
5699
+ this.#render();
5700
+ return true;
5701
+ }
5702
+ if (!['enter', 'left', 'right'].includes(event.id))
5703
+ return false;
5704
+ this.#toggleProcessItem(selected, event.id);
5705
+ return true;
5706
+ }
5707
+ #toggleProcessItem(selected, direction) {
5708
+ const block = this.#state.blocks[selected];
5709
+ if (block?.kind === 'tool') {
5710
+ const visible = this.#detailedToolIds();
5711
+ const wasOpen = visible.has(block.callId);
5712
+ const open = direction === 'right' || direction !== 'left' && !wasOpen;
5713
+ const raw = this.#detailedToolIds(false).has(block.callId);
5714
+ if (wasOpen && !open) {
5715
+ const target = this.#fileActionFrame?.transcript?.disclosures?.find(item => item.block === selected);
5716
+ const offset = target?.detailEnd !== undefined && this.#scrollStart >= target.row && this.#scrollStart < target.detailEnd
5717
+ ? this.#scrollStart - target.row : 0;
5718
+ this.#toolReadingPositions.set(block.callId, { offset, width: this.#term.width(), raw });
5719
+ }
5720
+ if (open !== wasOpen) {
5721
+ const saved = open ? this.#toolReadingPositions.get(block.callId) : undefined;
5722
+ this.#focusBlock = selected;
5723
+ this.#focusBlockEdge = 'start';
5724
+ // Width changes and raw/readable switches change the row layout.
5725
+ this.#focusBlockOffset = saved?.width === this.#term.width() && saved.raw === raw ? saved.offset : 0;
5726
+ }
5727
+ // Once a reader edits one disclosure, preserve the other visible calls as
5728
+ // individual choices instead of letting the turn-wide override reopen it.
5729
+ this.#expandedToolRows = new Set(visible);
5730
+ this.#detailedTurns.clear();
5731
+ if (open)
5732
+ this.#expandedToolRows.add(block.callId);
5733
+ else
5734
+ this.#expandedToolRows.delete(block.callId);
5735
+ }
5736
+ else if (block?.kind === 'assistant') {
5737
+ const key = reasoningKey(block);
5738
+ const open = direction === 'right' || direction !== 'left' && !this.#visibleReasoning().has(key);
5739
+ if (open) {
5740
+ this.#expandedReasoning.add(key);
5741
+ this.#closedReasoning.delete(key);
5742
+ }
5743
+ else {
5744
+ this.#expandedReasoning.delete(key);
5745
+ this.#closedReasoning.add(key);
5746
+ }
5747
+ }
5748
+ const row = this.#blockStarts[selected];
5749
+ if (row === undefined || row < this.#scrollStart || row > this.#scrollStart + Math.floor(this.#scrollBudget / 2)) {
5750
+ this.#focusBlock = selected;
5751
+ this.#focusBlockEdge = 'start';
5752
+ }
5753
+ this.#render();
5754
+ }
5755
+ #visibleReasoning() {
5756
+ const open = new Set(this.#expandedReasoning);
5757
+ if (!this.#hideThinking)
5758
+ for (const block of this.#state.blocks) {
5759
+ if (block.kind === 'assistant')
5760
+ open.add(reasoningKey(block));
5761
+ }
5762
+ for (const key of this.#closedReasoning)
5763
+ open.delete(key);
5764
+ return open;
4797
5765
  }
4798
5766
  #detailScope(group) {
4799
5767
  return group.turn === undefined ? group.key : `turn:${group.turn}`;
4800
5768
  }
4801
- #detailedToolIds() {
4802
- const calls = new Set();
5769
+ #detailedToolIds(includeReadable = true) {
5770
+ const calls = new Set(includeReadable ? this.#expandedToolRows : []);
4803
5771
  if (this.#detailedTurns.size === 0)
4804
5772
  return calls;
4805
5773
  for (const group of turnGroups(this.#state)) {
@@ -4843,6 +5811,11 @@ export class LocalTui {
4843
5811
  this.#openedGroups.add(member.key);
4844
5812
  const tools = members.flatMap(member => this.#state.blocks.slice(member.start, member.until)
4845
5813
  .flatMap((block, offset) => block.kind === 'tool' ? [member.start + offset] : []));
5814
+ for (const at of tools) {
5815
+ const block = this.#state.blocks[at];
5816
+ if (block?.kind === 'tool')
5817
+ this.#expandedToolRows.delete(block.callId);
5818
+ }
4846
5819
  const target = tools.includes(anchor) ? anchor : tools[0];
4847
5820
  this.#inspect(target);
4848
5821
  this.#focusBlock = target;
@@ -4868,14 +5841,19 @@ export class LocalTui {
4868
5841
  return [];
4869
5842
  const options = {
4870
5843
  width: this.#term.width(),
5844
+ language: resolveUiLanguage(this.#uiLanguage),
4871
5845
  colors: this.#colors,
4872
5846
  themeName: this.#themeName,
5847
+ terminal: this.#terminalColors,
5848
+ compactLinks: this.#compactLinks,
4873
5849
  ...(this.#colorDepth === undefined ? {} : { colorDepth: this.#colorDepth }),
4874
5850
  mathMode: this.#mathMode,
5851
+ mermaidMode: this.#mermaidMode,
4875
5852
  toolsExpanded: this.#toolsExpanded,
4876
5853
  expandedOutputs: this.#expandedOutputs,
4877
- hideThinking: this.#hideThinking,
5854
+ hideThinking: true,
4878
5855
  expandedTools: this.#detailedToolIds(),
5856
+ expandedReasoning: this.#visibleReasoning(),
4879
5857
  ...(this.#openedGroups === undefined ? {} : { openedGroups: this.#openedGroups }),
4880
5858
  };
4881
5859
  return searchMatches(renderedBlocks(this.#state, options), search.query);
@@ -4978,6 +5956,7 @@ export function apply(ctx, config) {
4978
5956
  // settings service, which persists the edit into the active profile patch.
4979
5957
  const storedPrefs = () => {
4980
5958
  const prefs = {
5959
+ uiLanguage: config.uiLanguage.get(),
4981
5960
  theme: config.theme.get(),
4982
5961
  colors: resolveColors(config.colors.get(), term.output.isTTY === true),
4983
5962
  motion: config.motion.get(),
@@ -4988,6 +5967,7 @@ export function apply(ctx, config) {
4988
5967
  terminalState: config.terminalState.get(),
4989
5968
  copyOnSelect: config.copyOnSelect.get(),
4990
5969
  mouseInteraction: config.mouseInteraction.get(),
5970
+ busyEnter: config.busyEnter.get(),
4991
5971
  pasteProtection: config.pasteProtection.get(),
4992
5972
  checkUpdates: config.checkUpdates.get(),
4993
5973
  startupChangelog: config.startupChangelog.get(),