@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.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 (229) hide show
  1. package/README.md +29 -10
  2. package/bin/dsh-tui.js +1 -1
  3. package/lib/types/commands.d.ts.map +1 -1
  4. package/lib/types/commands.js +4 -2
  5. package/lib/types/components/ActivityPicker.d.ts +4 -1
  6. package/lib/types/components/ActivityPicker.d.ts.map +1 -1
  7. package/lib/types/components/ActivityPicker.js +2 -2
  8. package/lib/types/components/BtwPanel.d.ts.map +1 -1
  9. package/lib/types/components/BtwPanel.js +1 -1
  10. package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
  11. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
  12. package/lib/types/components/Chat/SubagentMessage.js +11 -3
  13. package/lib/types/components/CommandSuggestions.d.ts +21 -4
  14. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  15. package/lib/types/components/CommandSuggestions.js +41 -14
  16. package/lib/types/components/EffortSlider.d.ts +4 -1
  17. package/lib/types/components/EffortSlider.d.ts.map +1 -1
  18. package/lib/types/components/EffortSlider.js +4 -2
  19. package/lib/types/components/ExtensionDialog.js +6 -2
  20. package/lib/types/components/FileSuggestions.d.ts +26 -5
  21. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  22. package/lib/types/components/FileSuggestions.js +53 -16
  23. package/lib/types/components/GoalTodoPanel.d.ts +26 -2
  24. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  25. package/lib/types/components/GoalTodoPanel.js +86 -17
  26. package/lib/types/components/HelpMenu.d.ts +7 -1
  27. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  28. package/lib/types/components/HelpMenu.js +12 -4
  29. package/lib/types/components/HistorySearchDialog.d.ts +4 -1
  30. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  31. package/lib/types/components/HistorySearchDialog.js +2 -2
  32. package/lib/types/components/LangPicker.d.ts +14 -0
  33. package/lib/types/components/LangPicker.d.ts.map +1 -0
  34. package/lib/types/components/LangPicker.js +17 -0
  35. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  36. package/lib/types/components/LoadedContextPanel.js +3 -1
  37. package/lib/types/components/LogoV2.d.ts +5 -1
  38. package/lib/types/components/LogoV2.d.ts.map +1 -1
  39. package/lib/types/components/LogoV2.js +12 -3
  40. package/lib/types/components/Markdown.d.ts.map +1 -1
  41. package/lib/types/components/Markdown.js +12 -3
  42. package/lib/types/components/MessageList.d.ts +43 -2
  43. package/lib/types/components/MessageList.d.ts.map +1 -1
  44. package/lib/types/components/MessageList.js +553 -41
  45. package/lib/types/components/ModelPicker.d.ts +4 -1
  46. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  47. package/lib/types/components/ModelPicker.js +2 -2
  48. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  49. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  50. package/lib/types/components/PermissionsPicker.js +27 -0
  51. package/lib/types/components/PlanPicker.d.ts +15 -0
  52. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  53. package/lib/types/components/PlanPicker.js +18 -0
  54. package/lib/types/components/PresetPicker.d.ts +4 -1
  55. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  56. package/lib/types/components/PresetPicker.js +2 -2
  57. package/lib/types/components/PromptInput.d.ts +4 -3
  58. package/lib/types/components/PromptInput.d.ts.map +1 -1
  59. package/lib/types/components/PromptInput.js +273 -50
  60. package/lib/types/components/RewindPicker.d.ts +12 -1
  61. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  62. package/lib/types/components/RewindPicker.js +6 -4
  63. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  64. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  65. package/lib/types/components/ScrollbarGutter.js +68 -0
  66. package/lib/types/components/Select.d.ts +8 -1
  67. package/lib/types/components/Select.d.ts.map +1 -1
  68. package/lib/types/components/Select.js +4 -2
  69. package/lib/types/components/SkillsPicker.d.ts +4 -1
  70. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  71. package/lib/types/components/SkillsPicker.js +2 -2
  72. package/lib/types/components/SubagentCard.d.ts +2 -1
  73. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  74. package/lib/types/components/SubagentCard.js +6 -1
  75. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  76. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  77. package/lib/types/components/SubagentDashboard.js +9 -1
  78. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  79. package/lib/types/components/SubagentDetailScene.js +5 -4
  80. package/lib/types/components/SuggestionCard.d.ts +63 -0
  81. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  82. package/lib/types/components/SuggestionCard.js +77 -0
  83. package/lib/types/components/ThemePicker.d.ts +4 -1
  84. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  85. package/lib/types/components/ThemePicker.js +2 -2
  86. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  87. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  88. package/lib/types/components/ThinkingToggle.js +2 -2
  89. package/lib/types/components/TimelineRail.d.ts +53 -0
  90. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  91. package/lib/types/components/TimelineRail.js +190 -0
  92. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  93. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  94. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  95. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  96. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  97. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  98. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  99. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  100. package/lib/types/components/WorkspacePicker.js +2 -2
  101. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  102. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  103. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  104. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  105. package/lib/types/components/design-system/ListItem.js +8 -3
  106. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  107. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  108. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  109. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  110. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  111. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  112. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  113. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  114. package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
  115. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  116. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  117. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  118. package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
  119. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  120. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  121. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  122. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  123. package/lib/types/components/sessions/SessionListRow.js +4 -2
  124. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  125. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  126. package/lib/types/components/trajectory/HotspotView.js +8 -2
  127. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  128. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  129. package/lib/types/components/trajectory/Ledger.js +23 -5
  130. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  131. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  132. package/lib/types/components/trajectory/WaveBand.js +7 -2
  133. package/lib/types/dsh-adapter/channel.d.ts +27 -6
  134. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/channel.js +432 -66
  136. package/lib/types/dsh-adapter/contract.d.ts +58 -18
  137. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/contract.js +124 -36
  139. package/lib/types/dsh-adapter/index.d.ts +10 -2
  140. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  141. package/lib/types/dsh-adapter/index.js +10 -13
  142. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  143. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  144. package/lib/types/dsh-adapter/plugin.js +136 -6
  145. package/lib/types/i18n.d.ts +194 -16
  146. package/lib/types/i18n.d.ts.map +1 -1
  147. package/lib/types/i18n.js +73 -10
  148. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  149. package/lib/types/ink/components/AlternateScreen.js +13 -1
  150. package/lib/types/ink/components/App.d.ts +12 -1
  151. package/lib/types/ink/components/App.d.ts.map +1 -1
  152. package/lib/types/ink/components/App.js +101 -4
  153. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  154. package/lib/types/ink/components/ClockContext.js +2 -0
  155. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  156. package/lib/types/ink/components/ScrollBox.js +170 -91
  157. package/lib/types/ink/constants.d.ts +9 -0
  158. package/lib/types/ink/constants.d.ts.map +1 -1
  159. package/lib/types/ink/constants.js +9 -0
  160. package/lib/types/ink/events/click-event.d.ts +7 -16
  161. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  162. package/lib/types/ink/events/click-event.js +16 -19
  163. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  164. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  165. package/lib/types/ink/events/event-handlers.js +2 -0
  166. package/lib/types/ink/events/input-event.d.ts +9 -0
  167. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  168. package/lib/types/ink/events/input-event.js +4 -0
  169. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  170. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  171. package/lib/types/ink/events/pointer-event.js +61 -0
  172. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  173. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  174. package/lib/types/ink/events/wheel-event.js +20 -0
  175. package/lib/types/ink/geometry-trace.d.ts +51 -0
  176. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  177. package/lib/types/ink/geometry-trace.js +83 -0
  178. package/lib/types/ink/hit-test.d.ts +56 -1
  179. package/lib/types/ink/hit-test.d.ts.map +1 -1
  180. package/lib/types/ink/hit-test.js +146 -8
  181. package/lib/types/ink/ink.d.ts +58 -1
  182. package/lib/types/ink/ink.d.ts.map +1 -1
  183. package/lib/types/ink/ink.js +268 -52
  184. package/lib/types/ink/node-cache.d.ts +5 -0
  185. package/lib/types/ink/node-cache.d.ts.map +1 -1
  186. package/lib/types/ink/parse-keypress.d.ts +8 -0
  187. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  188. package/lib/types/ink/parse-keypress.js +101 -12
  189. package/lib/types/ink/render-node-to-output.d.ts +16 -3
  190. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  191. package/lib/types/ink/render-node-to-output.js +144 -25
  192. package/lib/types/ink/selection.d.ts +29 -4
  193. package/lib/types/ink/selection.d.ts.map +1 -1
  194. package/lib/types/ink/selection.js +41 -5
  195. package/lib/types/ink/timeline-rail.d.ts +138 -0
  196. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  197. package/lib/types/ink/timeline-rail.js +179 -0
  198. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  199. package/lib/types/ink/wrap-text.js +47 -5
  200. package/lib/types/screens/Chat.d.ts.map +1 -1
  201. package/lib/types/screens/Chat.js +819 -337
  202. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  203. package/lib/types/screens/SessionBrowser.js +56 -29
  204. package/lib/types/screens/Settings.d.ts.map +1 -1
  205. package/lib/types/screens/Settings.js +64 -22
  206. package/lib/types/screens/StatusLine.d.ts +2 -1
  207. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  208. package/lib/types/screens/StatusLine.js +17 -1
  209. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  210. package/lib/types/screens/TrajectoryScene.js +81 -14
  211. package/lib/types/screens/chatOverlay.d.ts +221 -0
  212. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  213. package/lib/types/screens/chatOverlay.js +114 -0
  214. package/lib/types/tips.d.ts.map +1 -1
  215. package/lib/types/tips.js +64 -10
  216. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  217. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  218. package/lib/types/tuiDisplayPrefs.js +9 -0
  219. package/lib/types/utils/debug.d.ts +10 -0
  220. package/lib/types/utils/debug.d.ts.map +1 -1
  221. package/lib/types/utils/debug.js +24 -0
  222. package/lib/types/utils/fileSuggestions.d.ts +19 -0
  223. package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
  224. package/lib/types/utils/fileSuggestions.js +40 -0
  225. package/lib/types/utils/mentions.d.ts.map +1 -1
  226. package/lib/types/utils/mentions.js +3 -1
  227. package/lib/types/utils/paths.d.ts.map +1 -1
  228. package/lib/types/utils/paths.js +1 -0
  229. package/package.json +69 -66
