@agi-fans/dsh-tui 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +199 -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 +97 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +2 -8
  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 +197 -120
  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 +111 -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 +112 -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 +1264 -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 +32 -13
  208. package/lib/session/session-controller.js +444 -98
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/subagent-roster.d.ts +1 -0
  211. package/lib/session/subagent-roster.js +16 -5
  212. package/lib/session/subagent-roster.js.map +1 -1
  213. package/lib/session/subagent-tracker.d.ts +2 -0
  214. package/lib/session/subagent-tracker.js +18 -0
  215. package/lib/session/subagent-tracker.js.map +1 -1
  216. package/lib/session/tui-settings.d.ts +9 -5
  217. package/lib/session/tui-settings.js +3 -1
  218. package/lib/session/tui-settings.js.map +1 -1
  219. package/lib/session/workflow-projection.d.ts +2 -1
  220. package/lib/session/workflow-projection.js +34 -4
  221. package/lib/session/workflow-projection.js.map +1 -1
  222. package/lib/views/autocomplete.d.ts +10 -7
  223. package/lib/views/autocomplete.js +44 -37
  224. package/lib/views/autocomplete.js.map +1 -1
  225. package/lib/views/context-overview.d.ts +4 -0
  226. package/lib/views/context-overview.js +43 -0
  227. package/lib/views/context-overview.js.map +1 -0
  228. package/lib/views/copy-targets.d.ts +6 -2
  229. package/lib/views/copy-targets.js +19 -14
  230. package/lib/views/copy-targets.js.map +1 -1
  231. package/lib/views/document-reader.d.ts +10 -1
  232. package/lib/views/document-reader.js +59 -5
  233. package/lib/views/document-reader.js.map +1 -1
  234. package/lib/views/event-views.js +29 -11
  235. package/lib/views/event-views.js.map +1 -1
  236. package/lib/views/file-actions.js +21 -0
  237. package/lib/views/file-actions.js.map +1 -1
  238. package/lib/views/file-browser.d.ts +98 -0
  239. package/lib/views/file-browser.js +120 -0
  240. package/lib/views/file-browser.js.map +1 -0
  241. package/lib/views/history-search.js +4 -4
  242. package/lib/views/history-search.js.map +1 -1
  243. package/lib/views/hotkey-format.d.ts +3 -2
  244. package/lib/views/hotkey-format.js +4 -3
  245. package/lib/views/hotkey-format.js.map +1 -1
  246. package/lib/views/hotkeys.js +10 -12
  247. package/lib/views/hotkeys.js.map +1 -1
  248. package/lib/views/list-view.js +16 -11
  249. package/lib/views/list-view.js.map +1 -1
  250. package/lib/views/prompt-selector.d.ts +23 -1
  251. package/lib/views/prompt-selector.js +123 -60
  252. package/lib/views/prompt-selector.js.map +1 -1
  253. package/lib/views/settings-list.d.ts +6 -1
  254. package/lib/views/settings-list.js +69 -46
  255. package/lib/views/settings-list.js.map +1 -1
  256. package/lib/views/tool-actions.d.ts +16 -0
  257. package/lib/views/tool-actions.js +29 -0
  258. package/lib/views/tool-actions.js.map +1 -0
  259. package/lib/views/trajectory-timeline.d.ts +5 -6
  260. package/lib/views/trajectory-timeline.js +25 -15
  261. package/lib/views/trajectory-timeline.js.map +1 -1
  262. package/lib/views/trajectory.d.ts +23 -2
  263. package/lib/views/trajectory.js +404 -186
  264. package/lib/views/trajectory.js.map +1 -1
  265. package/lib/views/transcript-render.d.ts +39 -15
  266. package/lib/views/transcript-render.js +406 -208
  267. package/lib/views/transcript-render.js.map +1 -1
  268. package/lib/views/transcript-types.d.ts +3 -2
  269. package/lib/views/transcript-types.js.map +1 -1
  270. package/lib/views/workflow.d.ts +16 -0
  271. package/lib/views/workflow.js +42 -0
  272. package/lib/views/workflow.js.map +1 -0
  273. package/package.json +61 -58
  274. package/lib/chrome/welcome-tips.d.ts +0 -23
  275. package/lib/chrome/welcome-tips.js +0 -24
  276. package/lib/chrome/welcome-tips.js.map +0 -1
  277. package/lib/session/steering.d.ts +0 -3
  278. package/lib/session/steering.js +0 -9
  279. 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
  *