@@ -9,12 +9,16 @@ import { EffortInputBorder } from './EffortInputBorder.js';
9
9
  import { EffortTierBadge } from './EffortTierBadge.js';
10
10
  import { isLightThemeActive } from '../theme.js';
11
11
  import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
12
+ import { noteAuxNumber } from '../ink/geometry-trace.js';
13
+ import instances from '../ink/instances.js';
12
14
  import { stringWidth } from '../ink/stringWidth.js';
15
+ import { getGraphemeSegmenter } from '../utils/intl.js';
13
16
  import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
14
17
  import { editInExternalEditor } from '../utils/externalEditor.js';
15
18
  import { isHiddenCommandName, parseCommandName } from '../commands.js';
16
19
  import { appendHistory } from '../history.js';
17
20
  import { mentionAtCaret } from '../utils/mentions.js';
21
+ import { preserveSelection } from '../utils/fileSuggestions.js';
18
22
  import { isMod } from '../utils/modifiers.js';
19
23
  import { CommandSuggestions } from './CommandSuggestions.js';
20
24
  import { FileSuggestions } from './FileSuggestions.js';
@@ -51,6 +55,84 @@ function wordBoundaryRight(text, cursor) {
51
55
  index++;
52
56
  return index;
53
57
  }
58
+ // --- grapheme-cluster geometry ---------------------------------------------
59
+ // The caret, editing keys, and wrapping MUST agree on one text unit. Mixing
60
+ // UTF-16 code units (arrows/backspace), code points (wrap), and display
61
+ // cells (stringWidth) lets the caret land inside a surrogate pair or a ZWJ
62
+ // emoji — the inverted caret then shows half a glyph, Backspace deletes
63
+ // half a character, and `line.slice()` splits clusters. All offsets below
64
+ // are UTF-16 indices snapped to grapheme boundaries via the shared
65
+ // Intl.Segmenter (utils/intl.ts).
66
+ /** Ascending grapheme boundary offsets of `text` (starts at 0, ends at
67
+ * `text.length`). Empty text yields `[0]`. */
68
+ function graphemeBoundaries(text) {
69
+ const bounds = [0];
70
+ for (const { index, segment } of getGraphemeSegmenter().segment(text)) {
71
+ const end = index + segment.length;
72
+ if (end > bounds[bounds.length - 1])
73
+ bounds.push(end);
74
+ }
75
+ return bounds;
76
+ }
77
+ /** Largest grapheme boundary `<= offset` (clamped into the text). Snaps a
78
+ * cursor that landed mid-cluster back onto a boundary. */
79
+ function boundaryAtOrBefore(bounds, offset) {
80
+ let lo = 0;
81
+ let hi = bounds.length - 1;
82
+ let ans = bounds[0];
83
+ while (lo <= hi) {
84
+ const mid = (lo + hi) >> 1;
85
+ if (bounds[mid] <= offset) {
86
+ ans = bounds[mid];
87
+ lo = mid + 1;
88
+ }
89
+ else {
90
+ hi = mid - 1;
91
+ }
92
+ }
93
+ return ans;
94
+ }
95
+ /** Largest grapheme boundary strictly before `offset` (0 when none). */
96
+ function previousGraphemeBoundary(bounds, offset) {
97
+ let lo = 0;
98
+ let hi = bounds.length - 1;
99
+ let ans = 0;
100
+ while (lo <= hi) {
101
+ const mid = (lo + hi) >> 1;
102
+ if (bounds[mid] < offset) {
103
+ ans = bounds[mid];
104
+ lo = mid + 1;
105
+ }
106
+ else {
107
+ hi = mid - 1;
108
+ }
109
+ }
110
+ return ans;
111
+ }
112
+ /** Smallest grapheme boundary strictly after `offset` (text.length when
113
+ * none). Returns `offset` unchanged when it already is the last boundary. */
114
+ function nextGraphemeBoundary(bounds, offset) {
115
+ let lo = 0;
116
+ let hi = bounds.length - 1;
117
+ let ans = bounds[hi] ?? 0;
118
+ while (lo <= hi) {
119
+ const mid = (lo + hi) >> 1;
120
+ if (bounds[mid] > offset) {
121
+ ans = bounds[mid];
122
+ hi = mid - 1;
123
+ }
124
+ else {
125
+ lo = mid + 1;
126
+ }
127
+ }
128
+ return ans;
129
+ }
130
+ /** Snap an arbitrary UTF-16 offset onto a grapheme boundary of `text`,
131
+ * clamping into range. The safety net under every cursor write. */
132
+ function normalizeCursorOffset(text, offset) {
133
+ const clamped = Math.max(0, Math.min(offset, text.length));
134
+ return boundaryAtOrBefore(graphemeBoundaries(text), clamped);
135
+ }
54
136
  /**
55
137
  * The empty input deliberately shows NO placeholder text: terminal emulators
56
138
  * paint the IME preedit (pinyin) at the physical cursor, which the app parks
@@ -65,9 +147,10 @@ const MAX_VISIBLE_LINES = 5;
65
147
  /**
66
148
  * Claude Code style prompt input: rounded border box (top+bottom borders
67
149
  * only), `❯ ` prompt char (dimmed while a turn is working), the text with a
68
- * block cursor at the cursor position, and below it the slash-command
69
- * suggestion overlay (name column + description, selected row in the
70
- * `suggestion` color — mirroring Claude Code's PromptInputFooterSuggestions).
150
+ * block cursor at the cursor position, and above it the slash-command /
151
+ * file-completion suggestion card (SuggestionCard: rounded panel with the
152
+ * selected row behind a `❯` pointer in the theme's `suggestion` color,
153
+ * mirroring Claude Code's PromptInputFooterSuggestions layout).
71
154
  *
72
155
  * Empty input: a solid block caret on a blank cell and nothing else — no
73
156
  * placeholder text, so the terminal-painted IME preedit (pinyin) at the
@@ -128,10 +211,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
128
211
  React.useEffect(() => {
129
212
  if (fillText && fillText !== lastFill.current) {
130
213
  lastFill.current = fillText;
131
- valueRef.current = fillText;
132
- cursorRef.current = fillText.length;
133
- setValue(fillText);
134
- setCursor(fillText.length);
214
+ setInput(fillText);
135
215
  onFillConsumed?.();
136
216
  }
137
217
  }, [fillText, onFillConsumed]);
@@ -152,10 +232,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
152
232
  }, []);
153
233
  const { columns, rows: terminalRows } = useTerminalSize();
154
234
  const helpScrollRef = React.useRef(null);
155
- // OverlayAbove reserves six terminal rows for the composer/status chrome;
156
- // the help block also keeps one row below it. Its own final row is a
157
- // persistent navigation hint, leaving the remainder to ScrollBox.
158
- const helpViewportHeight = Math.max(3, Math.max(terminalRows - 6, 4) - 1);
235
+ // Help viewport budget: the overlay anchors at the composer's top edge
236
+ // (OverlayAbove bottom:'100%') and grows UP, so its budget is the space
237
+ // ABOVE that anchor — smallest on an empty session, where the whale
238
+ // splash (~15 rows) sits between the screen top and the composer
239
+ // (terminalRows minus chrome only applies once the transcript fills the
240
+ // viewport). Take the conservative intersection: 15 rows of viewport (16
241
+ // with the hint + margin) fits the empty-session anchor on the default
242
+ // layout at any terminal size, and the renderer's bottom-anchored
243
+ // clipping for absolute overlays (no negative-y clamp) then never has
244
+ // to eat the overlay's FIRST rows — the shortcut-column headers. The
245
+ // command registry scrolls inside the viewport, so a taller terminal
246
+ // loses nothing functional. (PR #446; restored after the picker
247
+ // snapshot's cherry-pick resurrected the old formula.)
248
+ const helpViewportHeight = Math.max(3, Math.min(terminalRows - 7, 15));
159
249
  const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
160
250
  const overlayOpen = suggestions.length > 0 &&
161
251
  !helpOpen &&
@@ -165,29 +255,31 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
165
255
  // CARET, so `@` works mid-message (`看看 @src/a.ts 这个`), not only when it
166
256
  // is the input's first character. The cwd listing loads when the trigger
167
257
  // appears.
168
- const [fileList, setFileList] = React.useState([]);
258
+ const [fileMatches, setFileMatches] = React.useState([]);
169
259
  const [fileSelected, setFileSelected] = React.useState(0);
170
260
  const mention = mentionAtCaret(value, cursor);
171
261
  const atTrigger = mention !== undefined;
262
+ const fileRequestId = React.useRef(0);
263
+ const selectedFile = fileMatches[fileSelected];
172
264
  React.useEffect(() => {
173
- if (atTrigger) {
174
- void channel.listFiles().then(setFileList);
175
- }
176
- }, [atTrigger, channel]);
177
- const atRest = (mention?.query ?? '').toLowerCase();
178
- // Match the relative path prefix OR the basename (CC's IDE suggestions do
179
- // both): `@src/ink` and `@ink` both find `src/ink/Box.js`.
180
- const fileMatches = atTrigger
181
- ? fileList.filter(file => {
182
- const lower = file.toLowerCase();
183
- if (lower.startsWith(atRest))
184
- return true;
185
- if (atRest.includes('/'))
186
- return false;
187
- const base = lower.split('/').pop() ?? '';
188
- return base.startsWith(atRest);
189
- })
190
- : [];
265
+ const requestId = ++fileRequestId.current;
266
+ if (!mention) {
267
+ setFileMatches([]);
268
+ setFileSelected(0);
269
+ return;
270
+ }
271
+ const previous = selectedFile;
272
+ // Deps key on `mention.query` (and trigger on/off) only: cursor movement
273
+ // within the same token must NOT refetch, and `selectedFile`/`fileSelected`
274
+ // are read as their render-time values only to seed selection preservation.
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ void channel.listFileCandidates(mention.query, { topK: 50 }).then(next => {
277
+ if (requestId !== fileRequestId.current)
278
+ return;
279
+ setFileMatches(next);
280
+ setFileSelected(preserveSelection(previous, next, fileSelected));
281
+ });
282
+ }, [channel, mention?.query, atTrigger]);
191
283
  // Esc dismisses the overlay for the token being edited (it reopens once the
192
284
  // text changes); it must NOT clear a mid-message input.
193
285
  const fileEscRef = React.useRef(-1);
@@ -200,7 +292,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
200
292
  fileEscRef.current !== mention?.start;
201
293
  const setInput = (next, cursorOffset = next.length) => {
202
294
  valueRef.current = next;
203
- cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
295
+ // Normalize onto a grapheme boundary (also clamps into range): every
296
+ // caller passes a caret they believe is on a character edge — paste
297
+ // merges, history fills, and IME composition can still hand back an
298
+ // offset inside a surrogate pair or combining cluster.
299
+ cursorRef.current = normalizeCursorOffset(next, cursorOffset);
204
300
  setValue(next);
205
301
  setCursor(cursorRef.current);
206
302
  };
@@ -210,11 +306,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
210
306
  * directory inserts `@dir/` without a trailing space so completion
211
307
  * continues into it; a file completes the token with a space.
212
308
  */
213
- const acceptFile = (file) => {
309
+ const acceptFile = (candidate) => {
214
310
  if (!mention)
215
311
  return;
312
+ const file = candidate.path;
216
313
  const body = /\s/.test(file) ? `@"${file}"` : `@${file}`;
217
- const insert = file.endsWith('/') ? body : `${body} `;
314
+ const insert = candidate.kind === 'directory' ? body : `${body} `;
218
315
  const next = value.slice(0, mention.start) + insert + value.slice(mention.end);
219
316
  setInput(next, mention.start + insert.length);
220
317
  setFileSelected(0);
@@ -384,6 +481,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
384
481
  // the text/caret produced by the preceding event in that batch.
385
482
  const value = valueRef.current;
386
483
  const cursor = cursorRef.current;
484
+ // Grapheme boundaries of the current text: every caret move / delete
485
+ // below snaps onto one of these offsets (never mid-cluster).
486
+ const bounds = graphemeBoundaries(value);
387
487
  /** Insert text at the caret (typing, paste) and dismiss overlays. */
388
488
  const insertAtCaret = (text) => {
389
489
  if (helpOpen)
@@ -747,23 +847,27 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
747
847
  return;
748
848
  }
749
849
  if (key.leftArrow) {
750
- setInput(value, Math.max(0, cursor - 1));
850
+ // Grapheme-step: skip the whole cluster (surrogate pair, ZWJ emoji,
851
+ // combining mark) so the caret never sits inside one.
852
+ setInput(value, previousGraphemeBoundary(bounds, cursor));
751
853
  return;
752
854
  }
753
855
  if (key.rightArrow) {
754
- setInput(value, Math.min(value.length, cursor + 1));
856
+ setInput(value, nextGraphemeBoundary(bounds, cursor));
755
857
  return;
756
858
  }
757
859
  if (key.backspace) {
758
860
  if (cursor === 0)
759
861
  return;
760
- setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
862
+ const start = previousGraphemeBoundary(bounds, cursor);
863
+ setInput(value.slice(0, start) + value.slice(cursor), start);
761
864
  return;
762
865
  }
763
866
  if (key.delete) {
764
- if (cursor >= value.length)
867
+ const end = nextGraphemeBoundary(bounds, cursor);
868
+ if (end === cursor)
765
869
  return;
766
- setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
870
+ setInput(value.slice(0, cursor) + value.slice(end), cursor);
767
871
  return;
768
872
  }
769
873
  if (key.home) {
@@ -890,7 +994,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
890
994
  // === Render: hard-wrap every logical line at the input width, then show
891
995
  // the window of visual lines with the caret row always visible (CC's
892
996
  // maxVisibleLines behavior with automatic wrapping).
893
- const inputWidth = Math.max(10, columns - 3);
997
+ // Narrow terminals: the usable width follows the real terminal down to a
998
+ // single column — a fixed floor of 10 would wrap far too early and park
999
+ // the declared cursor past the value box's actual width.
1000
+ const inputWidth = Math.max(1, columns - 3);
894
1001
  const visualLines = wrapToWidth(value, inputWidth);
895
1002
  const caretVisualLine = wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
896
1003
  const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
@@ -918,13 +1025,38 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
918
1025
  if (absoluteLine !== caretVisualLine) {
919
1026
  return (_jsx(Text, { wrap: "truncate-end", children: line }, absoluteLine));
920
1027
  }
921
- // Caret row: invert the char at the caret column (solid block).
1028
+ // Caret row: invert the grapheme cluster at the caret column (solid
1029
+ // block). `col` is a cluster boundary by construction (the cursor is
1030
+ // normalized onto boundaries and wrapping only breaks between
1031
+ // graphemes), so [col, next boundary) covers the WHOLE cluster — a
1032
+ // surrogate pair or ZWJ emoji inverts as one glyph, never two broken
1033
+ // halves.
922
1034
  const col = caretCharCol();
1035
+ const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(line), col);
923
1036
  const before = line.slice(0, col);
924
- const at = line[col] ?? ' ';
925
- const after = line.slice(col + 1);
1037
+ const at = clusterEnd > col ? line.slice(col, clusterEnd) : ' ';
1038
+ const after = line.slice(clusterEnd);
926
1039
  return (_jsxs(Text, { wrap: "truncate-end", children: [before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
927
1040
  });
1041
+ // Composer height shrink: clearing multi-line text (Enter/Esc/Ctrl+C/
1042
+ // Backspace) collapses the input area within one commit, shifting the
1043
+ // status line up and the whole chrome with it. The renderer's
1044
+ // full-damage pass (didLayoutShift) repaints the shifted siblings, but
1045
+ // inline mode's virtual↔scrollback correspondence needs the stronger
1046
+ // in-place viewport repaint — same treatment as Ctrl+O and the
1047
+ // loaded-context toggle (see Chat.tsx). One-shot, only on SHRINK:
1048
+ // growth scrolls the terminal naturally and needs no recovery.
1049
+ const contentRows = value.length === 0 ? 1 : visibleLines.length;
1050
+ noteAuxNumber('promptContentRows', contentRows);
1051
+ const prevContentRowsRef = React.useRef(contentRows);
1052
+ React.useLayoutEffect(() => {
1053
+ if (contentRows < prevContentRowsRef.current) {
1054
+ const ink = instances.get(process.stdout) ?? instances.values().next().value;
1055
+ ink?.invalidatePrevFrame();
1056
+ ink?.reanchorViewport();
1057
+ }
1058
+ prevContentRowsRef.current = contentRows;
1059
+ }, [contentRows]);
928
1060
  const lastNotification = channel.notifications[channel.notifications.length - 1];
929
1061
  // Park the native terminal cursor at the input caret (via the renderer's
930
1062
  // cursor-declaration mechanism). Terminal emulators render IME preedit
@@ -935,27 +1067,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
935
1067
  // relative to the value box the ref attaches to.
936
1068
  const valueBoxRef = useDeclaredCursor({
937
1069
  line: caretVisualLine - windowStart,
938
- column: caretVisualCol(),
1070
+ // Clamp the declared column to the wrap width: a grapheme wider than
1071
+ // the last remaining column (emoji at width 1) can push the visual
1072
+ // column past inputWidth, and the park must stay inside the value box.
1073
+ column: Math.min(caretVisualCol(), inputWidth),
939
1074
  active: !selectionActive,
940
1075
  });
1076
+ /**
1077
+ * Click-to-position the caret: map a click inside the value box (local
1078
+ * row/column relative to the box) to a UTF-16 cursor offset via the same
1079
+ * grapheme walk the renderer wraps with — exact under CJK widths, wrapped
1080
+ * rows and multi-codepoint clusters. Clicks land on the boundary nearest
1081
+ * the clicked cell (mid-grapheme snaps to its start).
1082
+ */
1083
+ const handleValueClick = React.useCallback((e) => {
1084
+ const clickedVisual = windowStart + e.localRow;
1085
+ const clamped = Math.max(0, Math.min(clickedVisual, visualLines.length - 1));
1086
+ setCursor(clickToCursorOffset(value, inputWidth, clamped, e.localCol));
1087
+ }, [windowStart, visualLines.length, value, inputWidth]);
941
1088
  // 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
942
1089
  // absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
943
1090
  // 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
944
1091
  // 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
945
1092
  const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
946
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [floatersOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 6, 4), children: [helpOpen && (_jsx(Box, { marginBottom: 1, children: _jsx(HelpMenu, { commands: channel.commandList, viewportHeight: helpViewportHeight, viewportWidth: columns, scrollRef: helpScrollRef }) })), !helpOpen && channel.pending.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingBottom: 1, children: [channel.pending.some(item => item.placement === 'steer') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u26A1 ", t('input-pending-steer-label')] }), channel.pending
1093
+ // 补全卡片边框与输入框 idle 边框同色(plan 模式下整套面板一起变 sage 绿)。
1094
+ const promptAccent = channel.mode.plan === true ? 'planMode' : 'promptBorder';
1095
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [floatersOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 6, 1), children: [helpOpen && (_jsx(Box, { marginBottom: 1, children: _jsx(HelpMenu, { commands: channel.commandList, viewportHeight: helpViewportHeight, viewportWidth: columns, scrollRef: helpScrollRef, onCommandPick: (name) => {
1096
+ // 点击命令行 = 填入 /name 并关闭帮助(Tab 补全的鼠标等价)
1097
+ setInput(`/${name} `);
1098
+ onToggleHelp();
1099
+ } }) })), !helpOpen && channel.pending.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingBottom: 1, children: [channel.pending.some(item => item.placement === 'steer') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u26A1 ", t('input-pending-steer-label')] }), channel.pending
947
1100
  .filter(item => item.placement === 'steer')
948
1101
  .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), channel.pending.some(item => item.placement === 'followup') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u23F3 ", t('input-pending-queue-label')] }), channel.pending
949
1102
  .filter(item => item.placement === 'followup')
950
- .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(Box, { paddingLeft: 2, paddingBottom: 1, children: _jsx(FileSuggestions, { files: fileMatches, selectedIndex: fileSelected, columns: columns }) })), overlayOpen && (_jsx(Box, { paddingLeft: 2, paddingBottom: 1, children: _jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns }) }))] })), lastNotification && (_jsx(Box, { position: "absolute", marginTop: -1, height: 1, width: "100%", paddingLeft: 2, paddingRight: 1, flexDirection: "column", justifyContent: "flex-end", overflow: "hidden", children: _jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { color: lastNotification.color, dimColor: !lastNotification.color, wrap: "truncate", children: lastNotification.text }) }) })), _jsx(EffortInputBorder, { effort: channel.reasoningEffort, levels: channel.effortLevels, columns: columns, onLight: isLightThemeActive(themeName), idleColor: channel.mode.plan === true ? 'planMode' : 'promptBorder', children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [_jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), _jsx(Box, { ref: valueBoxRef, flexGrow: 1, flexShrink: 1, children: value.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(EffortTierBadge, { effort: channel.reasoningEffort, levels: channel.effortLevels, onLight: isLightThemeActive(themeName), columns: columns, leadingColumns: 3 })] })) : (_jsx(Box, { flexDirection: "column", children: rendered })) })] }) })] }));
1103
+ .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(FileSuggestions, { files: fileMatches, selectedIndex: fileSelected, columns: columns, query: mention?.query ?? '', accent: promptAccent,
1104
+ // 点击行 = 接受该项(与 Enter 同路径)
1105
+ onPick: (index) => {
1106
+ const file = fileMatches[index];
1107
+ if (file)
1108
+ acceptFile(file);
1109
+ },
1110
+ // 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
1111
+ onWheelStep: (step) => {
1112
+ setFileSelected(i => Math.max(0, Math.min(fileMatches.length - 1, i + step)));
1113
+ } })), overlayOpen && (_jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns, query: value, accent: promptAccent,
1114
+ // 点击行 = 运行该命令(与 Enter 同路径)
1115
+ onPick: (index) => {
1116
+ const command = suggestions[index];
1117
+ if (command)
1118
+ tryRunCommand(command.commandLine);
1119
+ },
1120
+ // 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
1121
+ onWheelStep: (step) => {
1122
+ setSelectedCommand(i => Math.max(0, Math.min(suggestions.length - 1, i + step)));
1123
+ } }))] })), lastNotification && (_jsx(Box, { position: "absolute", marginTop: -1, height: 1, width: "100%", paddingLeft: 2, paddingRight: 1, flexDirection: "column", justifyContent: "flex-end", overflow: "hidden", children: _jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { color: lastNotification.color, dimColor: !lastNotification.color, wrap: "truncate", children: lastNotification.text }) }) })), _jsx(EffortInputBorder, { effort: channel.reasoningEffort, levels: channel.effortLevels, columns: columns, onLight: isLightThemeActive(themeName), idleColor: promptAccent, children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [_jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), _jsx(Box, { ref: valueBoxRef, flexGrow: 1, flexShrink: 1, onClick: handleValueClick, children: value.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(EffortTierBadge, { effort: channel.reasoningEffort, levels: channel.effortLevels, onLight: isLightThemeActive(themeName), columns: columns, leadingColumns: 3 })] })) : (_jsx(Box, { flexDirection: "column", children: rendered })) })] }) })] }));
951
1124
  }