@@ -437,6 +471,10 @@ export class LocalTui {
437
471
  */
438
472
  #subagentSelection;
439
473
  #inspected;
474
+ #promptActions = [];
475
+ #promptActionPress;
476
+ #promptChoices = [];
477
+ #promptChoicePress;
440
478
  #promptDocument;
441
479
  /**
442
480
  * 24-bit switch for the current color preference, re-derived whenever that
@@ -444,6 +482,7 @@ export class LocalTui {
444
482
  * hexadecimal palettes without a restart.
445
483
  */
446
484
  #colorDepth = 'ansi16';
485
+ #compactLinks = supportsHyperlinks(process.env);
447
486
  /** Hold the first frame until the capability probe settles; scrollback rows are immutable, so the first real paint must use the reported colors. */
448
487
  #probeGate = false;
449
488
  #copy;
@@ -468,6 +507,7 @@ export class LocalTui {
468
507
  #prefsSaveTask;
469
508
  #disposeTask;
470
509
  #prefsVersion = 0;
510
+ #uiLanguage = 'auto';
471
511
  #agentBehavior;
472
512
  #updateAgentBehavior;
473
513
  #offAgentBehaviorWatch;
@@ -527,6 +567,7 @@ export class LocalTui {
527
567
  this.#syncColorDepth();
528
568
  this.#tty = term.input.isTTY === true && term.output.isTTY === true;
529
569
  this.#draftStore = this.#tty && paths.draftsPath !== undefined ? new ComposerDraftStore(paths.draftsPath) : undefined;
570
+ 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
571
  this.#plain = new PlainTui({
531
572
  term,
532
573
  blocks: () => this.#state.blocks,
@@ -554,7 +595,9 @@ export class LocalTui {
554
595
  // A terminal resize changes the committed/live seam; re-anchor the
555
596
  // live window so it stays anchored at the bottom.
556
597
  this.#selection.clear();
598
+ this.#mouseActionPress = undefined;
557
599
  this.#jumpPress = undefined;
600
+ this.#disclosurePress = undefined;
558
601
  const repaint = () => {
559
602
  this.#resizeTimer = null;
560
603
  this.#renderer.resize(this.#term.width(), this.#term.height());
@@ -637,7 +680,13 @@ export class LocalTui {
637
680
  this.#retryDeadline = Date.now() + event.data.delayMs;
638
681
  if (event.type === 'llm/retry-started' || event.type === 'turn/end')
639
682
  this.#retryDeadline = undefined;
683
+ const claimedHumanInput = event.type === 'agent/inbox/spliced' && event.data.outcome !== 'canceled'
684
+ && event.data.inserted.length === 0 && (event.data.target === 'next-step' ? this.#state.nextStepInbox : this.#state.nextTurnInbox)
685
+ .slice(event.data.start, event.data.start + (event.data.removedCount ?? 0)).some(message => message.source.kind === 'user');
640
686
  this.#state = applyEvent(this.#state, event, presentation, workspace);
687
+ if (claimedHumanInput && this.#tty && event.type === 'agent/inbox/spliced') {
688
+ this.#showTransientStatus(event.data.target === 'next-step' ? 'Steer taken for the next step' : 'Queue taken for the next turn');
689
+ }
641
690
  const compacting = this.#state.status === 'compacting';
642
691
  if (compacting && this.#compactionStartedAt === undefined)
643
692
  this.#compactionStartedAt = Date.now();
@@ -749,12 +798,24 @@ export class LocalTui {
749
798
  const statusChanged = previous.length !== incoming.length || incoming.some((agent, index) => {
750
799
  const before = previous[index];
751
800
  return before === undefined || before.id !== agent.id || before.label !== agent.label
752
- || before.phase !== agent.phase || before.depth !== agent.depth || before.mode !== agent.mode;
801
+ || before.phase !== agent.phase || before.needsApproval !== agent.needsApproval || before.depth !== agent.depth || before.mode !== agent.mode;
753
802
  });
803
+ const previousItems = rosterItems(this.#subagents);
804
+ const incomingItems = rosterItems(roster);
805
+ const workflowsChanged = (this.#subagents?.workflows?.length ?? 0) !== (roster?.workflows?.length ?? 0)
806
+ || (roster?.workflows ?? []).some((run, index) => {
807
+ const before = this.#subagents?.workflows?.[index];
808
+ return before?.id !== run.id || before.name !== run.name || before.status !== run.status || before.phase !== run.phase || before.members !== run.members;
809
+ });
810
+ const selectedId = this.#subagentSelection === undefined ? undefined : previousItems[this.#subagentSelection]?.key;
754
811
  this.#subagents = roster === undefined
755
812
  ? undefined
756
- : { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })) };
757
- if ((this.#subagents?.agents.length ?? 0) === 0)
813
+ : { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })), ...(roster.workflows === undefined ? {} : { workflows: [...roster.workflows] }) };
814
+ if (selectedId !== undefined) {
815
+ const index = incomingItems.findIndex(item => item.key === selectedId);
816
+ this.#subagentSelection = index < 0 ? undefined : index;
817
+ }
818
+ if (incomingItems.length === 0)
758
819
  this.#subagentSelection = undefined;
759
820
  const inspected = this.#inspected;
760
821
  if (inspected !== undefined) {
@@ -764,12 +825,13 @@ export class LocalTui {
764
825
  id: current.id,
765
826
  label: current.label,
766
827
  phase: current.phase,
828
+ needsApproval: current.needsApproval === true,
767
829
  ...(current.mode === undefined ? {} : { mode: current.mode }),
768
830
  writable: current.mode === 'continuable',
769
831
  };
770
832
  }
771
833
  }
772
- if (!statusChanged)
834
+ if (!statusChanged && !workflowsChanged)
773
835
  return;
774
836
  this.#syncTick();
775
837
  if (this.#tty)
@@ -777,11 +839,16 @@ export class LocalTui {
777
839
  }
778
840
  setInspectedSubagent(inspected) {
779
841
  if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
780
- && this.#inspected?.phase === inspected?.phase && this.#inspected?.mode === inspected?.mode
842
+ && this.#inspected?.phase === inspected?.phase && this.#inspected?.needsApproval === inspected?.needsApproval && this.#inspected?.mode === inspected?.mode
781
843
  && this.#inspected?.writable === inspected?.writable)
782
844
  return;
845
+ if (this.#inspected?.id !== inspected?.id) {
846
+ this.#fileReviewAbort?.abort();
847
+ this.#closeFileBrowser();
848
+ }
783
849
  this.#switchDraft(inspected?.id ?? this.#sessionId);
784
850
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
851
+ this.#drainPendingInput();
785
852
  if (inspected !== undefined) {
786
853
  this.#inspectEscapeFenceUntil = 0;
787
854
  this.#subagentSelection = undefined;
@@ -794,20 +861,54 @@ export class LocalTui {
794
861
  this.#tools = tools.map((tool) => ({ name: tool.name, description: tool.description }));
795
862
  }
796
863
  setCommands(commands) {
797
- this.#runtimeCommands = commands.map((command) => ({
798
- name: command.name,
799
- description: command.description,
800
- ...(command.inputHint === undefined ? {} : { inputHint: command.inputHint }),
801
- }));
864
+ this.#runtimeCommands = commands.map(command => ({ ...command }));
802
865
  this.#syncSettingsActions();
803
866
  this.#refreshAutocomplete();
804
867
  if (this.#tty)
805
868
  this.#render();
806
869
  }
870
+ async reviewFiles(request) {
871
+ if (!this.#tty || this.#disposed || request.signal.aborted)
872
+ return;
873
+ this.#flushPasteBurst(true);
874
+ this.#pasteBurst.clear();
875
+ this.#finishPrompt(null);
876
+ this.#closeFileBrowser();
877
+ this.#selection.clear();
878
+ this.#trajectory = null;
879
+ this.#settings = null;
880
+ this.#search = null;
881
+ this.#ac = null;
882
+ const browser = new FileBrowser(request, () => { if (!this.#disposed)
883
+ this.#render(); }, () => {
884
+ if (this.#fileBrowser === browser)
885
+ this.#fileBrowser = null;
886
+ if (this.#promptDisplaced?.kind === 'fileBrowser' && this.#promptDisplaced.browser === browser)
887
+ this.#promptDisplaced = null;
888
+ this.#selection.clear();
889
+ this.#trajectoryPress = undefined;
890
+ if (!this.#disposed)
891
+ this.#render();
892
+ });
893
+ this.#fileBrowser = browser;
894
+ browser.start();
895
+ await browser.done;
896
+ }
897
+ #closeFileBrowser() {
898
+ this.#fileBrowser?.close();
899
+ if (this.#promptDisplaced?.kind === 'fileBrowser')
900
+ this.#promptDisplaced.browser.close();
901
+ }
902
+ #applyFileBrowserInput(event) {
903
+ this.#fileBrowser?.input(event, createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks), this.#term.width(), this.#term.height());
904
+ }
807
905
  openTrajectory(events) {
808
906
  if (!this.#tty)
809
907
  return false;
810
908
  this.#finishPrompt(null);
909
+ this.#selection.clear();
910
+ this.#trajectoryPress = undefined;
911
+ this.#closeFileBrowser();
811
912
  this.#trajectory = createTrajectory(events);
812
913
  this.#settings = null;
813
914
  this.#search = null;
@@ -848,6 +949,11 @@ export class LocalTui {
848
949
  .join('\n');
849
950
  }
850
951
  notice(text, options = {}) {
952
+ if (options.transient) {
953
+ if (this.#tty && !this.#disposed)
954
+ this.#showTransientStatus(text);
955
+ return;
956
+ }
851
957
  const merged = options.job === undefined ? undefined : mergeJobSettlement(this.#state, options.job);
852
958
  if (merged !== undefined && this.#tty) {
853
959
  const origin = merged.blocks.find((block, index) => block !== this.#state.blocks[index]);
@@ -946,7 +1052,6 @@ export class LocalTui {
946
1052
  output: result.output === '' ? current.output : result.output,
947
1053
  status: failed ? 'error' : 'ok',
948
1054
  ...(result.code === undefined ? {} : { code: result.code }),
949
- ...(request.share === true ? { shared: true } : {}),
950
1055
  }));
951
1056
  paint();
952
1057
  return result;
@@ -1080,7 +1185,24 @@ export class LocalTui {
1080
1185
  next[at] = update(next[at]);
1081
1186
  this.#state = { ...this.#state, blocks: next };
1082
1187
  }
1188
+ openContext(request) {
1189
+ let snapshot = request.read();
1190
+ return this.#promptWithDocument({
1191
+ localize: ['title', 'options'], title: 'Context', question: '',
1192
+ detail: contextOverviewDetails(snapshot), presentation: 'document', allowCustom: false, notify: false,
1193
+ options: [{ label: 'Details', value: 'details' }, { label: 'Copy session ID', value: 'copy' },
1194
+ ...(request.canCompact ? [{ label: 'Compact', value: 'compact' }] : [])],
1195
+ refreshDocument: () => { snapshot = request.read(); return contextOverviewDetails(snapshot); },
1196
+ signal: request.signal, onSuperseded: request.onSuperseded,
1197
+ }, (theme, width) => renderContextOverview(snapshot, theme, width));
1198
+ }
1083
1199
  prompt(request) {
1200
+ return this.#promptWithDocument(request);
1201
+ }
1202
+ #promptWithDocument(request, renderDocument) {
1203
+ const sourceRequest = request;
1204
+ const browserState = promptBrowserState(request);
1205
+ request = localizePrompt(request, resolveUiLanguage(this.#uiLanguage));
1084
1206
  // Background approvals take the terminal from a user-opened file reader.
1085
1207
  if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal)
1086
1208
  this.#fileReviewAbort.abort();
@@ -1108,13 +1230,22 @@ export class LocalTui {
1108
1230
  }
1109
1231
  if (this.#tty && request.documentImage !== undefined && this.#imageProtocol !== undefined)
1110
1232
  this.#term.output.write('\x1b[16t');
1233
+ this.#selection.clear();
1234
+ this.#promptActionPress = undefined;
1235
+ this.#promptChoicePress = undefined;
1236
+ this.#disclosurePress = undefined;
1237
+ this.#mouseActionPress = undefined;
1238
+ this.#jumpPress = undefined;
1239
+ this.#wheelGesture = IDLE_WHEEL;
1111
1240
  const savedInput = this.#editor.snapshot();
1112
- this.#editor.setText(request.allowCustom === false || request.filterable === true || (request.options?.length && request.initialInput === undefined) ? '' : request.initialInput ?? '');
1241
+ this.#editor.setText(request.filterable === true ? browserState?.query ?? ''
1242
+ : request.allowCustom === false || (request.options?.length && request.initialInput === undefined) ? '' : request.initialInput ?? '');
1113
1243
  this.#ac = null;
1114
1244
  const displaced = this.#displaceSurface();
1115
1245
  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,
1246
+ const selected = Math.max(0, filteredPromptOptions(request, this.#editor.text).findIndex(option => (option.value ?? option.label) === (browserState?.selected ?? request.initialValue)));
1247
+ 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,
1248
+ ...(browserState === undefined ? {} : { browserState }),
1118
1249
  ...(request.armMs !== undefined && request.armMs > 0 ? { armedUntil: Date.now() + request.armMs } : {}),
1119
1250
  ...(request.documentPosition === undefined ? {} : { documentAnchor: request.documentPosition, documentQuery: request.documentPosition.query,
1120
1251
  documentWordWrap: request.documentPosition.wordWrap, documentColumn: request.documentPosition.column }),
@@ -1137,7 +1268,7 @@ export class LocalTui {
1137
1268
  }
1138
1269
  if (this.#tty && (request.refreshDocument !== undefined || request.refreshDocumentSource !== undefined || request.refreshOptions !== undefined)) {
1139
1270
  const tick = setInterval(() => {
1140
- if (this.#disposed || this.#prompt === null)
1271
+ if (this.#disposed || this.#prompt === null || this.#selection.active)
1141
1272
  return;
1142
1273
  try {
1143
1274
  const detail = request.refreshDocument?.();
@@ -1147,16 +1278,17 @@ export class LocalTui {
1147
1278
  if (source !== undefined && source !== this.#prompt.request.documentSource) {
1148
1279
  const shift = (this.#prompt.request.documentSource?.firstLine ?? 1) - (source.firstLine ?? 1);
1149
1280
  const anchor = this.#prompt.documentAnchor;
1150
- this.#prompt = { ...this.#prompt, request: { ...this.#prompt.request, documentSource: source },
1281
+ this.#prompt = { ...this.#prompt, sourceRequest: { ...this.#prompt.sourceRequest, documentSource: source }, request: { ...this.#prompt.request, documentSource: source },
1151
1282
  ...(anchor === undefined ? {} : { documentAnchor: { ...anchor, row: Math.max(0, anchor.row + shift) } }) };
1152
1283
  changed = true;
1153
1284
  }
1154
- if (options !== undefined && options !== this.#prompt.request.options) {
1155
- this.#prompt = refreshPromptOptions(this.#prompt, options, this.#editor.text);
1285
+ if (options !== undefined && options !== this.#prompt.sourceRequest.options) {
1286
+ this.#prompt.sourceRequest = { ...this.#prompt.sourceRequest, options };
1287
+ this.#prompt = refreshPromptOptions(this.#prompt, localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)).options, this.#editor.text);
1156
1288
  changed = true;
1157
1289
  }
1158
1290
  if (detail !== undefined && detail !== this.#prompt.request.detail) {
1159
- this.#prompt = { ...this.#prompt, request: { ...this.#prompt.request, detail } };
1291
+ this.#prompt = { ...this.#prompt, sourceRequest: { ...this.#prompt.sourceRequest, detail }, request: { ...this.#prompt.request, detail } };
1160
1292
  changed = true;
1161
1293
  }
1162
1294
  if (changed)
@@ -1193,8 +1325,11 @@ export class LocalTui {
1193
1325
  });
1194
1326
  }
1195
1327
  replaceSession(events, presentations, status = 'idle', reason = 'open') {
1196
- if (reason !== 'refresh')
1328
+ if (reason !== 'refresh') {
1197
1329
  this.#fileReviewAbort?.abort();
1330
+ this.#closeFileBrowser();
1331
+ this.#clearTransientStatus();
1332
+ }
1198
1333
  this.#trajectory = null;
1199
1334
  const state = replayEvents(events, presentations);
1200
1335
  // A notice does not turn a blank session into a document that /new needs
@@ -1212,10 +1347,13 @@ export class LocalTui {
1212
1347
  this.#plain.print();
1213
1348
  }
1214
1349
  setSession(info) {
1215
- if (info.id !== this.#sessionId)
1350
+ if (info.id !== this.#sessionId) {
1216
1351
  this.#fileReviewAbort?.abort();
1352
+ this.#closeFileBrowser();
1353
+ }
1217
1354
  this.#switchDraft(this.#inspected?.id ?? info.id);
1218
1355
  this.#sessionId = info.id;
1356
+ this.#drainPendingInput();
1219
1357
  this.#herdr.setSession(info.id);
1220
1358
  const title = info.title?.trim();
1221
1359
  this.#sessionTitle = title === undefined || title === '' ? undefined : title;
@@ -1287,6 +1425,7 @@ export class LocalTui {
1287
1425
  /** Currently applied preferences; sibling rows read these instead of the config references. */
1288
1426
  prefs() {
1289
1427
  return {
1428
+ uiLanguage: this.#uiLanguage,
1290
1429
  theme: this.#themeName,
1291
1430
  colors: this.#colors,
1292
1431
  motion: this.#motion,
@@ -1298,6 +1437,7 @@ export class LocalTui {
1298
1437
  terminalState: this.#terminalState,
1299
1438
  copyOnSelect: this.#copyOnSelect,
1300
1439
  mouseInteraction: this.#mouseInteraction,
1440
+ busyEnter: this.#busyEnter,
1301
1441
  pasteProtection: this.#pasteProtection,
1302
1442
  checkUpdates: this.#checkUpdates,
1303
1443
  startupChangelog: this.#startupChangelog,
@@ -1350,6 +1490,26 @@ export class LocalTui {
1350
1490
  this.#render();
1351
1491
  };
1352
1492
  }
1493
+ #takeQueuedInput() {
1494
+ const scope = this.#sessionId ?? '__default__';
1495
+ const inputs = this.#pendingInputs.session(scope).queued;
1496
+ const index = inputs.findIndex(item => !this.#pendingInputs.receipt(item)?.recovered);
1497
+ const queued = index < 0 ? undefined : inputs.splice(index, 1)[0];
1498
+ if (queued !== undefined)
1499
+ this.#pendingInputs.take(queued, scope);
1500
+ return queued;
1501
+ }
1502
+ #drainPendingInput() {
1503
+ if (this.#pending === null || this.#disposed)
1504
+ return;
1505
+ const queued = this.#takeQueuedInput();
1506
+ if (queued === undefined)
1507
+ return;
1508
+ const pending = this.#pending;
1509
+ this.#pending = null;
1510
+ pending.offAbort?.();
1511
+ pending.resolve(queued);
1512
+ }
1353
1513
  readInput(signal) {
1354
1514
  if (this.#pending !== null)
1355
1515
  return Promise.reject(new Error('omdsh-tui: input read already in flight'));
@@ -1365,7 +1525,7 @@ export class LocalTui {
1365
1525
  return this.#plain.readline(signal);
1366
1526
  // Lines submitted while a turn was still running were queued instead of
1367
1527
  // dropped; serve the oldest before waiting for fresh input.
1368
- const queued = this.#queuedSubmissions.shift();
1528
+ const queued = this.#takeQueuedInput();
1369
1529
  if (queued !== undefined) {
1370
1530
  this.#render();
1371
1531
  return Promise.resolve(queued);
@@ -1387,20 +1547,83 @@ export class LocalTui {
1387
1547
  }
1388
1548
  /** Compatibility helper for provider-level text editing tests and embedders. */
1389
1549
  async readline() {
1390
- return (await this.readInput())?.text ?? null;
1391
- }
1392
- restoreInput(submission) {
1550
+ const submission = await this.readInput();
1551
+ if (submission !== null)
1552
+ this.#pendingInputs.finish(submission);
1553
+ return submission?.text ?? null;
1554
+ }
1555
+ restoreInput(submission, sessionId) {
1556
+ const origin = this.#pendingInputs.receipt(submission)?.sessionId;
1557
+ if (origin !== '__default__')
1558
+ sessionId ??= origin;
1559
+ if (sessionId !== undefined && sessionId !== this.#draftScope || this.#prompt !== null || this.#disposed) {
1560
+ const target = sessionId ?? this.#draftScope;
1561
+ const current = target === this.#draftScope;
1562
+ const editor = new InputEditor();
1563
+ const images = new ComposerImages({ editor, notice: text => this.notice(text) });
1564
+ const files = new ComposerFiles({ text: () => editor.text,
1565
+ append: marker => { editor.setCursor(editor.text.length); editor.handle({ type: 'text', value: (editor.text === '' ? '' : ' ') + marker }); } });
1566
+ let existing = current ? this.#captureDraft() : target === undefined ? undefined : this.#unsavedDrafts.get(target);
1567
+ try {
1568
+ if (target !== undefined)
1569
+ existing ??= this.#draftStore?.load(target);
1570
+ }
1571
+ catch (error) {
1572
+ this.#reportDraftError('restore', error);
1573
+ }
1574
+ if (existing !== undefined) {
1575
+ images.replace({ text: existing.editor.text, images: existing.images });
1576
+ editor.restore(existing.editor);
1577
+ files.restoreSnapshot(existing.files);
1578
+ }
1579
+ images.restore(submission, files.adopt(submission.files ?? []));
1580
+ const draft = { editor: editor.snapshot(), images: images.copies(), files: files.snapshot(editor.text), literal: existing?.literal === true || submission.literal === true };
1581
+ if (current && this.#prompt !== null) {
1582
+ const answer = this.#editor.snapshot();
1583
+ this.#restoreDraft(draft);
1584
+ this.#prompt.savedInput = draft.editor;
1585
+ this.#editor.restore(answer);
1586
+ this.#draftDirty = true;
1587
+ this.#flushDraft();
1588
+ if (this.#draftStore === undefined || !this.#unsavedDrafts.has(this.#pendingScope()))
1589
+ this.#pendingInputs.finish(submission);
1590
+ return;
1591
+ }
1592
+ if (target === undefined)
1593
+ return;
1594
+ this.#unsavedDrafts.set(target, draft);
1595
+ try {
1596
+ if (this.#draftStore !== undefined) {
1597
+ this.#draftStore.save(target, draft);
1598
+ this.#unsavedDrafts.delete(target);
1599
+ }
1600
+ }
1601
+ catch (error) {
1602
+ this.#reportDraftError('save', error);
1603
+ return;
1604
+ }
1605
+ this.#pendingInputs.finish(submission);
1606
+ return;
1607
+ }
1393
1608
  this.#literalInput ||= submission.literal === true;
1394
- this.#images.restore(submission);
1395
- for (const file of submission.files ?? [])
1396
- this.#files.add(file);
1609
+ this.#images.restore(submission, this.#files.adopt(submission.files ?? []));
1610
+ this.#draftDirty = true;
1611
+ this.#flushDraft();
1612
+ if (this.#draftStore === undefined || !this.#unsavedDrafts.has(this.#pendingScope()))
1613
+ this.#pendingInputs.finish(submission);
1397
1614
  this.#refreshAutocomplete();
1398
1615
  if (this.#tty)
1399
1616
  this.#render();
1400
1617
  }
1401
- resolveQueueEdit(submission) {
1618
+ resolveQueueEdit(submission, request) {
1619
+ if (request !== undefined && (request !== this.#queueRecall?.request || request.signal.aborted)) {
1620
+ if (submission !== null)
1621
+ this.restoreInput(submission, request.sessionId);
1622
+ return;
1623
+ }
1402
1624
  if (!this.#queueEditPending)
1403
1625
  return;
1626
+ this.#queueRecall = undefined;
1404
1627
  this.#queueEditPending = false;
1405
1628
  if (submission === null) {
1406
1629
  if (this.#editor.text === '' && this.#images.count === 0 && this.#queueEditNewer?.length === 0) {
@@ -1410,25 +1633,21 @@ export class LocalTui {
1410
1633
  }
1411
1634
  if (this.#queueEditNewer === null)
1412
1635
  this.#queueEditNewer = [];
1413
- if (this.#editor.text !== '' || this.#images.count > 0) {
1636
+ if (this.#hasComposerContent()) {
1414
1637
  this.#queueEditNewer.unshift(this.#currentSubmission());
1415
1638
  }
1416
1639
  this.#replaceInput(submission);
1417
1640
  }
1418
1641
  #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
- };
1642
+ return this.#buildSubmission(this.#editor.expandedText).submission;
1426
1643
  }
1427
1644
  #replaceInput(submission) {
1428
1645
  this.#literalInput = submission.literal === true;
1429
- this.#images.replace(submission);
1430
- this.#files.restore(submission.files);
1646
+ this.#files.clear();
1647
+ this.#images.replace(submission, this.#files.adopt(submission.files ?? []));
1431
1648
  this.#historyIndex = 0;
1649
+ this.#draftDirty = true;
1650
+ this.#flushDraft();
1432
1651
  this.#refreshAutocomplete();
1433
1652
  if (this.#tty)
1434
1653
  this.#render();
@@ -1438,30 +1657,39 @@ export class LocalTui {
1438
1657
  return () => { this.#interrupts.delete(listener); };
1439
1658
  }
1440
1659
  pendingInputs = {
1660
+ receipt: submission => this.#pendingInputs.receipt(submission),
1661
+ prepare: (submission, messageId) => this.#pendingInputs.prepare(submission, messageId),
1662
+ acknowledge: submission => { this.#pendingInputs.finish(submission); if (this.#tty && !this.#disposed)
1663
+ this.#render(); },
1664
+ reconcile: (sessionId, accepted) => this.#pendingInputs.reconcile(sessionId, accepted),
1441
1665
  list: () => {
1442
1666
  if (this.#disposed)
1443
1667
  return [];
1444
1668
  if (this.#queueEditNewer !== null || this.#queueEditPending)
1445
1669
  throw new Error('Finish editing the queued follow-up with Enter before opening Queue.');
1446
1670
  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 };
1671
+ const receipt = this.#pendingInputs.identify(submission, this.#pendingScope());
1672
+ return { id: receipt.id, submission, ...(receipt.recovered ? { recovered: true } : {}) };
1453
1673
  });
1454
1674
  },
1455
1675
  update: (id, change) => {
1456
1676
  if (this.#disposed)
1457
1677
  return false;
1458
- const index = this.#queuedSubmissions.findIndex(item => this.#queuedInputIds.get(item) === id);
1678
+ const index = this.#queuedSubmissions.findIndex(item => this.#pendingInputs.receipt(item)?.id === id);
1459
1679
  if (index < 0)
1460
1680
  return false;
1461
- if (change.kind === 'remove')
1681
+ if (change.kind === 'release') {
1682
+ const submission = this.#queuedSubmissions[index];
1683
+ this.#queuedSubmissions.splice(index, 1);
1684
+ this.#dispatchInput(submission);
1685
+ }
1686
+ else if (change.kind === 'remove')
1462
1687
  this.#queuedSubmissions.splice(index, 1);
1463
1688
  else if (change.kind === 'replace') {
1464
- this.#queuedInputIds.set(change.submission, id);
1689
+ const old = this.#pendingInputs.receipt(this.#queuedSubmissions[index]);
1690
+ const receipt = this.#pendingInputs.identify(change.submission, this.#pendingScope());
1691
+ receipt.id = id;
1692
+ receipt.recovered = old.recovered ?? false;
1465
1693
  this.#queuedSubmissions[index] = change.submission;
1466
1694
  }
1467
1695
  else {
@@ -1477,6 +1705,46 @@ export class LocalTui {
1477
1705
  return true;
1478
1706
  },
1479
1707
  };
1708
+ setContinueHandler(handler) {
1709
+ this.#continueHandler = handler;
1710
+ return () => { if (this.#continueHandler === handler)
1711
+ this.#continueHandler = undefined; };
1712
+ }
1713
+ #canContinue() {
1714
+ if (this.#continueHandler === undefined || this.#continuing || this.#disposed || this.#sessionId === undefined
1715
+ || this.#sessionControls?.continuation === undefined || this.#inspected !== undefined || this.#state.status !== 'idle'
1716
+ || this.#prompt !== null || this.#hasComposerContent() || this.#pasteInFlight > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
1717
+ return false;
1718
+ const pending = this.#pendingInputs.session(this.#sessionId);
1719
+ return pending.queued.length === 0 && pending.sending.size === 0
1720
+ && this.#state.nextTurnInbox.length === 0 && !this.#state.nextStepInbox.some(message => message.source.kind === 'user');
1721
+ }
1722
+ setGoalHandler(handler) {
1723
+ this.#goalHandler = handler;
1724
+ this.#syncSettingsActions();
1725
+ return () => { if (this.#goalHandler === handler) {
1726
+ this.#goalAbort?.abort();
1727
+ this.#goalHandler = undefined;
1728
+ this.#syncSettingsActions();
1729
+ } };
1730
+ }
1731
+ #openGoal() {
1732
+ if (this.#inspected !== undefined)
1733
+ return;
1734
+ if (this.#goalHandler === undefined) {
1735
+ this.notice('Goal controls are unavailable in this profile.');
1736
+ return;
1737
+ }
1738
+ if (this.#goalAbort !== undefined)
1739
+ return;
1740
+ const abort = new AbortController();
1741
+ this.#goalAbort = abort;
1742
+ void this.#goalHandler(abort.signal).catch(error => {
1743
+ if (!abort.signal.aborted && !this.#disposed)
1744
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1745
+ }).finally(() => { if (this.#goalAbort === abort)
1746
+ this.#goalAbort = undefined; });
1747
+ }
1480
1748
  setQueueHandler(handler) {
1481
1749
  this.#queueHandler = handler;
1482
1750
  return () => { if (this.#queueHandler === handler) {
@@ -1496,6 +1764,10 @@ export class LocalTui {
1496
1764
  this.#inspects.add(listener);
1497
1765
  return () => { this.#inspects.delete(listener); };
1498
1766
  }
1767
+ onInspectWorkflow(listener) {
1768
+ this.#workflowInspects.add(listener);
1769
+ return () => { this.#workflowInspects.delete(listener); };
1770
+ }
1499
1771
  onInspectClose(listener) {
1500
1772
  this.#inspectCloses.add(listener);
1501
1773
  return () => { this.#inspectCloses.delete(listener); };
@@ -1504,11 +1776,6 @@ export class LocalTui {
1504
1776
  this.#inspectSubmits.add(listener);
1505
1777
  return () => { this.#inspectSubmits.delete(listener); };
1506
1778
  }
1507
- setSteerHandler(handler) {
1508
- this.#steerHandler = handler;
1509
- return () => { if (this.#steerHandler === handler)
1510
- this.#steerHandler = undefined; };
1511
- }
1512
1779
  /**
1513
1780
  * Last-resort restore for a process that exits without disposing: leave the
1514
1781
  * alternate screen, give every mode back and show the cursor. Writes to a TTY
@@ -1529,10 +1796,11 @@ export class LocalTui {
1529
1796
  return this.#disposeTask ?? this.#herdr.release();
1530
1797
  this.#flushPasteBurst(true);
1531
1798
  this.#flushDraft();
1799
+ this.#pendingInputs.flush(this.#pendingScope());
1532
1800
  this.#disposed = true;
1533
- this.#steerHandler = undefined;
1534
1801
  this.#imagePreviewAbort?.abort();
1535
1802
  this.#fileReviewAbort?.abort();
1803
+ this.#closeFileBrowser();
1536
1804
  this.#asideAbort?.abort();
1537
1805
  for (const directory of this.#imageTemporary) {
1538
1806
  try {
@@ -1541,8 +1809,13 @@ export class LocalTui {
1541
1809
  catch { /* OS may still hold the opened file. */ }
1542
1810
  }
1543
1811
  this.#imageTemporary.clear();
1812
+ this.#goalAbort?.abort();
1813
+ this.#goalAbort = undefined;
1814
+ this.#goalHandler = undefined;
1544
1815
  this.#queueHotkeyAbort?.abort();
1545
1816
  this.#queueHandler = undefined;
1817
+ this.#continueHandler = undefined;
1818
+ this.#cancelQueueRecall();
1546
1819
  if (this.#notificationTimer !== undefined)
1547
1820
  clearTimeout(this.#notificationTimer);
1548
1821
  this.#notificationQueue.clear();
@@ -1590,7 +1863,7 @@ export class LocalTui {
1590
1863
  this.#session.dispose();
1591
1864
  this.#term.output.write('\x1b[?25h\r\n');
1592
1865
  if (this.#resumeHintRequested && this.#sessionId !== undefined) {
1593
- this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
1866
+ this.#term.output.write(`\r\n${uiText(resolveUiLanguage(this.#uiLanguage), 'Resume this session with {command}', { command: `${APP_NAME} --resume ${this.#sessionId}` })}\r\n`);
1594
1867
  }
1595
1868
  // A tty stdin keeps the event loop alive after the tree disposes;
1596
1869
  // release the descriptor so natural completion can exit the process.
@@ -1638,10 +1911,10 @@ export class LocalTui {
1638
1911
  /** Countdown shown on the working row while a retry or a compaction is pending. */
1639
1912
  #workingLabel() {
1640
1913
  if (this.#retryDeadline !== undefined) {
1641
- return `Retrying in ${Math.max(0, Math.ceil((this.#retryDeadline - Date.now()) / 1000))}s`;
1914
+ return uiText(resolveUiLanguage(this.#uiLanguage), 'Retrying in {seconds}s', { seconds: Math.max(0, Math.ceil((this.#retryDeadline - Date.now()) / 1000)) });
1642
1915
  }
1643
1916
  if (this.#state.status === 'compacting' && this.#compactionStartedAt !== undefined) {
1644
- return `Compacting ${Math.max(0, Math.floor((Date.now() - this.#compactionStartedAt) / 1000))}s`;
1917
+ return uiText(resolveUiLanguage(this.#uiLanguage), 'Compacting {seconds}s', { seconds: Math.max(0, Math.floor((Date.now() - this.#compactionStartedAt) / 1000)) });
1645
1918
  }
1646
1919
  return undefined;
1647
1920
  }
@@ -1762,7 +2035,13 @@ export class LocalTui {
1762
2035
  clearTimeout(this.#draftTimer);
1763
2036
  this.#draftTimer = undefined;
1764
2037
  const id = this.#draftScope;
1765
- if (id === undefined || this.#draftStore === undefined || !this.#draftDirty && !this.#unsavedDrafts.has(id))
2038
+ if (id === undefined)
2039
+ return;
2040
+ if (this.#draftStore === undefined) {
2041
+ this.#unsavedDrafts.set(id, this.#captureDraft());
2042
+ return;
2043
+ }
2044
+ if (!this.#draftDirty && !this.#unsavedDrafts.has(id))
1766
2045
  return;
1767
2046
  const draft = this.#captureDraft();
1768
2047
  this.#draftDirty = false;
@@ -1784,7 +2063,7 @@ export class LocalTui {
1784
2063
  }
1785
2064
  #clearDraft() {
1786
2065
  this.#historyDraft = undefined;
1787
- if (this.#draftStore === undefined || this.#draftScope === undefined)
2066
+ if (this.#draftScope === undefined)
1788
2067
  return;
1789
2068
  // A sent or explicitly discarded input must not return after a quick exit.
1790
2069
  this.#draftFingerprint = this.#draftKey();
@@ -1792,12 +2071,20 @@ export class LocalTui {
1792
2071
  this.#flushDraft();
1793
2072
  }
1794
2073
  #switchDraft(id) {
1795
- if (this.#draftStore === undefined || id === this.#draftScope)
2074
+ if (id === this.#draftScope)
1796
2075
  return;
2076
+ this.#goalAbort?.abort();
2077
+ this.#goalAbort = undefined;
2078
+ this.#queueHotkeyAbort?.abort();
2079
+ this.#queueHotkeyAbort = undefined;
2080
+ this.#cancelQueueRecall();
1797
2081
  this.#flushPasteBurst(true);
1798
2082
  this.#finishPrompt(null);
1799
2083
  this.#flushDraft();
2084
+ this.#pendingInputs.flush(this.#pendingScope());
1800
2085
  const initialInput = this.#draftScope === undefined && this.#editor.text !== '' ? this.#captureDraft() : undefined;
2086
+ if (this.#draftScope === undefined && id !== undefined)
2087
+ this.#pendingInputs.bindInitial(id);
1801
2088
  this.#draftScope = id;
1802
2089
  this.#historyIndex = 0;
1803
2090
  this.#historyDraft = undefined;
@@ -1810,7 +2097,7 @@ export class LocalTui {
1810
2097
  let restoreError;
1811
2098
  if (id !== undefined) {
1812
2099
  try {
1813
- const draft = this.#unsavedDrafts.get(id) ?? this.#draftStore.load(id);
2100
+ const draft = this.#unsavedDrafts.get(id) ?? this.#draftStore?.load(id);
1814
2101
  if (draft !== undefined)
1815
2102
  this.#restoreDraft(draft);
1816
2103
  }
@@ -1826,6 +2113,7 @@ export class LocalTui {
1826
2113
  this.#reportDraftError('restore', restoreError);
1827
2114
  }
1828
2115
  #render() {
2116
+ this.#pendingInputs.flush(this.#pendingScope());
1829
2117
  this.#watchDraft();
1830
2118
  if (this.#streamRenderTimer !== null) {
1831
2119
  clearTimeout(this.#streamRenderTimer);
@@ -1848,16 +2136,16 @@ export class LocalTui {
1848
2136
  imageCellSize: this.#imageCellSize,
1849
2137
  mathMode: this.#mathMode,
1850
2138
  mermaidMode: this.#mermaidMode,
1851
- hideThinking: this.#hideThinking,
2139
+ hideThinking: true,
1852
2140
  height: this.#term.height(),
1853
2141
  model: this.#model,
1854
2142
  ...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
1855
2143
  input: this.#editor.text,
1856
2144
  inputCursor: this.#editor.cursor,
1857
2145
  inputImages: this.#images.count,
1858
- queuedSubmissions: this.#queueEditNewer === null
1859
- ? this.#queuedSubmissions
1860
- : [...this.#queuedSubmissions, ...this.#queueEditNewer],
2146
+ canContinue: this.#canContinue(),
2147
+ queuedSubmissionScope: this.#pendingScope(),
2148
+ queuedSubmissions: [...this.#queuedSubmissions, ...(this.#queueEditNewer ?? [])].map(submission => this.#pendingInputs.receipt(submission)?.recovered ? { ...submission, recovered: true } : submission),
1861
2149
  colors: this.#colors,
1862
2150
  pwd: this.#pwd,
1863
2151
  ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
@@ -1868,12 +2156,16 @@ export class LocalTui {
1868
2156
  spinnerFrame: this.#spinner,
1869
2157
  motion: this.#motion,
1870
2158
  colorDepth: this.#colorDepth,
2159
+ compactLinks: this.#compactLinks,
2160
+ language: resolveUiLanguage(this.#uiLanguage),
1871
2161
  themeName: this.#themeName,
1872
2162
  terminal: this.#terminalColors,
1873
2163
  ...(workingLabel === undefined ? {} : { workingLabel }),
1874
- ...(this.#transientStatus === undefined ? {} : { transientStatus: this.#transientStatus }),
2164
+ ...(this.#transientStatus !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), this.#transientStatus) }
2165
+ : this.#selection.active ? { selectionActions: this.#canQuoteSelection() ? 'copy-quote' : 'copy' }
2166
+ : 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
2167
  scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
1876
- ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start' }),
2168
+ ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start', ...(this.#focusBlockOffset === undefined ? {} : { focusBlockOffset: this.#focusBlockOffset }) }),
1877
2169
  ...(this.#transcriptSearch === null ? {} : {
1878
2170
  transcriptSearch: {
1879
2171
  query: this.#transcriptSearch.query,
@@ -1884,9 +2176,14 @@ export class LocalTui {
1884
2176
  toolsExpanded: this.#toolsExpanded,
1885
2177
  expandedOutputs: this.#expandedOutputs,
1886
2178
  expandedTools: this.#detailedToolIds(),
2179
+ rawTools: this.#detailedToolIds(false),
2180
+ expandedReasoning: this.#visibleReasoning(),
2181
+ ...(this.#processSelection === undefined ? {} : { selectedProcess: this.#processSelection }),
1887
2182
  toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
1888
2183
  turnDetailsKey: keysForAction(this.#keybindings, 'toggle-tools'),
1889
- ...(this.#steerHandler === undefined ? {} : { steerKey: keysForAction(this.#keybindings, 'steer-turn') }),
2184
+ busyEnter: this.#queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#busyEnter,
2185
+ steerKey: keysForAction(this.#keybindings, 'steer-turn'),
2186
+ queueKey: keysForAction(this.#keybindings, 'follow-up'),
1890
2187
  openedGroups: this.#openedGroups,
1891
2188
  commands: this.#commands(),
1892
2189
  modelReady: this.#modelReady,
@@ -1897,6 +2194,7 @@ export class LocalTui {
1897
2194
  ...(this.#subagentSelection === undefined ? {} : { subagentSelection: this.#subagentSelection }),
1898
2195
  ...(this.#inspected === undefined ? {} : { inspected: this.#inspected }),
1899
2196
  ...(this.#trajectory === null ? {} : { trajectory: this.#trajectory }),
2197
+ ...(this.#fileBrowser === null ? {} : { fileBrowser: this.#fileBrowser.state }),
1900
2198
  ...(this.#aside === undefined ? {} : { aside: this.#aside }),
1901
2199
  statusBar: this.#statusBar,
1902
2200
  ...(this.#prompt === null ? {} : { promptSelector: this.#prompt }),
@@ -1909,6 +2207,9 @@ export class LocalTui {
1909
2207
  : { lines: [] };
1910
2208
  this.#focusBlock = undefined;
1911
2209
  this.#focusBlockEdge = undefined;
2210
+ this.#focusBlockOffset = undefined;
2211
+ this.#promptActions = frame.promptActions ?? [];
2212
+ this.#promptChoices = frame.promptChoices ?? [];
1912
2213
  this.#promptDocument = frame.promptDocument;
1913
2214
  if (this.#prompt !== null && this.#prompt.documentScroll !== Number.POSITIVE_INFINITY && frame.promptDocument?.position !== undefined) {
1914
2215
  this.#prompt = { ...this.#prompt, documentAnchor: frame.promptDocument.position };
@@ -1916,8 +2217,8 @@ export class LocalTui {
1916
2217
  this.#syncScroll(frame.transcript);
1917
2218
  this.#jumpToLatest = frame.jumpToLatest;
1918
2219
  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));
2220
+ this.#setMouseTracking(this.#fileBrowser !== null || this.#trajectory !== null || this.#documentMouseEnabled() || this.#promptChoices.length > 0 || this.#promptActions.length > 0 || (frame.transcript !== undefined && frame.transientSurface !== 'overlay'
2221
+ && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0 || (frame.transcript.disclosures?.length ?? 0) > 0)));
1921
2222
  // A run opening or folding reshapes rows above the screen in place; the
1922
2223
  // renderer's frozen boundary is a row index and has to be told, or a fold
1923
2224
  // blanks the screen and a large one replays the transcript into history.
@@ -1996,14 +2297,38 @@ export class LocalTui {
1996
2297
  this.#toolsExpanded = false;
1997
2298
  this.#openedGroups.clear();
1998
2299
  this.#detailedTurns.clear();
2300
+ this.#expandedOutputs.clear();
2301
+ this.#expandedToolRows.clear();
2302
+ this.#toolReadingPositions.clear();
2303
+ this.#expandedReasoning.clear();
2304
+ this.#closedReasoning.clear();
2305
+ this.#processSelection = undefined;
2306
+ }
2307
+ #documentMouseEnabled() {
2308
+ const prompt = this.#prompt;
2309
+ return prompt !== null && prompt.feedback !== true && this.#promptDocument !== undefined
2310
+ && (prompt.request.presentation === 'document' || prompt.request.presentation === 'plan-review');
2311
+ }
2312
+ #scrollPromptDocument(row) {
2313
+ if (this.#prompt === null)
2314
+ return;
2315
+ this.#prompt = {
2316
+ ...this.#prompt,
2317
+ documentAnchor: undefined,
2318
+ documentTarget: undefined,
2319
+ documentScroll: row === Number.POSITIVE_INFINITY ? row : Math.max(0, Math.min(row, this.#promptDocument?.maxStart ?? 0)),
2320
+ };
2321
+ this.#render();
1999
2322
  }
2000
2323
  /** Capture transcript navigation; overlays and disposal restore terminal ownership. */
2001
2324
  #setMouseTracking(enabled) {
2002
2325
  enabled = enabled && this.#mouseInteraction !== 'native'
2003
2326
  && (this.#mouseInteraction === 'tui' || this.#tmuxMouseEnabled !== false);
2004
2327
  if (!enabled) {
2328
+ this.#mouseActionPress = undefined;
2005
2329
  this.#selection.clear();
2006
2330
  this.#jumpPress = undefined;
2331
+ this.#disclosurePress = undefined;
2007
2332
  }
2008
2333
  if (!this.#tty || enabled === this.#mouseTracking)
2009
2334
  return;
@@ -2035,7 +2360,7 @@ export class LocalTui {
2035
2360
  * or go back to the live tail once nothing is open any more.
2036
2361
  */
2037
2362
  #inspect(target) {
2038
- const open = this.#toolsExpanded || this.#openedGroups.size > 0;
2363
+ const open = this.#toolsExpanded || this.#openedGroups.size > 0 || this.#expandedOutputs.size > 0;
2039
2364
  if (!open) {
2040
2365
  this.#inspecting = false;
2041
2366
  this.#followTail();
@@ -2079,9 +2404,9 @@ export class LocalTui {
2079
2404
  }
2080
2405
  }
2081
2406
  #routeInputBatch(events) {
2082
- if (this.#pasteProtection && this.#state.status !== 'compacting' && !this.#paste && this.#pasteInFlight === 0
2407
+ if (this.#pasteProtection && !this.#paste && this.#pasteInFlight === 0
2083
2408
  && this.#prompt === null && this.#settings === null
2084
- && this.#search === null && this.#trajectory === null && this.#transcriptSearch === null
2409
+ && this.#search === null && this.#trajectory === null && this.#fileBrowser === null && this.#transcriptSearch === null
2085
2410
  && events.every(event => event.type === 'text' || (event.type === 'key' && ['enter', 'ctrl+j', 'tab'].includes(event.id)))) {
2086
2411
  if (this.#pasteBurstTimer !== undefined)
2087
2412
  clearTimeout(this.#pasteBurstTimer);
@@ -2104,8 +2429,8 @@ export class LocalTui {
2104
2429
  this.#dispatch(event);
2105
2430
  return;
2106
2431
  }
2107
- const composer = this.#state.status !== 'compacting' && this.#prompt === null && this.#settings === null
2108
- && this.#search === null && this.#trajectory === null && this.#transcriptSearch === null;
2432
+ const composer = this.#prompt === null && this.#settings === null
2433
+ && this.#search === null && this.#trajectory === null && this.#fileBrowser === null && this.#transcriptSearch === null;
2109
2434
  if (this.#paste || this.#pasteInFlight > 0 || !this.#pasteProtection || !composer
2110
2435
  || event.type === 'paste-start' || event.type === 'paste-end') {
2111
2436
  this.#flushPasteBurst(true);
@@ -2156,6 +2481,10 @@ export class LocalTui {
2156
2481
  });
2157
2482
  }
2158
2483
  async #acceptPastedText(text) {
2484
+ if (this.#fileBrowser !== null) {
2485
+ this.#applyFileBrowserInput({ type: 'text', value: text });
2486
+ return;
2487
+ }
2159
2488
  if (this.#prompt !== null) {
2160
2489
  const prompt = this.#prompt;
2161
2490
  if (prompt.documentInput !== undefined) {
@@ -2254,14 +2583,198 @@ export class LocalTui {
2254
2583
  if (this.#tty)
2255
2584
  this.#render();
2256
2585
  }
2586
+ /** Resolve only current painted rows, never reinterpret a frozen native-history snapshot. */
2587
+ #disclosureAt(row, column, action = false) {
2588
+ const frame = this.#fileActionFrame;
2589
+ const document = this.#renderer.documentRowAt(row);
2590
+ const source = frame?.documentRows;
2591
+ if (document === undefined || source == null || document < source.documentStart || document >= source.documentEnd)
2592
+ return;
2593
+ const target = frame?.transcript?.disclosures?.find(item => item.row === document && (action
2594
+ ? item.actionColumn !== undefined && column >= item.actionColumn && column < item.actionColumn + (item.key.startsWith('output:') ? 6 : 5)
2595
+ : column >= item.column && column < item.column + item.width));
2596
+ const expected = frame?.lines[source.frameStart + document - source.documentStart];
2597
+ const actual = this.#renderer.viewport()[row];
2598
+ if (target === undefined || expected === undefined || actual === undefined
2599
+ || stripAnsi(sanitizeDisplayLine(expected)).trimEnd() !== stripAnsi(actual).trimEnd())
2600
+ return;
2601
+ return target;
2602
+ }
2603
+ /** Preview text and spinner frames may change while a click stays on the same item. */
2604
+ #pressedDisclosureAt(row, column) {
2605
+ const pressed = this.#disclosurePress;
2606
+ if (pressed === undefined || pressed.row !== row)
2607
+ return;
2608
+ if (pressed.target.key.startsWith('output:') && Math.abs(pressed.column - column) > 1)
2609
+ return;
2610
+ const target = this.#disclosureAt(row, column);
2611
+ if (target?.key === pressed.target.key && target.row === pressed.target.row
2612
+ && target.column === pressed.target.column && target.width === pressed.target.width)
2613
+ return target;
2614
+ }
2615
+ #activateDisclosure(target) {
2616
+ const output = this.#state.blocks[target.block];
2617
+ if (output?.kind === 'commandOutput' && target.key === `output:${target.block}`) {
2618
+ if (this.#expandedOutputs.has(output))
2619
+ this.#expandedOutputs.delete(output);
2620
+ else
2621
+ this.#expandedOutputs.add(output);
2622
+ this.#inspect(target.block);
2623
+ this.#focusBlock = target.block;
2624
+ this.#focusBlockEdge = 'start';
2625
+ this.#render();
2626
+ return;
2627
+ }
2628
+ const groups = turnGroups(this.#state);
2629
+ const group = groups.find(item => target.group === undefined
2630
+ ? target.block >= item.start && target.block < item.until : item.key === target.group);
2631
+ if (group === undefined)
2632
+ return;
2633
+ const block = this.#state.blocks[target.block];
2634
+ if (target.group === undefined && target.key !== (block?.kind === 'tool' ? `call:${block.callId}`
2635
+ : block?.kind === 'assistant' ? `thought:${reasoningKey(block)}` : undefined))
2636
+ return;
2637
+ const scope = this.#detailScope(group);
2638
+ for (const member of groups)
2639
+ if (this.#detailScope(member) === scope)
2640
+ this.#openedGroups.add(member.key);
2641
+ this.#processSelection = this.#processRows().includes(target.block) ? target.block : this.#processRows()[0];
2642
+ this.#ac = null;
2643
+ this.#inspect(target.block);
2644
+ if (target.group === undefined)
2645
+ this.#toggleProcessItem(target.block, 'enter');
2646
+ else {
2647
+ this.#focusBlock = target.block;
2648
+ this.#focusBlockEdge = 'start';
2649
+ this.#render();
2650
+ }
2651
+ }
2652
+ #mouseActionAt(row, column) {
2653
+ const frame = this.#fileActionFrame;
2654
+ const viewport = this.#renderer.viewport();
2655
+ for (const target of frame?.selectionActions ?? []) {
2656
+ const physical = viewport.length - (frame.lines.length - target.row);
2657
+ if (row === physical && column >= target.column && column < target.column + target.width
2658
+ && this.#selection.active && this.#selection.valid(viewport)
2659
+ && stripAnsi(viewport[row] ?? '').trimEnd() === stripAnsi(frame.lines[target.row] ?? '').trimEnd()) {
2660
+ return { ...target, row: physical, key: target.action };
2661
+ }
2662
+ }
2663
+ const tool = this.#disclosureAt(row, column, true);
2664
+ if (tool?.actionColumn !== undefined)
2665
+ return { row, column: tool.actionColumn, width: tool.key.startsWith('output:') ? 6 : 5, key: tool.key, action: 'tool', block: tool.block };
2666
+ return undefined;
2667
+ }
2257
2668
  #handleMouse(event) {
2258
2669
  if (!this.#mouseTracking || this.#paste || this.#pasteInFlight > 0)
2259
2670
  return;
2671
+ if (this.#trajectory !== null || this.#fileBrowser !== null) {
2672
+ this.#handleTrajectoryMouse(event);
2673
+ return;
2674
+ }
2675
+ const promptAction = this.#promptActions.find(action => event.row === action.row && event.column >= action.column && event.column < action.column + action.width);
2676
+ if (this.#promptActionPress !== undefined) {
2677
+ const pressed = this.#promptActionPress;
2678
+ if (event.action === 'release') {
2679
+ this.#promptActionPress = undefined;
2680
+ this.#promptChoicePress = undefined;
2681
+ if (event.button === 'left' && !event.shift && promptAction?.index === pressed.index && promptAction.key === pressed.key
2682
+ && promptAction.row === pressed.row && promptAction.column === pressed.column && this.#prompt !== null) {
2683
+ if (pressed.key === undefined) {
2684
+ this.#prompt = { ...this.#prompt, selected: pressed.index, documentInput: undefined };
2685
+ this.#handlePlanReview({ type: 'key', id: 'enter' }, this.#prompt);
2686
+ }
2687
+ else if (pressed.selectedValue === (filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected]?.value
2688
+ ?? filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected]?.label)) {
2689
+ this.#handlePrompt({ type: 'key', id: pressed.key });
2690
+ }
2691
+ }
2692
+ }
2693
+ else if (event.action === 'drag' && (promptAction?.index !== pressed.index || promptAction.key !== pressed.key
2694
+ || promptAction.row !== pressed.row || promptAction.column !== pressed.column || event.shift)) {
2695
+ this.#promptActionPress = { ...pressed, index: -2 };
2696
+ }
2697
+ return;
2698
+ }
2699
+ if (event.button === 'left' && event.action === 'press' && !event.shift && promptAction !== undefined) {
2700
+ this.#selection.clear();
2701
+ const selected = this.#prompt === null ? undefined : filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed)[this.#prompt.selected];
2702
+ this.#promptActionPress = { ...promptAction, selectedValue: selected?.value ?? selected?.label };
2703
+ return;
2704
+ }
2705
+ if (this.#promptChoices.length > 0 && event.button === 'left') {
2706
+ const choice = this.#promptChoices.find(item => item.row === event.row && event.column >= item.column && event.column < item.column + item.width);
2707
+ if (event.action === 'press') {
2708
+ const prompt = this.#prompt;
2709
+ if (prompt?.request.wait !== undefined && prompt.waitHeld !== true) {
2710
+ prompt.request.wait.hold();
2711
+ prompt.offTick?.();
2712
+ this.#prompt = { ...prompt, waitHeld: true };
2713
+ }
2714
+ this.#promptChoicePress = choice === undefined ? undefined : { value: choice.value, row: choice.row, dragged: event.shift };
2715
+ this.#selection.begin(this.#renderer.viewport(), event, Date.now(), event.shift);
2716
+ }
2717
+ else if (event.action === 'drag') {
2718
+ if (this.#promptChoicePress !== undefined)
2719
+ this.#promptChoicePress.dragged = true;
2720
+ this.#selection.move(event);
2721
+ }
2722
+ else {
2723
+ const pressed = this.#promptChoicePress;
2724
+ this.#promptChoicePress = undefined;
2725
+ const text = this.#selection.release(event);
2726
+ if (text !== undefined) {
2727
+ if (this.#copyOnSelect)
2728
+ void this.#copySelection();
2729
+ }
2730
+ else if (pressed !== undefined && !pressed.dragged && !event.shift && choice?.value === pressed.value && choice.row === pressed.row && this.#prompt !== null) {
2731
+ this.#prompt = { ...this.#prompt, selected: choice.index, previewScroll: 0 };
2732
+ if (this.#prompt.request.multiSelect === true && filteredPromptOptions(this.#prompt.request, this.#editor.text)[choice.index]?.disabled !== true)
2733
+ this.#prompt = togglePromptSelection(this.#prompt, this.#editor.text);
2734
+ }
2735
+ }
2736
+ this.#render();
2737
+ return;
2738
+ }
2739
+ const pressedAction = this.#mouseActionPress;
2740
+ if (pressedAction !== undefined) {
2741
+ const target = this.#mouseActionAt(event.row, event.column);
2742
+ if (event.action === 'release') {
2743
+ this.#mouseActionPress = undefined;
2744
+ if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
2745
+ if (target.action === 'copy')
2746
+ void this.#copySelection(true);
2747
+ else if (target.action === 'quote')
2748
+ this.#quoteSelection();
2749
+ else {
2750
+ const block = this.#state.blocks[target.block];
2751
+ if (block?.kind === 'tool')
2752
+ void this.#openToolActions(block);
2753
+ else if (block?.kind === 'commandOutput')
2754
+ void this.#readCommandOutput(block);
2755
+ }
2756
+ }
2757
+ }
2758
+ else if (event.action === 'drag' && (target?.key !== pressedAction.key || event.shift)) {
2759
+ // Keep consuming the gesture, but never activate after leaving the button.
2760
+ this.#mouseActionPress = { ...pressedAction, key: '' };
2761
+ }
2762
+ return;
2763
+ }
2764
+ if (event.button === 'left' && event.action === 'press' && !event.shift) {
2765
+ const target = this.#mouseActionAt(event.row, event.column);
2766
+ if (target !== undefined) {
2767
+ this.#mouseActionPress = target;
2768
+ this.#disclosurePress = undefined;
2769
+ return;
2770
+ }
2771
+ }
2260
2772
  const inside = (target) => event.row === target.row && event.column >= target.column && event.column < target.column + target.width;
2261
2773
  if (event.button === 'right') {
2774
+ this.#disclosurePress = undefined;
2262
2775
  if (event.action === 'press') {
2263
2776
  if (this.#selection.active)
2264
- void this.#copySelection();
2777
+ void this.#copySelection(true);
2265
2778
  else {
2266
2779
  const document = this.#renderer.documentRowAt(event.row);
2267
2780
  const frame = this.#fileActionFrame;
@@ -2283,6 +2796,8 @@ export class LocalTui {
2283
2796
  return;
2284
2797
  }
2285
2798
  if (event.action === 'press') {
2799
+ const disclosure = event.shift ? undefined : this.#disclosureAt(event.row, event.column);
2800
+ this.#disclosurePress = disclosure === undefined ? undefined : { target: disclosure, row: event.row, column: event.column };
2286
2801
  const target = this.#jumpToLatest;
2287
2802
  this.#jumpPress = target !== undefined && inside(target) ? target : undefined;
2288
2803
  if (this.#jumpPress !== undefined)
@@ -2301,15 +2816,93 @@ export class LocalTui {
2301
2816
  this.#followTail();
2302
2817
  }
2303
2818
  }
2304
- else if (event.action === 'drag')
2305
- this.#selection.move(event);
2819
+ else if (event.action === 'drag') {
2820
+ // Motion confined to the empty gutter is click jitter. Once it leaves,
2821
+ // selection owns the gesture even if the pointer returns to the arrow.
2822
+ if (event.shift || this.#pressedDisclosureAt(event.row, event.column) === undefined) {
2823
+ this.#disclosurePress = undefined;
2824
+ this.#selection.move(event);
2825
+ }
2826
+ }
2306
2827
  else {
2828
+ const target = this.#pressedDisclosureAt(event.row, event.column);
2829
+ this.#disclosurePress = undefined;
2830
+ if (!event.shift && target !== undefined) {
2831
+ this.#selection.clear();
2832
+ this.#activateDisclosure(target);
2833
+ return;
2834
+ }
2307
2835
  const text = this.#selection.release(event);
2308
2836
  if (text !== undefined && this.#copyOnSelect)
2309
2837
  void this.#copySelection();
2310
2838
  }
2311
2839
  this.#render();
2312
2840
  }
2841
+ #handleTrajectoryMouse(event) {
2842
+ if (event.button === 'right') {
2843
+ if (event.action === 'press' && this.#selection.active)
2844
+ void this.#copySelection(true);
2845
+ return;
2846
+ }
2847
+ if (event.action === 'press') {
2848
+ this.#trajectoryPress = { row: event.row, column: event.column,
2849
+ text: stripAnsi(this.#renderer.viewport()[event.row] ?? ''), dragged: event.shift };
2850
+ this.#selection.begin(this.#renderer.viewport(), event, Date.now(), event.shift);
2851
+ }
2852
+ else if (event.action === 'drag') {
2853
+ if (this.#trajectoryPress !== undefined)
2854
+ this.#trajectoryPress.dragged = true;
2855
+ this.#selection.move(event);
2856
+ }
2857
+ else {
2858
+ const press = this.#trajectoryPress;
2859
+ this.#trajectoryPress = undefined;
2860
+ const text = this.#selection.release(event);
2861
+ if (text !== undefined) {
2862
+ if (this.#copyOnSelect)
2863
+ void this.#copySelection();
2864
+ }
2865
+ else if (press !== undefined && !press.dragged && !event.shift && press.row === event.row
2866
+ && Math.abs(press.column - event.column) <= 1
2867
+ && stripAnsi(this.#renderer.viewport()[event.row] ?? '') === press.text) {
2868
+ if (this.#fileBrowser !== null)
2869
+ this.#fileBrowser.click(event.row, event.column, this.#term.width(), this.#term.height());
2870
+ else if (this.#trajectory !== null)
2871
+ this.#trajectory = trajectoryClick(this.#trajectory, this.#term.width(), this.#term.height(), event.row, event.column);
2872
+ }
2873
+ }
2874
+ this.#render();
2875
+ }
2876
+ #applyTrajectoryInput(event) {
2877
+ const state = this.#trajectory;
2878
+ if (state === null)
2879
+ return;
2880
+ const { pageSize } = trajectoryListMetrics(state, this.#term.height(), this.#term.width());
2881
+ const { pageLines } = trajectoryDetailMetrics(state, this.#term.height());
2882
+ const theme = createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks);
2883
+ const maxDetailScroll = trajectoryDetailScrollLimit(state, theme, this.#term.width(), this.#term.height());
2884
+ const command = applyTrajectoryEvent(state, event, { pageSize, detailPageLines: pageLines, width: this.#term.width(), maxDetailScroll });
2885
+ if ('close' in command) {
2886
+ this.#trajectory = null;
2887
+ this.#trajectoryPress = undefined;
2888
+ }
2889
+ else if ('copy' in command) {
2890
+ void this.#deliverCopy(command.copy).then(delivery => {
2891
+ if (this.#trajectory === state) {
2892
+ this.#trajectory = { ...state, notice: delivery === 'requested' ? 'Sent to terminal clipboard' : `Copied ${command.label}` };
2893
+ this.#render();
2894
+ }
2895
+ }, () => {
2896
+ if (this.#trajectory === state) {
2897
+ this.#trajectory = { ...state, notice: 'Copy failed' };
2898
+ this.#render();
2899
+ }
2900
+ });
2901
+ }
2902
+ else
2903
+ this.#trajectory = command.state;
2904
+ this.#render();
2905
+ }
2313
2906
  /** Keep older clipboard operations from finishing after a newer user copy. */
2314
2907
  #deliverCopy(text) {
2315
2908
  const task = this.#copyTask.then(() => {
@@ -2320,7 +2913,7 @@ export class LocalTui {
2320
2913
  this.#copyTask = task.catch(() => { });
2321
2914
  return task;
2322
2915
  }
2323
- async #copySelection() {
2916
+ async #copySelection(explicit = false) {
2324
2917
  if (!this.#selection.valid(this.#renderer.viewport())) {
2325
2918
  this.#selection.clear();
2326
2919
  return;
@@ -2329,11 +2922,99 @@ export class LocalTui {
2329
2922
  if (text === undefined)
2330
2923
  return;
2331
2924
  try {
2332
- await this.#deliverCopy(text);
2925
+ const delivery = await this.#deliverCopy(text);
2926
+ if (explicit && !this.#disposed) {
2927
+ this.#showTransientStatus(delivery === 'requested' ? 'Selection sent to terminal clipboard' : 'Copied selection');
2928
+ this.#render();
2929
+ }
2333
2930
  }
2334
2931
  catch {
2335
- if (!this.#disposed)
2336
- this.notice('Could not copy selection to the clipboard.', { level: 'error' });
2932
+ if (!this.#disposed) {
2933
+ this.#showTransientStatus('Copy failed');
2934
+ this.#render();
2935
+ }
2936
+ }
2937
+ }
2938
+ #canQuoteSelection() {
2939
+ return this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null && this.#search === null
2940
+ && this.#transcriptSearch === null && (this.#inspected === undefined || this.#inspected.writable === true);
2941
+ }
2942
+ #quoteSelection() {
2943
+ if (!this.#canQuoteSelection())
2944
+ return;
2945
+ const text = this.#selection.valid(this.#renderer.viewport()) ? this.#selection.text() : undefined;
2946
+ this.#selection.clear();
2947
+ if (text !== undefined) {
2948
+ this.#appendQuote(text.split('\n').map(line => '> ' + line).join('\n'));
2949
+ }
2950
+ this.#render();
2951
+ }
2952
+ #appendQuote(text) {
2953
+ this.#editor.setCursor(this.#editor.text.length);
2954
+ this.#editor.paste((this.#editor.text === '' ? '' : '\n\n') + text + '\n\n');
2955
+ this.#processSelection = undefined;
2956
+ this.#refreshAutocomplete();
2957
+ this.#showTransientStatus('Quoted in composer');
2958
+ this.#render();
2959
+ }
2960
+ /** Explicit process selection wins; scrolling through an open detail follows that tool. */
2961
+ #currentTool() {
2962
+ const detail = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
2963
+ const selected = this.#processSelection;
2964
+ const row = selected === undefined ? undefined : this.#blockStarts[selected];
2965
+ const selectedVisible = row !== undefined && row >= this.#scrollStart && row < this.#scrollStart + this.#scrollBudget - 1;
2966
+ const index = selectedVisible ? selected : detail?.block ?? selected ?? this.#viewportBlock();
2967
+ const block = index === undefined ? undefined : this.#state.blocks[index];
2968
+ return block?.kind === 'tool' ? block : undefined;
2969
+ }
2970
+ async #openToolActions(block = this.#currentTool()) {
2971
+ if (this.#fileReviewAbort !== undefined || this.#search !== null || this.#prompt !== null || this.#settings !== null)
2972
+ return;
2973
+ if (block === undefined) {
2974
+ this.#showTransientStatus(uiText(resolveUiLanguage(this.#uiLanguage), 'No tool selected · use {key}', { key: keysForAction(this.#keybindings, 'toggle-tools') }), 4_000);
2975
+ return;
2976
+ }
2977
+ const command = toolCommand(block);
2978
+ const files = blockFileAction(block);
2979
+ const source = toolSource(block);
2980
+ const options = [
2981
+ ...(command === undefined ? [] : [{ label: 'Copy command', value: 'command', description: command.split('\n')[0] ?? '' }]),
2982
+ ...(block.output.trim() === '' ? [] : [{ label: 'Copy result', value: 'result' }]),
2983
+ ...(files === undefined ? [] : [{ label: 'View file', value: 'files' }]),
2984
+ ...(source === undefined ? [] : [{ label: 'Copy source', value: 'source-copy', description: source.label },
2985
+ ...(this.#canQuoteSelection() ? [{ label: 'Quote source', value: 'source-quote', description: source.label }] : [])]),
2986
+ ];
2987
+ if (options.length === 0) {
2988
+ this.#showTransientStatus('No content available for this tool.', 4_000);
2989
+ return;
2990
+ }
2991
+ const abort = new AbortController();
2992
+ this.#fileReviewAbort = abort;
2993
+ try {
2994
+ const answer = await this.prompt({ localize: ['title'], title: 'Tool actions', question: toolActionContext(block),
2995
+ options: options.map(option => ({ ...option, label: uiText(resolveUiLanguage(this.#uiLanguage), option.label) })), allowCustom: false, notify: false, signal: abort.signal, onSuperseded: () => { abort.abort(); } });
2996
+ if (abort.signal.aborted || this.#disposed)
2997
+ return;
2998
+ if (answer === 'command' && command !== undefined)
2999
+ await this.#copyPicked(command, 'command');
3000
+ else if (answer === 'result')
3001
+ await this.#copyPicked(block.output, 'tool result');
3002
+ else if (answer === 'source-copy' && source !== undefined)
3003
+ await this.#copyPicked(source.text, 'source');
3004
+ else if (answer === 'source-quote' && source !== undefined && this.#canQuoteSelection())
3005
+ this.#appendQuote(sourceQuote(source));
3006
+ else if (answer === 'files' && files !== undefined) {
3007
+ this.#fileReviewAbort = undefined;
3008
+ this.#openFileAction(files);
3009
+ }
3010
+ }
3011
+ catch (error) {
3012
+ if (!abort.signal.aborted && !this.#disposed)
3013
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
3014
+ }
3015
+ finally {
3016
+ if (this.#fileReviewAbort === abort)
3017
+ this.#fileReviewAbort = undefined;
2337
3018
  }
2338
3019
  }
2339
3020
  #openFileAction(action, initial) {
@@ -2382,10 +3063,23 @@ export class LocalTui {
2382
3063
  this.#handleMouse(event);
2383
3064
  return;
2384
3065
  }
3066
+ this.#promptActionPress = undefined;
3067
+ this.#promptChoicePress = undefined;
2385
3068
  this.#jumpPress = undefined;
3069
+ this.#disclosurePress = undefined;
3070
+ this.#mouseActionPress = undefined;
3071
+ if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3072
+ && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3073
+ this.#keyTest.observe(event);
3074
+ return;
3075
+ }
2386
3076
  if (this.#selection.active) {
3077
+ if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection') {
3078
+ this.#quoteSelection();
3079
+ return;
3080
+ }
2387
3081
  if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
2388
- void this.#copySelection();
3082
+ void this.#copySelection(true);
2389
3083
  return;
2390
3084
  }
2391
3085
  this.#selection.clear();
@@ -2396,32 +3090,37 @@ export class LocalTui {
2396
3090
  }
2397
3091
  }
2398
3092
  if (event.type === 'wheel') {
3093
+ if (this.#fileBrowser !== null) {
3094
+ this.#applyFileBrowserInput(event);
3095
+ return;
3096
+ }
3097
+ if (this.#trajectory !== null) {
3098
+ this.#applyTrajectoryInput(event);
3099
+ return;
3100
+ }
2399
3101
  this.#jumpPress = undefined;
2400
3102
  if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
2401
3103
  const step = wheelStep(this.#wheelGesture, Date.now(), event.direction === 'up' ? -1 : 1, !this.#wheelAccelerates);
2402
3104
  this.#wheelGesture = step.gesture;
2403
- if (step.lines !== 0)
2404
- this.#scrollBy(step.lines);
3105
+ if (step.lines !== 0) {
3106
+ if (this.#prompt !== null && this.#prompt.request.presentation !== 'document' && this.#prompt.request.presentation !== 'plan-review') {
3107
+ if (this.#prompt.request.wait !== undefined && this.#prompt.waitHeld !== true) {
3108
+ this.#prompt.request.wait.hold();
3109
+ this.#prompt.offTick?.();
3110
+ this.#prompt = { ...this.#prompt, waitHeld: true };
3111
+ }
3112
+ const count = filteredPromptOptions(this.#prompt.request, this.#editor.text, this.#prompt.collapsed).length;
3113
+ this.#prompt = movePromptSelection(this.#prompt, Math.max(0, Math.min(count - 1, this.#prompt.selected + step.lines)), count);
3114
+ this.#render();
3115
+ }
3116
+ else if (this.#documentMouseEnabled())
3117
+ this.#scrollPromptDocument(this.#promptDocument.start + step.lines);
3118
+ else
3119
+ this.#scrollBy(step.lines);
3120
+ }
2405
3121
  }
2406
3122
  return;
2407
3123
  }
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
3124
  // Clipboard image inspection is asynchronous. Preserve the exact key
2426
3125
  // order so a fast Ctrl+V, Enter submits the finished image draft rather
2427
3126
  // than an empty prompt.
@@ -2449,6 +3148,8 @@ export class LocalTui {
2449
3148
  return;
2450
3149
  }
2451
3150
  if (event.type === 'paste-start') {
3151
+ if (this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null)
3152
+ this.#processSelection = undefined;
2452
3153
  this.#paste = true;
2453
3154
  this.#pasteBuf = '';
2454
3155
  return;
@@ -2466,15 +3167,12 @@ export class LocalTui {
2466
3167
  }
2467
3168
  if (this.#handlePrompt(event))
2468
3169
  return;
3170
+ if (this.#fileBrowser !== null) {
3171
+ this.#applyFileBrowserInput(event);
3172
+ return;
3173
+ }
2469
3174
  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();
3175
+ this.#applyTrajectoryInput(event);
2478
3176
  return;
2479
3177
  }
2480
3178
  if (this.#transcriptSearch !== null) {
@@ -2500,6 +3198,8 @@ export class LocalTui {
2500
3198
  this.#applySettings(event);
2501
3199
  return;
2502
3200
  }
3201
+ if (this.#search === null && this.#handleProcessNavigation(event))
3202
+ return;
2503
3203
  /**
2504
3204
  * The side pane is a companion, not a mode: it answers where the reader is
2505
3205
  * already working, so it must not take keys the composer is using. It reads
@@ -2588,10 +3288,7 @@ export class LocalTui {
2588
3288
  return true;
2589
3289
  }
2590
3290
  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();
3291
+ // Harness retains accepted input; stopping a turn does not withdraw messages.
2595
3292
  for (const listener of this.#interrupts)
2596
3293
  listener();
2597
3294
  return true;
@@ -2629,7 +3326,7 @@ export class LocalTui {
2629
3326
  }
2630
3327
  // A running Shell line owns Ctrl+C before the composer does: the reader is
2631
3328
  // watching a command, and the key they press to stop it is the same one.
2632
- if (this.#shellAbort !== undefined && !this.#hasComposerContent()) {
3329
+ if (this.#inspected === undefined && this.#shellAbort !== undefined && !this.#hasComposerContent()) {
2633
3330
  this.#shellAbort();
2634
3331
  return true;
2635
3332
  }
@@ -2658,7 +3355,6 @@ export class LocalTui {
2658
3355
  // A running turn keeps Ctrl+C as the familiar abort while the composer is
2659
3356
  // empty; Escape already covers it, and refusing the reflex would be worse.
2660
3357
  if (this.#state.status !== 'idle' && !this.#hasComposerContent()) {
2661
- this.#restoreQueuedBeforeAbort();
2662
3358
  for (const listener of this.#interrupts)
2663
3359
  listener();
2664
3360
  return true;
@@ -2666,20 +3362,6 @@ export class LocalTui {
2666
3362
  this.#clearComposerWithUndo();
2667
3363
  return true;
2668
3364
  }
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
3365
  #handlePrompt(event) {
2684
3366
  let prompt = this.#prompt;
2685
3367
  if (prompt === null)
@@ -2727,13 +3409,15 @@ export class LocalTui {
2727
3409
  const selected = filteredPromptOptions(prompt.request, '', prompt.collapsed)[prompt.selected];
2728
3410
  if (action !== undefined && (action.scope === 'list' || selected !== undefined)) {
2729
3411
  if (!disarmed) {
2730
- this.#finishPrompt(action.valuePrefix + (action.scope === 'list' ? '' : selected.value ?? selected.label));
3412
+ this.#finishPrompt(action.valuePrefix + (action.scope === 'list' ? '' : selected.value ?? selected.label), 'action');
2731
3413
  this.#render();
2732
3414
  }
2733
3415
  return true;
2734
3416
  }
2735
3417
  }
2736
- if (event.type === 'text' && event.value === ' ' && prompt.request.multiSelect === true) {
3418
+ const toggleAnswer = prompt.request.answerFormat === 'json' && event.type === 'key' && event.id === 'ctrl+g';
3419
+ const toggleSpace = event.type === 'text' && event.value === ' ' && (prompt.request.answerFormat !== 'json' || this.#editor.text === '');
3420
+ if (prompt.request.multiSelect === true && (toggleAnswer || toggleSpace)) {
2737
3421
  this.#prompt = togglePromptSelection(prompt, this.#editor.text);
2738
3422
  this.#render();
2739
3423
  return true;
@@ -2755,14 +3439,11 @@ export class LocalTui {
2755
3439
  const filtered = filteredPromptOptions(prompt.request, this.#editor.text, prompt.collapsed);
2756
3440
  const count = filtered.length;
2757
3441
  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')) {
3442
+ if (keyAction !== undefined && (keyAction.scope === 'list' || filtered[prompt.selected] !== undefined)) {
2761
3443
  if (!disarmed) {
2762
3444
  const selected = filtered[prompt.selected];
2763
3445
  if (keyAction.scope === 'list' || selected !== undefined) {
2764
- this.#editor.setText('');
2765
- this.#finishPrompt(keyAction.valuePrefix + (keyAction.scope === 'list' ? '' : selected.value ?? selected.label));
3446
+ this.#finishPrompt(keyAction.valuePrefix + (keyAction.scope === 'list' ? '' : selected.value ?? selected.label), 'action');
2766
3447
  this.#render();
2767
3448
  }
2768
3449
  }
@@ -2821,7 +3502,7 @@ export class LocalTui {
2821
3502
  // the other lists about what `tab` or `pageUp` means. `enter` stays below
2822
3503
  // because what it submits depends on the editor and on multi-select.
2823
3504
  const page = prompt.request.presentation === 'tree'
2824
- ? promptTreePageSize(prompt.request, this.#term.width(), this.#term.height()) : 10;
3505
+ ? promptTreePageSize(prompt.request, this.#term.width(), this.#term.height(), resolveUiLanguage(this.#uiLanguage)) : 10;
2825
3506
  const navigation = listNavigation(event);
2826
3507
  if (navigation.kind === 'move' || navigation.kind === 'page') {
2827
3508
  // The tree clamps: its rows are a structure, and wrapping past the last
@@ -2838,11 +3519,10 @@ export class LocalTui {
2838
3519
  }
2839
3520
  if (event.id === 'enter' && (this.#editor.text === '' || prompt.request.multiSelect === true)) {
2840
3521
  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)
3522
+ // Supplementary text can answer a question even with no checked options.
3523
+ if (answer === null && !(prompt.request.answerFormat === 'json' && this.#editor.expandedText.trim()))
2844
3524
  return true;
2845
- this.#finishPrompt(answer);
3525
+ this.#finishPrompt(answer ?? '', 'selection');
2846
3526
  this.#render();
2847
3527
  return true;
2848
3528
  }
@@ -2852,7 +3532,6 @@ export class LocalTui {
2852
3532
  // closing on it would lose both the list and the filter.
2853
3533
  if (answer === null)
2854
3534
  return true;
2855
- this.#editor.setText('');
2856
3535
  this.#finishPrompt(answer);
2857
3536
  this.#render();
2858
3537
  return true;
@@ -2882,7 +3561,7 @@ export class LocalTui {
2882
3561
  }
2883
3562
  return true;
2884
3563
  }
2885
- if (!document && event.type === 'key' && (event.id === 'ctrl+x' || this.#boundAction(event.id) === 'external-editor')) {
3564
+ if (!document && prompt.request.allowCustom !== false && event.type === 'key' && (event.id === 'ctrl+x' || this.#boundAction(event.id) === 'external-editor')) {
2886
3565
  this.#openPlanFeedbackEditor(prompt);
2887
3566
  return true;
2888
3567
  }
@@ -2945,13 +3624,7 @@ export class LocalTui {
2945
3624
  else if (event.id === 'end')
2946
3625
  documentScroll = prompt.request.documentTail === true ? Number.POSITIVE_INFINITY : scroll?.maxStart ?? prompt.documentScroll ?? 0;
2947
3626
  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();
3627
+ this.#scrollPromptDocument(documentScroll);
2955
3628
  return true;
2956
3629
  }
2957
3630
  if (event.id === 'tab' || event.id === 'right') {
@@ -2972,7 +3645,7 @@ export class LocalTui {
2972
3645
  return true;
2973
3646
  const approve = prompt.request.approveValue ?? prompt.request.options?.[0]?.value
2974
3647
  ?? prompt.request.options?.[0]?.label;
2975
- if (document || answer === approve) {
3648
+ if (document || answer === approve || prompt.request.allowCustom === false) {
2976
3649
  this.#finishPrompt(answer);
2977
3650
  }
2978
3651
  else {
@@ -2993,7 +3666,11 @@ export class LocalTui {
2993
3666
  * something up in it.
2994
3667
  */
2995
3668
  #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));
3669
+ const rows = () => {
3670
+ const theme = createTheme(this.#colors, this.#colorDepth, this.#themeName, this.#terminalColors, resolveUiLanguage(this.#uiLanguage), this.#compactLinks);
3671
+ const width = Math.max(1, this.#term.width() - 6);
3672
+ return prompt.renderDocument?.(theme, width) ?? documentRows(prompt.request, theme, width);
3673
+ };
2997
3674
  // Always build on the newest state: typing updates the query while the find
2998
3675
  // is open, and jumping from a stale copy would drop it again.
2999
3676
  const jumpTo = (row) => {
@@ -3055,7 +3732,7 @@ export class LocalTui {
3055
3732
  }
3056
3733
  #handleDocumentReader(event, prompt) {
3057
3734
  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), {
3735
+ 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
3736
  wordWrap: prompt.documentWordWrap ?? true, column: prompt.documentColumn ?? 0,
3060
3737
  });
3061
3738
  const start = this.#promptDocument?.start ?? 0;
@@ -3137,7 +3814,7 @@ export class LocalTui {
3137
3814
  listener(id);
3138
3815
  }
3139
3816
  #handleSubagentLauncher(event) {
3140
- const agents = this.#subagents?.agents ?? [];
3817
+ const agents = rosterItems(this.#subagents);
3141
3818
  if (this.#subagentSelection !== undefined) {
3142
3819
  if (agents.length === 0) {
3143
3820
  this.#subagentSelection = undefined;
@@ -3146,7 +3823,12 @@ export class LocalTui {
3146
3823
  const at = Math.min(this.#subagentSelection, agents.length - 1);
3147
3824
  if (event.type === 'key' && event.id === 'enter') {
3148
3825
  this.#subagentSelection = undefined;
3149
- this.#openSelectedSubagent(agents[at]);
3826
+ const item = agents[at];
3827
+ if (item.kind === 'agent')
3828
+ this.#openSelectedSubagent(item.agent);
3829
+ else
3830
+ for (const listener of this.#workflowInspects)
3831
+ listener(item.run.id);
3150
3832
  return true;
3151
3833
  }
3152
3834
  if (event.type === 'key' && event.id === 'escape') {
@@ -3172,7 +3854,7 @@ export class LocalTui {
3172
3854
  return false;
3173
3855
  if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
3174
3856
  return false;
3175
- this.#subagentSelection = Math.min(agents.length - 1, Math.max(0, subagentPreviewStart(agents)));
3857
+ this.#subagentSelection = Math.min(agents.length - 1, Math.max(0, rosterPreviewStart(this.#subagents)));
3176
3858
  this.#render();
3177
3859
  return true;
3178
3860
  }
@@ -3216,11 +3898,15 @@ export class LocalTui {
3216
3898
  const previous = options.persist ? this.prefs() : undefined;
3217
3899
  const version = this.#prefsVersion;
3218
3900
  const previousMotion = this.#motion;
3901
+ this.#uiLanguage = prefs.uiLanguage ?? 'auto';
3902
+ if (this.#prompt !== null)
3903
+ this.#prompt = { ...this.#prompt, request: localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)) };
3219
3904
  this.#themeName = prefs.theme;
3220
3905
  this.#colors = prefs.colors;
3221
3906
  this.#syncColorDepth();
3222
3907
  this.#motion = prefs.motion ?? 'full';
3223
3908
  this.#flushPasteBurst(true);
3909
+ this.#busyEnter = prefs.busyEnter ?? 'queue';
3224
3910
  this.#pasteProtection = prefs.pasteProtection !== false;
3225
3911
  this.#externalEditor = prefs.editor ?? 'auto';
3226
3912
  this.#terminalTitle = prefs.terminalTitle ?? 'status';
@@ -3229,7 +3915,7 @@ export class LocalTui {
3229
3915
  this.#copyOnSelect = prefs.copyOnSelect ?? true;
3230
3916
  this.#mathMode = prefs.mathMode ?? 'auto';
3231
3917
  this.#mermaidMode = prefs.mermaidMode ?? 'auto';
3232
- this.#hideThinking = prefs.hideThinking === true;
3918
+ this.#hideThinking = prefs.hideThinking !== false;
3233
3919
  this.#mouseInteraction = prefs.mouseInteraction ?? 'auto';
3234
3920
  this.#checkUpdates = prefs.checkUpdates ?? true;
3235
3921
  this.#startupChangelog = prefs.startupChangelog ?? 'summary';
@@ -3334,6 +4020,10 @@ export class LocalTui {
3334
4020
  if (command.kind === 'open-command') {
3335
4021
  if (this.#prefsSaveTask !== undefined)
3336
4022
  return;
4023
+ if (command.name === 'manage-goal') {
4024
+ this.#openGoal();
4025
+ return;
4026
+ }
3337
4027
  if (command.name === 'keybindings') {
3338
4028
  void this.#editKeybindings();
3339
4029
  return;
@@ -3514,7 +4204,7 @@ export class LocalTui {
3514
4204
  return;
3515
4205
  }
3516
4206
  const commands = this.#commands();
3517
- const slashResult = slashSuggestions(this.#editor.text, this.#editor.cursor, commands);
4207
+ const slashResult = slashSuggestions(this.#editor.text, this.#editor.cursor, commands, resolveUiLanguage(this.#uiLanguage));
3518
4208
  if (slashResult !== null) {
3519
4209
  this.#setAutocomplete(slashResult);
3520
4210
  return;
@@ -3653,11 +4343,6 @@ export class LocalTui {
3653
4343
  return;
3654
4344
  }
3655
4345
  if (command.kind === 'submit') {
3656
- if (this.#inspected !== undefined) {
3657
- if (this.#inspected.writable === true)
3658
- this.#submitInspect(command.text);
3659
- return;
3660
- }
3661
4346
  this.#submit(command.text);
3662
4347
  return;
3663
4348
  }
@@ -3713,8 +4398,8 @@ export class LocalTui {
3713
4398
  #toggleThinking() {
3714
4399
  const next = !this.#hideThinking;
3715
4400
  this.#hideThinking = next;
3716
- if (!next)
3717
- this.#expandedReasoning.clear();
4401
+ this.#expandedReasoning.clear();
4402
+ this.#closedReasoning.clear();
3718
4403
  const persist = this.#persistPrefs;
3719
4404
  if (persist !== null)
3720
4405
  void persist(this.prefs(), [{ op: 'set', path: ['hideThinking'], value: next }]);
@@ -3749,7 +4434,22 @@ export class LocalTui {
3749
4434
  this.#refreshAutocomplete();
3750
4435
  this.#render();
3751
4436
  }
4437
+ #cancelQueueRecall() {
4438
+ this.#queueRecall?.abort.abort();
4439
+ this.#queueRecall = undefined;
4440
+ this.#queueEditPending = false;
4441
+ }
4442
+ #requestQueueRecall() {
4443
+ const abort = new AbortController();
4444
+ const request = { signal: abort.signal, ...(this.#draftScope === undefined ? {} : { sessionId: this.#draftScope }) };
4445
+ this.#queueRecall = { abort, request };
4446
+ this.#queueEditPending = true;
4447
+ for (const listener of this.#queueEdits)
4448
+ listener(request);
4449
+ }
3752
4450
  #restoreLatestQueuedSubmission(allowNonEmpty = false) {
4451
+ if (this.#inspected !== undefined && !this.#inspected.writable)
4452
+ return false;
3753
4453
  if (this.#queueEditPending)
3754
4454
  return true;
3755
4455
  if (this.#queueEditNewer !== null) {
@@ -3762,9 +4462,7 @@ export class LocalTui {
3762
4462
  const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
3763
4463
  if (!hasDurableFollowup || this.#queueEdits.size === 0)
3764
4464
  return true;
3765
- this.#queueEditPending = true;
3766
- for (const listener of this.#queueEdits)
3767
- listener();
4465
+ this.#requestQueueRecall();
3768
4466
  return true;
3769
4467
  }
3770
4468
  if (this.#historyIndex !== 0)
@@ -3781,10 +4479,7 @@ export class LocalTui {
3781
4479
  const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
3782
4480
  if (!hasDurableFollowup || this.#queueEdits.size === 0)
3783
4481
  return false;
3784
- this.#queueEditNewer = [];
3785
- this.#queueEditPending = true;
3786
- for (const listener of this.#queueEdits)
3787
- listener();
4482
+ this.#requestQueueRecall();
3788
4483
  return true;
3789
4484
  }
3790
4485
  #historyNext() {
@@ -3826,7 +4521,8 @@ export class LocalTui {
3826
4521
  const images = this.#images.copies();
3827
4522
  const files = this.#files.copies();
3828
4523
  const typedText = this.#files.text(images.length > 0 ? text.trim() : text);
3829
- const segments = images.length === 0 ? [] : splitComposerImageMarkers(typedText, images);
4524
+ const segments = images.length === 0 && files.length === 0 ? []
4525
+ : splitComposerAttachmentMarkers(text.trim(), images, this.#files.snapshot(text).entries.map(item => item.marker));
3830
4526
  // Stripping a marker that sat on its own line leaves that line break behind.
3831
4527
  const submittedText = images.length > 0 ? stripComposerImageMarkers(typedText, images).trim() : typedText;
3832
4528
  const literal = text === this.#editor.expandedText && (this.#literalInput || this.#editor.startsWithPaste);
@@ -3842,19 +4538,48 @@ export class LocalTui {
3842
4538
  historyText,
3843
4539
  };
3844
4540
  }
3845
- #submit(text) {
4541
+ #dispatchInput(submission) {
4542
+ const receipt = this.#pendingInputs.identify(submission, this.#pendingScope());
4543
+ receipt.recovered = false;
4544
+ if (this.#inspected !== undefined) {
4545
+ this.#pendingInputs.take(submission, this.#pendingScope());
4546
+ for (const listener of this.#inspectSubmits)
4547
+ listener(submission);
4548
+ }
4549
+ else if (this.#pending !== null) {
4550
+ const pending = this.#pending;
4551
+ this.#pending = null;
4552
+ pending.offAbort?.();
4553
+ this.#pendingInputs.take(submission, this.#pendingScope());
4554
+ pending.resolve(submission);
4555
+ }
4556
+ else
4557
+ this.#queuedSubmissions.push(submission);
4558
+ this.#pendingInputs.flush(this.#pendingScope());
4559
+ }
4560
+ #submit(text, delivery) {
3846
4561
  if (this.#prompt !== null) {
3847
4562
  if (this.#prompt.request.presentation === 'progress')
3848
4563
  return;
3849
- this.#editor.setText('');
3850
4564
  const answer = this.#prompt.request.preserveWhitespace === true ? text : text.trim();
3851
4565
  this.#finishPrompt(answer.trim() === '' ? null : answer);
3852
4566
  this.#render();
3853
4567
  return;
3854
4568
  }
4569
+ if (this.#inspected !== undefined) {
4570
+ this.#submitInspect(text, delivery);
4571
+ return;
4572
+ }
3855
4573
  const { submission, historyText } = this.#buildSubmission(text);
3856
- const { images, files = [], literal = false, text: submittedText } = submission;
4574
+ const { images, files = [], text: submittedText } = submission;
3857
4575
  const queueEditNewer = this.#queueEditNewer;
4576
+ // Commands keep their own routing; recalled follow-ups remain next-turn messages.
4577
+ if (delivery === 'steer')
4578
+ submission.literal = true;
4579
+ const command = !submission.literal && (submittedText.trimStart().startsWith('/') || (images.length === 0 && files.length === 0 && submittedText.startsWith('!')));
4580
+ if (!command && this.#state.status === 'running') {
4581
+ submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : delivery ?? this.#busyEnter;
4582
+ }
3858
4583
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
3859
4584
  this.#history.push(historyText);
3860
4585
  this.#historyStore?.add(historyText);
@@ -3863,11 +4588,10 @@ export class LocalTui {
3863
4588
  this.#draft = '';
3864
4589
  this.#literalInput = false;
3865
4590
  this.#queueEditNewer = null;
3866
- this.#queueEditPending = false;
4591
+ this.#cancelQueueRecall();
3867
4592
  this.#editor.setText('');
3868
4593
  this.#images.clear();
3869
4594
  this.#files.clear();
3870
- this.#clearDraft();
3871
4595
  this.#ac = null;
3872
4596
  this.#search = null;
3873
4597
  this.#settings = null;
@@ -3875,63 +4599,69 @@ export class LocalTui {
3875
4599
  // Image placeholders are TUI-owned. Mixed image+slash drafts stay a
3876
4600
  // submission so restore can keep the original markers; the runner strips
3877
4601
  // them only to detect and execute Harness commands.
3878
- const slash = !literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
4602
+ const slash = !submission.literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
3879
4603
  if (slash !== null) {
3880
4604
  if (queueEditNewer !== null)
3881
4605
  this.#queuedSubmissions.push(...queueEditNewer);
3882
4606
  this.#runSlash(slash.name, slash.args);
4607
+ this.#clearDraft();
3883
4608
  return;
3884
4609
  }
3885
4610
  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
- }
4611
+ if (submittedText !== '' || images.length > 0 || files.length > 0 || this.#pending !== null)
4612
+ this.#dispatchInput(submission);
4613
+ if (queueEditNewer !== null)
4614
+ this.#queuedSubmissions.push(...queueEditNewer);
3901
4615
  if (submittedText !== '' || images.length > 0 || files.length > 0) {
3902
4616
  this.#popStashedDraft();
3903
4617
  }
4618
+ this.#pendingInputs.flush(this.#pendingScope());
4619
+ this.#clearDraft();
3904
4620
  this.#render();
3905
4621
  }
3906
- #submitInspect(text) {
4622
+ #submitInspect(text, delivery) {
4623
+ if (this.#inspected?.writable !== true)
4624
+ return;
3907
4625
  const { submission, historyText } = this.#buildSubmission(text);
4626
+ const newer = this.#queueEditNewer;
4627
+ if (delivery === 'steer')
4628
+ submission.literal = true;
4629
+ submission.delivery = newer !== null || this.#queueEditPending ? 'queue' : this.#state.status === 'running' ? delivery ?? this.#busyEnter : 'queue';
3908
4630
  const { images, files = [], literal = false, text: submittedText } = submission;
3909
4631
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
3910
4632
  this.#history.push(historyText);
3911
4633
  this.#historyStore?.add(historyText);
3912
4634
  }
4635
+ this.#queueEditNewer = null;
4636
+ this.#cancelQueueRecall();
3913
4637
  this.#historyIndex = 0;
3914
4638
  this.#draft = '';
3915
4639
  this.#literalInput = false;
3916
4640
  this.#editor.setText('');
3917
4641
  this.#images.clear();
3918
4642
  this.#files.clear();
3919
- this.#clearDraft();
3920
4643
  this.#ac = null;
3921
4644
  this.#search = null;
3922
4645
  const slash = !literal && images.length === 0 && files.length === 0 ? parseSlashInput(submittedText) : null;
3923
4646
  if (slash !== null) {
4647
+ this.#queueEditNewer = newer;
3924
4648
  this.#runSlash(slash.name, slash.args);
3925
4649
  return;
3926
4650
  }
3927
4651
  this.#followTail();
3928
- if (submittedText === '' && images.length === 0 && files.length === 0) {
4652
+ if (submittedText === '' && images.length === 0 && files.length === 0 && !newer?.length) {
3929
4653
  this.#render();
3930
4654
  return;
3931
4655
  }
3932
4656
  const { literal: _literal, ...inspectSubmission } = submission;
3933
- for (const listener of this.#inspectSubmits)
3934
- listener(inspectSubmission);
4657
+ if (submittedText !== '' || images.length > 0 || files.length > 0) {
4658
+ this.#dispatchInput(inspectSubmission);
4659
+ }
4660
+ for (const draft of newer ?? []) {
4661
+ this.#dispatchInput({ ...draft, delivery: 'queue' });
4662
+ }
4663
+ this.#pendingInputs.flush(this.#pendingScope());
4664
+ this.#clearDraft();
3935
4665
  this.#popStashedDraft();
3936
4666
  this.#render();
3937
4667
  }
@@ -3949,6 +4679,10 @@ export class LocalTui {
3949
4679
  }
3950
4680
  // Reading controls cover the current view; submitting a command that
3951
4681
  // opens one must preserve the same return position as a keyboard action.
4682
+ if (command.name === 'queue' && this.#inspected !== undefined) {
4683
+ this.#runAction('manage-queue');
4684
+ return;
4685
+ }
3952
4686
  if (command.name === 'settings') {
3953
4687
  this.#runSettings(args);
3954
4688
  return;
@@ -3966,18 +4700,17 @@ export class LocalTui {
3966
4700
  return;
3967
4701
  }
3968
4702
  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
- }
4703
+ this.#dispatchInput({ text: raw, images: [] });
3978
4704
  this.#render();
3979
4705
  return;
3980
4706
  }
4707
+ if (command.name === 'help') {
4708
+ if (args !== '' && args !== 'full')
4709
+ this.notice('Usage: /help [full]');
4710
+ else
4711
+ this.#runHelp(args === 'full');
4712
+ return;
4713
+ }
3981
4714
  this.#followTail();
3982
4715
  if (command.name === 'quit') {
3983
4716
  this.#render();
@@ -3998,30 +4731,39 @@ export class LocalTui {
3998
4731
  this.#runTools();
3999
4732
  return;
4000
4733
  }
4001
- if (args !== '' && args !== 'full') {
4002
- this.#notice('Usage: /help [full]');
4003
- this.#render();
4004
- return;
4005
- }
4006
- this.#runHelp(args === 'full');
4734
+ this.#render();
4007
4735
  }
4008
4736
  #commands() {
4009
4737
  const localNames = new Set(BUILTIN_SLASH_COMMANDS.flatMap((command) => [command.name, ...(command.aliases ?? [])]));
4010
4738
  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
- }));
4739
+ .filter((command) => !localNames.has(command.name));
4018
4740
  return [...BUILTIN_SLASH_COMMANDS, ...runtime];
4019
4741
  }
4020
- #finishPrompt(answer) {
4742
+ #finishPrompt(answer, kind = 'text') {
4021
4743
  const pending = this.#prompt;
4022
4744
  if (pending === null)
4023
4745
  return;
4746
+ const draft = structuredPromptAnswer(pending, this.#editor.expandedText);
4747
+ try {
4748
+ pending.request.onAnswerDraft?.(draft);
4749
+ }
4750
+ catch (error) {
4751
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
4752
+ }
4753
+ if (answer !== null && pending.request.answerFormat === 'json' && kind !== 'action' && this.#tty) {
4754
+ answer = JSON.stringify(kind === 'selection' ? draft : { selected: [], ...(answer === '' ? {} : { custom: answer }) });
4755
+ }
4756
+ if (pending.browserState !== undefined) {
4757
+ const selected = filteredPromptOptions(pending.request, this.#editor.text, pending.collapsed)[pending.selected];
4758
+ pending.browserState.query = this.#editor.text;
4759
+ if (selected !== undefined)
4760
+ pending.browserState.selected = selected.value ?? selected.label;
4761
+ }
4024
4762
  this.#prompt = null;
4763
+ this.#promptActionPress = undefined;
4764
+ this.#promptChoicePress = undefined;
4765
+ this.#selection.clear();
4766
+ this.#wheelGesture = IDLE_WHEEL;
4025
4767
  if (pending.request.notify !== false) {
4026
4768
  this.#herdr.promptResolved();
4027
4769
  this.#syncWindowTitle();
@@ -4054,11 +4796,14 @@ export class LocalTui {
4054
4796
  }
4055
4797
  /** Save and close the visible overlay so a prompt owns both input and screen. */
4056
4798
  #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;
4799
+ const displaced = this.#fileBrowser !== null
4800
+ ? { kind: 'fileBrowser', browser: this.#fileBrowser }
4801
+ : this.#trajectory !== null
4802
+ ? { kind: 'trajectory', state: this.#trajectory }
4803
+ : this.#settings !== null
4804
+ ? { kind: 'settings', state: this.#settings }
4805
+ : this.#search !== null ? { kind: 'search', state: this.#search } : null;
4806
+ this.#fileBrowser = null;
4062
4807
  this.#trajectory = null;
4063
4808
  this.#settings = null;
4064
4809
  this.#search = null;
@@ -4070,7 +4815,9 @@ export class LocalTui {
4070
4815
  this.#promptDisplaced = null;
4071
4816
  if (displaced === null)
4072
4817
  return;
4073
- if (displaced.kind === 'trajectory')
4818
+ if (displaced.kind === 'fileBrowser')
4819
+ this.#fileBrowser = displaced.browser;
4820
+ else if (displaced.kind === 'trajectory')
4074
4821
  this.#trajectory = displaced.state;
4075
4822
  else if (displaced.kind === 'settings') {
4076
4823
  this.#settings = displaced.state;
@@ -4116,7 +4863,7 @@ export class LocalTui {
4116
4863
  * that did not answer to the shared list vocabulary, which is exactly the
4117
4864
  * inconsistency the reader noticed.
4118
4865
  */
4119
- const answer = await this.prompt({
4866
+ const answer = await this.prompt({ localize: true,
4120
4867
  title: 'Copy',
4121
4868
  question: items.length === 1 ? 'One thing to copy' : 'Choose what to copy',
4122
4869
  filterable: true,
@@ -4152,13 +4899,17 @@ export class LocalTui {
4152
4899
  }
4153
4900
  #settingsActions() {
4154
4901
  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' },
4902
+ { 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' },
4903
+ { 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' },
4904
+ { 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' },
4905
+ { 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' },
4906
+ { 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' },
4907
+ { 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
4908
  ].filter(item => this.#runtimeCommands.some(command => command.name === item.action));
4159
4909
  const customBindings = Object.entries(this.#keybindings).filter(([key, action]) => DEFAULT_KEYBINDINGS[key] !== action).length;
4160
4910
  return [
4161
4911
  ...runtimeActions,
4912
+ ...(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
4913
  ...(this.#keybindingsPath === undefined ? [] : [{
4163
4914
  id: 'action:keybindings',
4164
4915
  label: 'Keybindings',
@@ -4208,6 +4959,7 @@ export class LocalTui {
4208
4959
  }
4209
4960
  #configureNotifications() {
4210
4961
  this.#notifications.configure({
4962
+ language: resolveUiLanguage(this.#uiLanguage),
4211
4963
  policy: this.#notificationPolicy,
4212
4964
  thresholdMs: NOTIFICATION_THRESHOLD_MS,
4213
4965
  });
@@ -4234,32 +4986,66 @@ export class LocalTui {
4234
4986
  this.#term.output.write(sequence);
4235
4987
  }, 25);
4236
4988
  }
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
4989
  #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 });
4990
+ if (!this.#tty) {
4991
+ this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings),
4992
+ '', formatHelpText(this.#commands(), resolveUiLanguage(this.#uiLanguage)),
4993
+ ...(full ? ['', formatHotkeysText(this.#keybindings)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
4994
+ return;
4995
+ }
4996
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
4997
+ return;
4998
+ const abort = new AbortController();
4999
+ this.#fileReviewAbort = abort;
5000
+ const surface = {
5001
+ prompt: (request) => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5002
+ copyText: (text, label) => this.copyText(text, label),
5003
+ insertText: (text) => this.insertText(text),
5004
+ };
5005
+ void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal)
5006
+ .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5007
+ this.notice(String(error), { level: 'error' }); })
5008
+ .finally(() => { if (this.#fileReviewAbort === abort)
5009
+ this.#fileReviewAbort = undefined; });
4254
5010
  }
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
- */
5011
+ async #readCommandOutput(block) {
5012
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null || this.#disposed)
5013
+ return;
5014
+ const abort = new AbortController();
5015
+ this.#fileReviewAbort = abort;
5016
+ try {
5017
+ await readReference({
5018
+ prompt: request => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5019
+ copyText: (text, label) => this.copyText(text, label),
5020
+ }, `/${block.command}`, block.text, abort.signal);
5021
+ }
5022
+ catch (error) {
5023
+ if (!abort.signal.aborted && !this.#disposed)
5024
+ this.notice(String(error), { level: 'error' });
5025
+ }
5026
+ finally {
5027
+ if (this.#fileReviewAbort === abort)
5028
+ this.#fileReviewAbort = undefined;
5029
+ }
5030
+ }
5031
+ /** Open the searchable catalog, retaining plain output for line mode. */
4259
5032
  #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 });
5033
+ if (!this.#tty) {
5034
+ this.commandOutput('tools', `Tools · ${this.#tools.length} active\n\n${formatToolsText(this.#tools)}`);
5035
+ return;
5036
+ }
5037
+ if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
5038
+ return;
5039
+ const abort = new AbortController();
5040
+ this.#fileReviewAbort = abort;
5041
+ void browseTools({
5042
+ prompt: request => this.prompt({ ...request, signal: abort.signal, onSuperseded: () => { abort.abort(); } }),
5043
+ copyText: (text, label) => this.copyText(text, label),
5044
+ }, this.#tools, resolveUiLanguage(this.#uiLanguage), abort.signal)
5045
+ .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5046
+ this.notice(String(error), { level: 'error' }); })
5047
+ .finally(() => { if (this.#fileReviewAbort === abort)
5048
+ this.#fileReviewAbort = undefined; });
4263
5049
  }
4264
5050
  /** Resolve a configured action for one key id, tolerating camelCase ids. */
4265
5051
  #boundAction(id) {
@@ -4363,10 +5149,12 @@ export class LocalTui {
4363
5149
  this.#render();
4364
5150
  return true;
4365
5151
  }
5152
+ if (action === 'manage-goal') {
5153
+ this.#openGoal();
5154
+ return true;
5155
+ }
4366
5156
  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)
5157
+ if (this.#queueHandler === undefined)
4370
5158
  this.notice('Queue management is unavailable in this profile.');
4371
5159
  else if (this.#queueHotkeyAbort === undefined) {
4372
5160
  const abort = new AbortController();
@@ -4382,33 +5170,30 @@ export class LocalTui {
4382
5170
  if (action === 'steer-turn') {
4383
5171
  if (this.#search !== null)
4384
5172
  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.');
5173
+ if (this.#inspected !== undefined && !this.#inspected.writable)
5174
+ return true;
5175
+ if (this.#state.status !== 'running') {
5176
+ this.notice('Steering needs an active turn. Use Enter to send the next message.');
4390
5177
  }
4391
5178
  else if (this.#queueEditNewer !== null || this.#queueEditPending) {
4392
5179
  this.notice('Finish editing the queued follow-up with Enter before steering.');
4393
5180
  }
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
- }
5181
+ else if (this.#editor.expandedText.trim() !== '' || this.#images.count > 0 || this.#files.copies().length > 0) {
5182
+ this.#submit(this.#editor.expandedText, 'steer');
4409
5183
  }
4410
5184
  return true;
4411
5185
  }
5186
+ if (action === 'quote-selection')
5187
+ return true;
5188
+ if (action === 'tool-actions') {
5189
+ const anchor = this.#viewportBlock();
5190
+ const block = anchor === undefined ? undefined : this.#state.blocks[anchor];
5191
+ if (block?.kind === 'commandOutput')
5192
+ void this.#readCommandOutput(block);
5193
+ else
5194
+ void this.#openToolActions();
5195
+ return true;
5196
+ }
4412
5197
  if (action === 'review-changes') {
4413
5198
  // The keyboard twin of right-clicking a tool's path: open the files the
4414
5199
  // tool under the viewport declared. It shares the mouse path's region
@@ -4424,7 +5209,7 @@ export class LocalTui {
4424
5209
  const regions = this.#fileActionFrame?.transcript?.fileRegions ?? [];
4425
5210
  const region = (top === undefined ? undefined : regions.find(region => top >= region.start && top < region.end))
4426
5211
  ?? (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)];
5212
+ const block = this.#currentTool() ?? this.#state.blocks[region?.block ?? (this.#viewportBlock() ?? this.#state.blocks.length - 1)];
4428
5213
  const files = block?.kind === 'tool' ? blockFileAction(block) : undefined;
4429
5214
  if (files === undefined) {
4430
5215
  this.#showTransientStatus('No tool files here · use /diff', 4_000);
@@ -4490,7 +5275,16 @@ export class LocalTui {
4490
5275
  return true;
4491
5276
  }
4492
5277
  if (action === 'retry') {
4493
- this.#submit('/retry');
5278
+ if (!this.#canContinue())
5279
+ return true;
5280
+ const request = { sessionId: this.#sessionId, failureSeq: this.#sessionControls.continuation };
5281
+ this.#continuing = true;
5282
+ void this.#continueHandler(request).catch(error => {
5283
+ if (!this.#disposed && this.#sessionId === request.sessionId)
5284
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
5285
+ }).finally(() => { this.#continuing = false; if (!this.#disposed)
5286
+ this.#render(); });
5287
+ this.#render();
4494
5288
  return true;
4495
5289
  }
4496
5290
  if (action === 'cycle-model-forward') {
@@ -4514,12 +5308,11 @@ export class LocalTui {
4514
5308
  this.#startAsyncPaste(this.#pasteClipboard());
4515
5309
  return true;
4516
5310
  }
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.
5311
+ // Explicit queue delivery is independent of the busy-Enter preference.
4519
5312
  if (action === 'follow-up') {
4520
- if (this.#editor.expandedText.trim() === '')
5313
+ if (this.#editor.expandedText.trim() === '' && this.#images.count === 0 && this.#files.copies().length === 0)
4521
5314
  return false;
4522
- this.#submit(this.#editor.expandedText);
5315
+ this.#submit(this.#editor.expandedText, 'queue');
4523
5316
  return true;
4524
5317
  }
4525
5318
  if (action !== 'external-editor')
@@ -4574,8 +5367,10 @@ export class LocalTui {
4574
5367
  this.#images.clear();
4575
5368
  this.#files.clear();
4576
5369
  if (options.clearQueueEdit) {
5370
+ if (this.#queueEditNewer !== null)
5371
+ this.#queuedSubmissions.push(...this.#queueEditNewer);
4577
5372
  this.#queueEditNewer = null;
4578
- this.#queueEditPending = false;
5373
+ this.#cancelQueueRecall();
4579
5374
  }
4580
5375
  if (options.closeSearch)
4581
5376
  this.#search = null;
@@ -4633,11 +5428,39 @@ export class LocalTui {
4633
5428
  this.#historyIndex = 0;
4634
5429
  this.#refreshAutocomplete();
4635
5430
  }
5431
+ async #testKeys() {
5432
+ const abort = new AbortController();
5433
+ // Reuse browsing cancellation: approvals and session switches own the terminal.
5434
+ this.#fileReviewAbort = abort;
5435
+ this.#keyTest = { signal: abort.signal, observe: event => {
5436
+ const pending = this.#prompt;
5437
+ const detail = keyTestDescription(event, this.#keybindings, resolveUiLanguage(this.#uiLanguage));
5438
+ if (pending?.request.signal !== abort.signal || detail === undefined)
5439
+ return;
5440
+ pending.sourceRequest = { ...pending.sourceRequest, detail };
5441
+ pending.request = localizePrompt(pending.sourceRequest, resolveUiLanguage(this.#uiLanguage));
5442
+ this.#render();
5443
+ } };
5444
+ try {
5445
+ await this.prompt({ localize: true, title: 'Key test', presentation: 'progress',
5446
+ question: 'Press a key. Esc or Ctrl+C returns without changing bindings.',
5447
+ detail: uiText(resolveUiLanguage(this.#uiLanguage), 'If nothing appears, the terminal or operating system may have intercepted the key.'),
5448
+ allowCustom: false, notify: false, signal: abort.signal, onSuperseded: () => { abort.abort(); } });
5449
+ }
5450
+ finally {
5451
+ if (this.#keyTest?.signal === abort.signal)
5452
+ this.#keyTest = undefined;
5453
+ if (this.#fileReviewAbort === abort)
5454
+ this.#fileReviewAbort = undefined;
5455
+ }
5456
+ return !abort.signal.aborted;
5457
+ }
4636
5458
  async #editKeybindings() {
4637
5459
  if (this.#keybindingsPath === undefined)
4638
5460
  return;
4639
5461
  while (!this.#disposed) {
4640
5462
  const options = [
5463
+ { label: 'Test keys', value: '__test_keys__', description: 'See what the terminal sends without running an action.' },
4641
5464
  ...TUI_ACTIONS.map(action => {
4642
5465
  const keys = keysForAction(this.#keybindings, action);
4643
5466
  const defaultKeys = keysForAction(DEFAULT_KEYBINDINGS, action);
@@ -4653,7 +5476,7 @@ export class LocalTui {
4653
5476
  description: 'Restore default shortcuts in keybindings.json',
4654
5477
  },
4655
5478
  ];
4656
- const choice = await this.prompt({
5479
+ const choice = await this.prompt({ localize: true,
4657
5480
  title: 'Keybindings',
4658
5481
  question: 'Choose an action to remap, or Esc to return to /settings.',
4659
5482
  options,
@@ -4663,6 +5486,11 @@ export class LocalTui {
4663
5486
  });
4664
5487
  if (choice === null || this.#disposed)
4665
5488
  break;
5489
+ if (choice === '__test_keys__') {
5490
+ if (!await this.#testKeys())
5491
+ break;
5492
+ continue;
5493
+ }
4666
5494
  if (choice === '__reset_all__') {
4667
5495
  try {
4668
5496
  saveKeybindings(this.#keybindingsPath, DEFAULT_KEYBINDINGS);
@@ -4681,7 +5509,7 @@ export class LocalTui {
4681
5509
  continue;
4682
5510
  const currentRawKeys = Object.entries(this.#keybindings).filter(([, bound]) => bound === action).map(([key]) => key);
4683
5511
  const defaultKeys = keysForAction(DEFAULT_KEYBINDINGS, action);
4684
- const entered = await this.prompt({
5512
+ const entered = await this.prompt({ localize: true,
4685
5513
  title: `Remap ${TUI_ACTION_LABELS[action]}`,
4686
5514
  question: `Enter key chord (e.g. ctrl+s, alt+k), "default" (${defaultKeys}), or "none" to unbind.`,
4687
5515
  initialInput: currentRawKeys[0] ?? '',
@@ -4756,16 +5584,18 @@ export class LocalTui {
4756
5584
  * above them.
4757
5585
  */
4758
5586
  const anchored = anchor === undefined ? undefined : this.#state.blocks[anchor];
4759
- if (anchor !== undefined && (anchored?.kind === 'commandOutput' || anchored?.kind === 'aside')) {
5587
+ if (anchor !== undefined && (anchored?.kind === 'commandOutput' || anchored?.kind === 'aside' || (anchored?.kind === 'notice' && anchored.detail !== undefined))) {
4760
5588
  if (this.#expandedOutputs.has(anchored))
4761
5589
  this.#expandedOutputs.delete(anchored);
4762
5590
  else
4763
5591
  this.#expandedOutputs.add(anchored);
4764
5592
  this.#inspect(anchor);
5593
+ this.#focusBlock = anchor;
5594
+ this.#focusBlockEdge = 'start';
4765
5595
  this.#render();
4766
5596
  return;
4767
5597
  }
4768
- const groups = turnGroups(this.#state).filter(item => !item.live);
5598
+ const groups = turnGroups(this.#state);
4769
5599
  // The answer, changed-file record, and following notices still address
4770
5600
  // their turn when the reader is following the tail.
4771
5601
  const reach = (group) => {
@@ -4788,18 +5618,132 @@ export class LocalTui {
4788
5618
  return anchor >= first && anchor <= reach(item);
4789
5619
  });
4790
5620
  if (group !== undefined) {
4791
- this.#toggleSet(this.#openedGroups, group.key);
4792
- this.#inspect((group.answer ?? group.until) - 1);
5621
+ const scope = this.#detailScope(group);
5622
+ for (const member of groups)
5623
+ if (this.#detailScope(member) === scope)
5624
+ this.#openedGroups.add(member.key);
5625
+ const rows = this.#processRows();
5626
+ this.#processSelection = rows.includes(anchor) ? anchor : rows[0];
5627
+ this.#ac = null;
5628
+ this.#inspect(this.#processSelection ?? group.start);
5629
+ this.#focusBlock = this.#processSelection ?? group.start;
5630
+ this.#focusBlockEdge = 'start';
4793
5631
  this.#render();
4794
5632
  return;
4795
5633
  }
4796
- // A live turn already has the process presentation; Alt+O owns tool details.
5634
+ }
5635
+ /** The process list follows durable block order and grows without moving selection. */
5636
+ #processRows() {
5637
+ 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() !== ''
5638
+ ? [group.start + offset] : []));
5639
+ }
5640
+ #handleProcessNavigation(event) {
5641
+ const selected = this.#processSelection;
5642
+ if (selected === undefined)
5643
+ return false;
5644
+ if (event.type === 'text' || event.type === 'paste-start') {
5645
+ this.#processSelection = undefined;
5646
+ return false;
5647
+ }
5648
+ if (event.type !== 'key')
5649
+ return false;
5650
+ if (event.id === 'escape' || event.id === 'end') {
5651
+ this.#followTail();
5652
+ this.#render();
5653
+ return true;
5654
+ }
5655
+ if (event.id === 'home') {
5656
+ const current = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
5657
+ this.#processSelection = current?.block ?? selected;
5658
+ this.#focusBlock = this.#processSelection;
5659
+ this.#focusBlockEdge = 'start';
5660
+ this.#render();
5661
+ return true;
5662
+ }
5663
+ if (event.id === 'up' || event.id === 'down') {
5664
+ const rows = this.#processRows();
5665
+ const at = Math.max(0, rows.indexOf(selected));
5666
+ const next = rows[Math.max(0, Math.min(rows.length - 1, at + (event.id === 'up' ? -1 : 1)))];
5667
+ if (next !== undefined) {
5668
+ this.#processSelection = next;
5669
+ const row = this.#blockStarts[next];
5670
+ if (row === undefined || row < this.#scrollStart || row >= this.#scrollStart + this.#scrollBudget - 1) {
5671
+ this.#focusBlock = next;
5672
+ this.#focusBlockEdge = 'start';
5673
+ }
5674
+ }
5675
+ this.#render();
5676
+ return true;
5677
+ }
5678
+ if (!['enter', 'left', 'right'].includes(event.id))
5679
+ return false;
5680
+ this.#toggleProcessItem(selected, event.id);
5681
+ return true;
5682
+ }
5683
+ #toggleProcessItem(selected, direction) {
5684
+ const block = this.#state.blocks[selected];
5685
+ if (block?.kind === 'tool') {
5686
+ const visible = this.#detailedToolIds();
5687
+ const wasOpen = visible.has(block.callId);
5688
+ const open = direction === 'right' || direction !== 'left' && !wasOpen;
5689
+ const raw = this.#detailedToolIds(false).has(block.callId);
5690
+ if (wasOpen && !open) {
5691
+ const target = this.#fileActionFrame?.transcript?.disclosures?.find(item => item.block === selected);
5692
+ const offset = target?.detailEnd !== undefined && this.#scrollStart >= target.row && this.#scrollStart < target.detailEnd
5693
+ ? this.#scrollStart - target.row : 0;
5694
+ this.#toolReadingPositions.set(block.callId, { offset, width: this.#term.width(), raw });
5695
+ }
5696
+ if (open !== wasOpen) {
5697
+ const saved = open ? this.#toolReadingPositions.get(block.callId) : undefined;
5698
+ this.#focusBlock = selected;
5699
+ this.#focusBlockEdge = 'start';
5700
+ // Width changes and raw/readable switches change the row layout.
5701
+ this.#focusBlockOffset = saved?.width === this.#term.width() && saved.raw === raw ? saved.offset : 0;
5702
+ }
5703
+ // Once a reader edits one disclosure, preserve the other visible calls as
5704
+ // individual choices instead of letting the turn-wide override reopen it.
5705
+ this.#expandedToolRows = new Set(visible);
5706
+ this.#detailedTurns.clear();
5707
+ if (open)
5708
+ this.#expandedToolRows.add(block.callId);
5709
+ else
5710
+ this.#expandedToolRows.delete(block.callId);
5711
+ }
5712
+ else if (block?.kind === 'assistant') {
5713
+ const key = reasoningKey(block);
5714
+ const open = direction === 'right' || direction !== 'left' && !this.#visibleReasoning().has(key);
5715
+ if (open) {
5716
+ this.#expandedReasoning.add(key);
5717
+ this.#closedReasoning.delete(key);
5718
+ }
5719
+ else {
5720
+ this.#expandedReasoning.delete(key);
5721
+ this.#closedReasoning.add(key);
5722
+ }
5723
+ }
5724
+ const row = this.#blockStarts[selected];
5725
+ if (row === undefined || row < this.#scrollStart || row > this.#scrollStart + Math.floor(this.#scrollBudget / 2)) {
5726
+ this.#focusBlock = selected;
5727
+ this.#focusBlockEdge = 'start';
5728
+ }
5729
+ this.#render();
5730
+ }
5731
+ #visibleReasoning() {
5732
+ const open = new Set(this.#expandedReasoning);
5733
+ if (!this.#hideThinking)
5734
+ for (const block of this.#state.blocks) {
5735
+ if (block.kind === 'assistant')
5736
+ open.add(reasoningKey(block));
5737
+ }
5738
+ for (const key of this.#closedReasoning)
5739
+ open.delete(key);
5740
+ return open;
4797
5741
  }
4798
5742
  #detailScope(group) {
4799
5743
  return group.turn === undefined ? group.key : `turn:${group.turn}`;
4800
5744
  }
4801
- #detailedToolIds() {
4802
- const calls = new Set();
5745
+ #detailedToolIds(includeReadable = true) {
5746
+ const calls = new Set(includeReadable ? this.#expandedToolRows : []);
4803
5747
  if (this.#detailedTurns.size === 0)
4804
5748
  return calls;
4805
5749
  for (const group of turnGroups(this.#state)) {
@@ -4843,6 +5787,11 @@ export class LocalTui {
4843
5787
  this.#openedGroups.add(member.key);
4844
5788
  const tools = members.flatMap(member => this.#state.blocks.slice(member.start, member.until)
4845
5789
  .flatMap((block, offset) => block.kind === 'tool' ? [member.start + offset] : []));
5790
+ for (const at of tools) {
5791
+ const block = this.#state.blocks[at];
5792
+ if (block?.kind === 'tool')
5793
+ this.#expandedToolRows.delete(block.callId);
5794
+ }
4846
5795
  const target = tools.includes(anchor) ? anchor : tools[0];
4847
5796
  this.#inspect(target);
4848
5797
  this.#focusBlock = target;
@@ -4868,14 +5817,19 @@ export class LocalTui {
4868
5817
  return [];
4869
5818
  const options = {
4870
5819
  width: this.#term.width(),
5820
+ language: resolveUiLanguage(this.#uiLanguage),
4871
5821
  colors: this.#colors,
4872
5822
  themeName: this.#themeName,
5823
+ terminal: this.#terminalColors,
5824
+ compactLinks: this.#compactLinks,
4873
5825
  ...(this.#colorDepth === undefined ? {} : { colorDepth: this.#colorDepth }),
4874
5826
  mathMode: this.#mathMode,
5827
+ mermaidMode: this.#mermaidMode,
4875
5828
  toolsExpanded: this.#toolsExpanded,
4876
5829
  expandedOutputs: this.#expandedOutputs,
4877
- hideThinking: this.#hideThinking,
5830
+ hideThinking: true,
4878
5831
  expandedTools: this.#detailedToolIds(),
5832
+ expandedReasoning: this.#visibleReasoning(),
4879
5833
  ...(this.#openedGroups === undefined ? {} : { openedGroups: this.#openedGroups }),
4880
5834
  };
4881
5835
  return searchMatches(renderedBlocks(this.#state, options), search.query);
@@ -4978,6 +5932,7 @@ export function apply(ctx, config) {
4978
5932
  // settings service, which persists the edit into the active profile patch.
4979
5933
  const storedPrefs = () => {
4980
5934
  const prefs = {
5935
+ uiLanguage: config.uiLanguage.get(),
4981
5936
  theme: config.theme.get(),
4982
5937
  colors: resolveColors(config.colors.get(), term.output.isTTY === true),
4983
5938
  motion: config.motion.get(),
@@ -4988,6 +5943,7 @@ export function apply(ctx, config) {
4988
5943
  terminalState: config.terminalState.get(),
4989
5944
  copyOnSelect: config.copyOnSelect.get(),
4990
5945
  mouseInteraction: config.mouseInteraction.get(),
5946
+ busyEnter: config.busyEnter.get(),
4991
5947
  pasteProtection: config.pasteProtection.get(),
4992
5948
  checkUpdates: config.checkUpdates.get(),
4993
5949
  startupChangelog: config.startupChangelog.get(),