952
1125
  /**
953
1126
  * Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
954
1127
  * stringWidth). Used by the input renderer so long lines wrap instead of
955
- * truncating, with exact caret-row mapping.
1128
+ * truncating, with exact caret-row mapping. Wrapping only ever breaks
1129
+ * BETWEEN grapheme clusters: iterating code points would split ZWJ emoji
1130
+ * and combining sequences across rows, leaving a broken half at each edge
1131
+ * and desyncing the caret's row arithmetic (which walks cluster
1132
+ * boundaries).
956
1133
  */
957
1134
  function wrapToWidth(text, width) {
958
1135
  const rows = [];
1136
+ const segmenter = getGraphemeSegmenter();
959
1137
  for (const line of text.split('\n')) {
960
1138
  if (line === '') {
961
1139
  rows.push('');
@@ -963,15 +1141,15 @@ function wrapToWidth(text, width) {
963
1141
  }
964
1142
  let current = '';
965
1143
  let currentWidth = 0;
966
- for (const ch of line) {
967
- const w = stringWidth(ch);
1144
+ for (const { segment } of segmenter.segment(line)) {
1145
+ const w = stringWidth(segment);
968
1146
  if (currentWidth + w > width && current !== '') {
969
1147
  rows.push(current);
970
- current = ch;
1148
+ current = segment;
971
1149
  currentWidth = w;
972
1150
  }
973
1151
  else {
974
- current += ch;
1152
+ current += segment;
975
1153
  currentWidth += w;
976
1154
  }
977
1155
  }
@@ -979,3 +1157,48 @@ function wrapToWidth(text, width) {
979
1157
  }
980
1158
  return rows;
981
1159
  }
1160
+ /**
1161
+ * Inverse of {@link wrapToWidth}: map a click position (visual row index +
1162
+ * visual column) back to a UTF-16 offset in the original text. Walks the
1163
+ * same grapheme boundaries with the same break rule, so every visual row's
1164
+ * start offset is known exactly. Within the clicked row, the caret snaps to
1165
+ * the boundary nearest the click: a grapheme whose midpoint lies past the
1166
+ * click column takes the caret before it, otherwise after.
1167
+ */
1168
+ function clickToCursorOffset(text, width, visualLine, visualCol) {
1169
+ const segmenter = getGraphemeSegmenter();
1170
+ let row = 0;
1171
+ let offset = 0;
1172
+ for (const line of text.split('\n')) {
1173
+ if (line === '') {
1174
+ if (row === visualLine)
1175
+ return offset;
1176
+ row++;
1177
+ offset++; // the '\n'
1178
+ continue;
1179
+ }
1180
+ let currentWidth = 0;
1181
+ for (const { segment } of segmenter.segment(line)) {
1182
+ const w = stringWidth(segment);
1183
+ if (currentWidth + w > width && currentWidth > 0) {
1184
+ if (row === visualLine)
1185
+ return offset; // end of the clicked wrapped row
1186
+ row++;
1187
+ currentWidth = 0;
1188
+ }
1189
+ if (row === visualLine) {
1190
+ if (currentWidth + w / 2 > visualCol)
1191
+ return offset;
1192
+ if (currentWidth + w > visualCol)
1193
+ return offset + segment.length;
1194
+ }
1195
+ currentWidth += w;
1196
+ offset += segment.length;
1197
+ }
1198
+ if (row === visualLine)
1199
+ return offset;
1200
+ row++;
1201
+ offset++; // the '\n'
1202
+ }
1203
+ return offset;
1204
+ }
@@ -12,7 +12,7 @@ import type { TuiRewindMode } from '../dsh-adapter/extension-events.js';
12
12
  * a choice list whose option zero is always the built-in conversation-only
13
13
  * rewind, followed by the plugin's modes (e.g. "also restore files").
14
14
  */
15
- export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, modeIndex, busy, }: {
15
+ export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, modeIndex, busy, onPickRow, onConfirm, onPickMode, }: {
16
16
  rows: readonly ChatRow[];
17
17
  focusIndex: number;
18
18
  confirmRow: ChatRow | null;
@@ -22,5 +22,16 @@ export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, mode
22
22
  modeIndex?: number;
23
23
  /** True while the plugin decision is in flight. */
24
24
  busy?: boolean;
25
+ /**
26
+ * Mouse pick on a list row (fullscreen): sets focus only — rewind is a
27
+ * high-risk invisible-confirm operation, so stepping into the confirm
28
+ * state stays an explicit keyboard Enter.
29
+ */
30
+ onPickRow?: (index: number) => void;
31
+ /** Mouse click on the plain confirm row: executes the rewind directly —
32
+ * the confirm pane itself is the explicit confirmation layer. */
33
+ onConfirm?: () => void;
34
+ /** Mouse pick on a modes-list option: executes that mode directly. */
35
+ onPickMode?: (index: number) => void;
25
36
  }): React.ReactNode;
26
37
  //# sourceMappingURL=RewindPicker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RewindPicker.d.ts","sourceRoot":"","sources":["../../../src/components/RewindPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AAMvE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,UAAU,EACV,UAAU,EACV,KAAY,EACZ,SAAa,EACb,IAAY,GACb,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAA;IAC1B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAA;IACvC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,GAAG,KAAK,CAAC,SAAS,CA4GlB"}
1
+ {"version":3,"file":"RewindPicker.d.ts","sourceRoot":"","sources":["../../../src/components/RewindPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AAMvE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,UAAU,EACV,UAAU,EACV,KAAY,EACZ,SAAa,EACb,IAAY,EACZ,SAAS,EACT,SAAS,EACT,UAAU,GACX,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAA;IAC1B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAA;IACvC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;sEACkE;IAClE,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,GAAG,KAAK,CAAC,SAAS,CAkHlB"}
@@ -16,7 +16,7 @@ import { listWindow } from './listWindow.js';
16
16
  * a choice list whose option zero is always the built-in conversation-only
17
17
  * rewind, followed by the plugin's modes (e.g. "also restore files").
18
18
  */
19
- export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeIndex = 0, busy = false, }) {
19
+ export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeIndex = 0, busy = false, onPickRow, onConfirm, onPickMode, }) {
20
20
  if (confirmRow !== null) {
21
21
  if (modes !== null) {
22
22
  // Plugin modes: a described choice list (one extra row per
@@ -34,10 +34,10 @@ export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeI
34
34
  ];
35
35
  return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }), _jsx(Text, { dimColor: true, children: preview(confirmRow.text) })] }), options.slice(start, end).map((option, index) => {
36
36
  const absoluteIndex = start + index;
37
- return (_jsx(ListItem, { isFocused: absoluteIndex === modeIndex, description: option.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < options.length, children: option.label }, option.key));
37
+ return (_jsx(ListItem, { isFocused: absoluteIndex === modeIndex, description: option.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < options.length, onClick: onPickMode === undefined ? undefined : () => onPickMode(absoluteIndex), children: option.label }, option.key));
38
38
  })] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-select-exit') }) })] }));
39
39
  }
40
- return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }) }), _jsx(ListItem, { isFocused: false, description: t('rewind-confirm-desc'), children: preview(confirmRow.text) }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-rewind-back') }) })] }) }));
40
+ return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }) }), _jsx(ListItem, { isFocused: false, description: t('rewind-confirm-desc'), onClick: onConfirm, children: preview(confirmRow.text) }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-rewind-back') }) })] }) }));
41
41
  }
42
42
  const { rows: terminalRows } = useTerminalSize();
43
43
  // 焦点窗口化按行预算:首项带 'last message' 描述占 2 行、其余 1 行
@@ -46,7 +46,9 @@ export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeI
46
46
  const { start, end } = listWindow(rows.map((_, i) => (i === 0 ? 2 : 1)), focusIndex, Math.max(terminalRows - 14, 2));
47
47
  return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "remember", bold: true, children: t('rewind-title') }), _jsx(Text, { dimColor: true, children: t('rewind-subtitle') })] }), rows.length === 0 ? (_jsx(ListItem, { isFocused: false, children: t('rewind-empty') })) : (rows.slice(start, end).map((row, index) => {
48
48
  const absoluteIndex = start + index;
49
- return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex, description: absoluteIndex === 0 ? t('rewind-last-message') : undefined, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < rows.length, children: preview(row.text) }, row.id));
49
+ return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex, description: absoluteIndex === 0 ? t('rewind-last-message') : undefined, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < rows.length, onClick: onPickRow !== undefined && !busy
50
+ ? () => onPickRow(absoluteIndex)
51
+ : undefined, children: preview(row.text) }, row.id));
50
52
  })), busy && _jsx(Text, { dimColor: true, children: t('rewind-waiting-plugins') })] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-select-exit') }) })] }));
51
53
  }
52
54
  /** One-line preview of a message (newlines flattened, capped). */
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { type ScrollBoxHandle } from '../ui.js';
3
+ /**
4
+ * Proportional scrollbar for the fullscreen transcript's gutter — the
5
+ * `scrollbar` option of the `dsh-tui.scrollGutter` setting (the timeline
6
+ * rail's sibling; same 2-column slot, same chrome rules):
7
+ *
8
+ * - the thumb (██) shows the visible window's position AND size over the
9
+ * whole content (viewport²/content tall, positioned by scrollTop);
10
+ * - clicking the track scrolls the clicked position to the viewport top
11
+ * (classic scrollbar semantics — the thumb centers under the click
12
+ * through the follow-up renders);
13
+ * - the gutter is permanent while scrollable (Qwen's rule: an
14
+ * auto-hiding gutter that changes content width rewraps everything);
15
+ * hidden below 60 terminal columns or when the content fits (inline
16
+ * mode keeps the terminal's native scrollback);
17
+ * - NoSelect fences the glyphs out of click-drag text selection; the
18
+ * wheel over the gutter scrolls the transcript.
19
+ *
20
+ * Thumb dragging is intentionally NOT implemented (Grok's MVP rule): a
21
+ * proportional thumb encodes position, and the tick rail remains the
22
+ * semantic navigator. Click-to-position covers the same need.
23
+ */
24
+ export declare function ScrollbarGutter({ handle, terminalWidth, }: {
25
+ handle: ScrollBoxHandle | null;
26
+ terminalWidth: number;
27
+ }): React.ReactNode;
28
+ //# sourceMappingURL=ScrollbarGutter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollbarGutter.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollbarGutter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAOpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,KAAK,CAAC,SAAS,CA2DlB"}