@deepseek-harness-tui/dsh-tui 0.8.8 → 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 (215) hide show
  1. package/README.md +27 -8
  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 +24 -7
  21. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  22. package/lib/types/components/FileSuggestions.js +39 -12
  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/Markdown.d.ts.map +1 -1
  38. package/lib/types/components/Markdown.js +12 -3
  39. package/lib/types/components/MessageList.d.ts +43 -2
  40. package/lib/types/components/MessageList.d.ts.map +1 -1
  41. package/lib/types/components/MessageList.js +553 -41
  42. package/lib/types/components/ModelPicker.d.ts +4 -1
  43. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  44. package/lib/types/components/ModelPicker.js +2 -2
  45. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  46. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  47. package/lib/types/components/PermissionsPicker.js +27 -0
  48. package/lib/types/components/PlanPicker.d.ts +15 -0
  49. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  50. package/lib/types/components/PlanPicker.js +18 -0
  51. package/lib/types/components/PresetPicker.d.ts +4 -1
  52. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  53. package/lib/types/components/PresetPicker.js +2 -2
  54. package/lib/types/components/PromptInput.d.ts +4 -3
  55. package/lib/types/components/PromptInput.d.ts.map +1 -1
  56. package/lib/types/components/PromptInput.js +248 -29
  57. package/lib/types/components/RewindPicker.d.ts +12 -1
  58. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  59. package/lib/types/components/RewindPicker.js +6 -4
  60. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  61. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  62. package/lib/types/components/ScrollbarGutter.js +68 -0
  63. package/lib/types/components/Select.d.ts +8 -1
  64. package/lib/types/components/Select.d.ts.map +1 -1
  65. package/lib/types/components/Select.js +4 -2
  66. package/lib/types/components/SkillsPicker.d.ts +4 -1
  67. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  68. package/lib/types/components/SkillsPicker.js +2 -2
  69. package/lib/types/components/SubagentCard.d.ts +2 -1
  70. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  71. package/lib/types/components/SubagentCard.js +6 -1
  72. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  73. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  74. package/lib/types/components/SubagentDashboard.js +9 -1
  75. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  76. package/lib/types/components/SubagentDetailScene.js +5 -4
  77. package/lib/types/components/SuggestionCard.d.ts +63 -0
  78. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  79. package/lib/types/components/SuggestionCard.js +77 -0
  80. package/lib/types/components/ThemePicker.d.ts +4 -1
  81. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  82. package/lib/types/components/ThemePicker.js +2 -2
  83. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  84. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  85. package/lib/types/components/ThinkingToggle.js +2 -2
  86. package/lib/types/components/TimelineRail.d.ts +53 -0
  87. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  88. package/lib/types/components/TimelineRail.js +190 -0
  89. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  90. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  91. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  92. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  93. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  94. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  95. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  96. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  97. package/lib/types/components/WorkspacePicker.js +2 -2
  98. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  99. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  100. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  101. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  102. package/lib/types/components/design-system/ListItem.js +8 -3
  103. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  104. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  105. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  106. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  107. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  108. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  109. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  110. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  111. package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
  112. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  113. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  114. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  115. package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
  116. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  117. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  118. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  119. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  120. package/lib/types/components/sessions/SessionListRow.js +4 -2
  121. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  122. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  123. package/lib/types/components/trajectory/HotspotView.js +8 -2
  124. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  125. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  126. package/lib/types/components/trajectory/Ledger.js +23 -5
  127. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  128. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  129. package/lib/types/components/trajectory/WaveBand.js +7 -2
  130. package/lib/types/dsh-adapter/channel.d.ts +16 -5
  131. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  132. package/lib/types/dsh-adapter/channel.js +332 -29
  133. package/lib/types/dsh-adapter/index.d.ts +10 -2
  134. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/index.js +4 -1
  136. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  137. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/plugin.js +136 -6
  139. package/lib/types/i18n.d.ts +176 -15
  140. package/lib/types/i18n.d.ts.map +1 -1
  141. package/lib/types/i18n.js +51 -8
  142. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  143. package/lib/types/ink/components/AlternateScreen.js +13 -1
  144. package/lib/types/ink/components/App.d.ts +12 -1
  145. package/lib/types/ink/components/App.d.ts.map +1 -1
  146. package/lib/types/ink/components/App.js +101 -4
  147. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  148. package/lib/types/ink/components/ClockContext.js +2 -0
  149. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  150. package/lib/types/ink/components/ScrollBox.js +170 -91
  151. package/lib/types/ink/constants.d.ts +9 -0
  152. package/lib/types/ink/constants.d.ts.map +1 -1
  153. package/lib/types/ink/constants.js +9 -0
  154. package/lib/types/ink/events/click-event.d.ts +7 -16
  155. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  156. package/lib/types/ink/events/click-event.js +16 -19
  157. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  158. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  159. package/lib/types/ink/events/event-handlers.js +2 -0
  160. package/lib/types/ink/events/input-event.d.ts +9 -0
  161. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  162. package/lib/types/ink/events/input-event.js +4 -0
  163. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  164. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  165. package/lib/types/ink/events/pointer-event.js +61 -0
  166. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  167. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  168. package/lib/types/ink/events/wheel-event.js +20 -0
  169. package/lib/types/ink/geometry-trace.d.ts +51 -0
  170. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  171. package/lib/types/ink/geometry-trace.js +83 -0
  172. package/lib/types/ink/hit-test.d.ts +56 -1
  173. package/lib/types/ink/hit-test.d.ts.map +1 -1
  174. package/lib/types/ink/hit-test.js +146 -8
  175. package/lib/types/ink/ink.d.ts +58 -1
  176. package/lib/types/ink/ink.d.ts.map +1 -1
  177. package/lib/types/ink/ink.js +230 -29
  178. package/lib/types/ink/node-cache.d.ts +5 -0
  179. package/lib/types/ink/node-cache.d.ts.map +1 -1
  180. package/lib/types/ink/parse-keypress.d.ts +8 -0
  181. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  182. package/lib/types/ink/parse-keypress.js +101 -12
  183. package/lib/types/ink/render-node-to-output.d.ts +10 -0
  184. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  185. package/lib/types/ink/render-node-to-output.js +111 -18
  186. package/lib/types/ink/timeline-rail.d.ts +138 -0
  187. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  188. package/lib/types/ink/timeline-rail.js +179 -0
  189. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  190. package/lib/types/ink/wrap-text.js +47 -5
  191. package/lib/types/screens/Chat.d.ts.map +1 -1
  192. package/lib/types/screens/Chat.js +819 -337
  193. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  194. package/lib/types/screens/SessionBrowser.js +56 -29
  195. package/lib/types/screens/Settings.d.ts.map +1 -1
  196. package/lib/types/screens/Settings.js +64 -22
  197. package/lib/types/screens/StatusLine.d.ts +2 -1
  198. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  199. package/lib/types/screens/StatusLine.js +17 -1
  200. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  201. package/lib/types/screens/TrajectoryScene.js +81 -14
  202. package/lib/types/screens/chatOverlay.d.ts +221 -0
  203. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  204. package/lib/types/screens/chatOverlay.js +114 -0
  205. package/lib/types/tips.d.ts.map +1 -1
  206. package/lib/types/tips.js +42 -6
  207. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  208. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  209. package/lib/types/tuiDisplayPrefs.js +9 -0
  210. package/lib/types/utils/debug.d.ts +10 -0
  211. package/lib/types/utils/debug.d.ts.map +1 -1
  212. package/lib/types/utils/debug.js +24 -0
  213. package/lib/types/utils/paths.d.ts.map +1 -1
  214. package/lib/types/utils/paths.js +1 -0
  215. package/package.json +4 -2
@@ -9,9 +9,12 @@ import type { LlmModelInfo } from '../dsh-adapter/types.js';
9
9
  * 长列表按焦点窗口化(Select 同款):picker 经 OverlayAbove 浮层挂载后有
10
10
  * maxHeight 裁剪,全量渲染会让焦点行被裁掉(看不到焦点按 Enter)。
11
11
  */
12
- export declare function ModelPicker({ models, focusIndex, currentModel, }: {
12
+ export declare function ModelPicker({ models, focusIndex, currentModel, onPick, }: {
13
13
  models: readonly LlmModelInfo[];
14
14
  focusIndex: number;
15
15
  currentModel: string;
16
+ /** Mouse pick (fullscreen): reports the clicked row's absolute index —
17
+ * Chat applies it with the same code path as the keyboard Enter. */
18
+ onPick?: (index: number) => void;
16
19
  }): React.ReactNode;
17
20
  //# sourceMappingURL=ModelPicker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ModelPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ModelPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAM3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,UAAU,EACV,YAAY,GACb,EAAE;IACD,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CAuClB"}
1
+ {"version":3,"file":"ModelPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ModelPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAM3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,UAAU,EACV,YAAY,EACZ,MAAM,GACP,EAAE;IACD,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;yEACqE;IACrE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CAwClB"}
@@ -14,7 +14,7 @@ import { listWindow } from './listWindow.js';
14
14
  * 长列表按焦点窗口化(Select 同款):picker 经 OverlayAbove 浮层挂载后有
15
15
  * maxHeight 裁剪,全量渲染会让焦点行被裁掉(看不到焦点按 Enter)。
16
16
  */
17
- export function ModelPicker({ models, focusIndex, currentModel, }) {
17
+ export function ModelPicker({ models, focusIndex, currentModel, onPick, }) {
18
18
  const { rows: terminalRows } = useTerminalSize();
19
19
  // 焦点窗口化按行预算:ListItem 带 description 时占 2 行(正文+描述,均
20
20
  // truncate 成单行),只数项数会把焦点裁出浮层(二次审查实证)。
@@ -22,6 +22,6 @@ export function ModelPicker({ models, focusIndex, currentModel, }) {
22
22
  const { start, end } = listWindow(models.map(m => (m.description ? 2 : 1)), focusIndex, Math.max(terminalRows - 14, 2));
23
23
  return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('picker-title-model') }) }), models.slice(start, end).map((model, index) => {
24
24
  const absoluteIndex = start + index;
25
- return (_jsxs(ListItem, { isFocused: absoluteIndex === focusIndex, isSelected: `${model.provider}/${model.id}` === currentModel, description: model.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < models.length, children: [model.provider, " / ", model.name] }, `${model.provider}/${model.id}`));
25
+ return (_jsxs(ListItem, { isFocused: absoluteIndex === focusIndex, isSelected: `${model.provider}/${model.id}` === currentModel, description: model.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < models.length, onClick: onPick ? () => onPick(absoluteIndex) : undefined, children: [model.provider, " / ", model.name] }, `${model.provider}/${model.id}`));
26
26
  })] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }));
27
27
  }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ /**
3
+ * `/permission` sandbox-preset picker. The command itself is registered by
4
+ * the harness side (dsh-sandbox-policy / dsh-base permission-presets row);
5
+ * bare `/permission` opens this picker and Enter dispatches
6
+ * `/permission <preset>` through the same external-command path a hand-typed
7
+ * argument takes. The list mirrors the `sandbox/mode` vocabulary the session
8
+ * modes use (`sessionModes.ts`), so the ✓ mark stays consistent with the
9
+ * mode the TUI derives from the session log.
10
+ */
11
+ export declare const PERMISSION_PRESET_IDS: readonly ["read-only", "workspace-write", "danger-full-access"];
12
+ export type PermissionPresetId = (typeof PERMISSION_PRESET_IDS)[number];
13
+ export declare function PermissionsPicker({ focusIndex, currentMode, cwd, onPick, }: {
14
+ focusIndex: number;
15
+ currentMode: string | undefined;
16
+ cwd: string;
17
+ /** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
18
+ * the same code path as the keyboard Enter). */
19
+ onPick?: (index: number) => void;
20
+ }): React.ReactNode;
21
+ //# sourceMappingURL=PermissionsPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionsPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PermissionsPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,iEAIxB,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,WAAW,EACX,GAAG,EACH,MAAM,GACP,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CA4BlB"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { t } from '../i18n.js';
3
+ import { Box, Text } from '../ui.js';
4
+ import { Pane } from './design-system/Pane.js';
5
+ import { Select } from './Select.js';
6
+ import { HintLine } from './design-system/HintLine.js';
7
+ /**
8
+ * `/permission` sandbox-preset picker. The command itself is registered by
9
+ * the harness side (dsh-sandbox-policy / dsh-base permission-presets row);
10
+ * bare `/permission` opens this picker and Enter dispatches
11
+ * `/permission <preset>` through the same external-command path a hand-typed
12
+ * argument takes. The list mirrors the `sandbox/mode` vocabulary the session
13
+ * modes use (`sessionModes.ts`), so the ✓ mark stays consistent with the
14
+ * mode the TUI derives from the session log.
15
+ */
16
+ export const PERMISSION_PRESET_IDS = [
17
+ 'read-only',
18
+ 'workspace-write',
19
+ 'danger-full-access',
20
+ ];
21
+ export function PermissionsPicker({ focusIndex, currentMode, cwd, onPick, }) {
22
+ return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('permission-picker-title') }) }), _jsx(Select, { options: [
23
+ { value: 'read-only', label: t('permission-preset-readonly'), description: t('permission-preset-readonly-desc') },
24
+ { value: 'workspace-write', label: t('permission-preset-workspace-write'), description: t('permission-preset-workspace-write-desc') },
25
+ { value: 'danger-full-access', label: t('permission-preset-full-access'), description: t('permission-preset-full-access-desc') },
26
+ ], focusIndex: focusIndex, selectedValue: currentMode, onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) }), _jsx(Text, { dimColor: true, children: t('permission-root-hint', { cwd }) })] }) }));
27
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ /**
3
+ * `/plan` on/off picker. The command itself is registered by dsh-plan-mode
4
+ * on the harness side; bare `/plan` opens this picker (with the current
5
+ * state marked) and Enter dispatches `/plan` or `/plan off` through the
6
+ * same external-command path a hand-typed argument takes.
7
+ */
8
+ export declare function PlanPicker({ focusIndex, currentOn, onPick, }: {
9
+ focusIndex: number;
10
+ currentOn: boolean;
11
+ /** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
12
+ * the same code path as the keyboard Enter). */
13
+ onPick?: (index: number) => void;
14
+ }): React.ReactNode;
15
+ //# sourceMappingURL=PlanPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlanPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PlanPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,SAAS,EACT,MAAM,GACP,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CAwBlB"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { t } from '../i18n.js';
3
+ import { Box, Text } from '../ui.js';
4
+ import { Pane } from './design-system/Pane.js';
5
+ import { Select } from './Select.js';
6
+ import { HintLine } from './design-system/HintLine.js';
7
+ /**
8
+ * `/plan` on/off picker. The command itself is registered by dsh-plan-mode
9
+ * on the harness side; bare `/plan` opens this picker (with the current
10
+ * state marked) and Enter dispatches `/plan` or `/plan off` through the
11
+ * same external-command path a hand-typed argument takes.
12
+ */
13
+ export function PlanPicker({ focusIndex, currentOn, onPick, }) {
14
+ return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('plan-picker-title') }) }), _jsx(Select, { options: [
15
+ { value: 'on', label: t('plan-mode-on'), description: t('plan-mode-on-desc') },
16
+ { value: 'off', label: t('plan-mode-off'), description: t('plan-mode-off-desc') },
17
+ ], focusIndex: focusIndex, selectedValue: currentOn ? 'on' : 'off', onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
18
+ }
@@ -9,9 +9,12 @@ import type { PresetOption } from '../dsh-adapter/channel.js';
9
9
  * roster's discovery contract) but marked with their reason; the roster
10
10
  * default is tagged.
11
11
  */
12
- export declare function PresetPicker({ presets, focusIndex, currentPreset, }: {
12
+ export declare function PresetPicker({ presets, focusIndex, currentPreset, onPick, }: {
13
13
  presets: readonly PresetOption[];
14
14
  focusIndex: number;
15
15
  currentPreset: string | undefined;
16
+ /** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
17
+ * the same code path as the keyboard Enter). */
18
+ onPick?: (index: number) => void;
16
19
  }): React.ReactNode;
17
20
  //# sourceMappingURL=PresetPicker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PresetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PresetPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,UAAU,EACV,aAAa,GACd,EAAE;IACD,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;CAClC,GAAG,KAAK,CAAC,SAAS,CA2BlB"}
1
+ {"version":3,"file":"PresetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PresetPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,UAAU,EACV,aAAa,EACb,MAAM,GACP,EAAE;IACD,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CA4BlB"}
@@ -13,12 +13,12 @@ import { HintLine } from './design-system/HintLine.js';
13
13
  * roster's discovery contract) but marked with their reason; the roster
14
14
  * default is tagged.
15
15
  */
16
- export function PresetPicker({ presets, focusIndex, currentPreset, }) {
16
+ export function PresetPicker({ presets, focusIndex, currentPreset, onPick, }) {
17
17
  return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: "Agent preset" }) }), _jsx(Select, { options: presets.map(preset => ({
18
18
  value: preset.id,
19
19
  label: (preset.name ?? preset.id) +
20
20
  (preset.isDefault ? t('preset-default-tag') : '') +
21
21
  (preset.broken !== undefined ? t('preset-broken-tag') : ''),
22
22
  description: preset.broken ?? preset.description ?? preset.id,
23
- })), focusIndex: focusIndex, selectedValue: currentPreset }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
23
+ })), focusIndex: focusIndex, selectedValue: currentPreset, onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
24
24
  }
@@ -37,9 +37,10 @@ export interface PromptInputProps {
37
37
  /**
38
38
  * Claude Code style prompt input: rounded border box (top+bottom borders
39
39
  * only), `❯ ` prompt char (dimmed while a turn is working), the text with a
40
- * block cursor at the cursor position, and below it the slash-command
41
- * suggestion overlay (name column + description, selected row in the
42
- * `suggestion` color — mirroring Claude Code's PromptInputFooterSuggestions).
40
+ * block cursor at the cursor position, and above it the slash-command /
41
+ * file-completion suggestion card (SuggestionCard: rounded panel with the
42
+ * selected row behind a `❯` pointer in the theme's `suggestion` color,
43
+ * mirroring Claude Code's PromptInputFooterSuggestions layout).
43
44
  *
44
45
  * Empty input: a solid block caret on a blank cell and nothing else — no
45
46
  * placeholder text, so the terminal-painted IME preedit (pinyin) at the
@@ -1 +1 @@
1
- {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAazB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAmDxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAo/BlB"}
1
+ {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAmIxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAolClB"}
@@ -9,7 +9,10 @@ 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';
@@ -52,6 +55,84 @@ function wordBoundaryRight(text, cursor) {
52
55
  index++;
53
56
  return index;
54
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
+ }
55
136
  /**
56
137
  * The empty input deliberately shows NO placeholder text: terminal emulators
57
138
  * paint the IME preedit (pinyin) at the physical cursor, which the app parks
@@ -66,9 +147,10 @@ const MAX_VISIBLE_LINES = 5;
66
147
  /**
67
148
  * Claude Code style prompt input: rounded border box (top+bottom borders
68
149
  * only), `❯ ` prompt char (dimmed while a turn is working), the text with a
69
- * block cursor at the cursor position, and below it the slash-command
70
- * suggestion overlay (name column + description, selected row in the
71
- * `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).
72
154
  *
73
155
  * Empty input: a solid block caret on a blank cell and nothing else — no
74
156
  * placeholder text, so the terminal-painted IME preedit (pinyin) at the
@@ -129,10 +211,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
129
211
  React.useEffect(() => {
130
212
  if (fillText && fillText !== lastFill.current) {
131
213
  lastFill.current = fillText;
132
- valueRef.current = fillText;
133
- cursorRef.current = fillText.length;
134
- setValue(fillText);
135
- setCursor(fillText.length);
214
+ setInput(fillText);
136
215
  onFillConsumed?.();
137
216
  }
138
217
  }, [fillText, onFillConsumed]);
@@ -153,10 +232,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
153
232
  }, []);
154
233
  const { columns, rows: terminalRows } = useTerminalSize();
155
234
  const helpScrollRef = React.useRef(null);
156
- // OverlayAbove reserves six terminal rows for the composer/status chrome;
157
- // the help block also keeps one row below it. Its own final row is a
158
- // persistent navigation hint, leaving the remainder to ScrollBox.
159
- 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));
160
249
  const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
161
250
  const overlayOpen = suggestions.length > 0 &&
162
251
  !helpOpen &&
@@ -203,7 +292,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
203
292
  fileEscRef.current !== mention?.start;
204
293
  const setInput = (next, cursorOffset = next.length) => {
205
294
  valueRef.current = next;
206
- 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);
207
300
  setValue(next);
208
301
  setCursor(cursorRef.current);
209
302
  };
@@ -388,6 +481,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
388
481
  // the text/caret produced by the preceding event in that batch.
389
482
  const value = valueRef.current;
390
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);
391
487
  /** Insert text at the caret (typing, paste) and dismiss overlays. */
392
488
  const insertAtCaret = (text) => {
393
489
  if (helpOpen)
@@ -751,23 +847,27 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
751
847
  return;
752
848
  }
753
849
  if (key.leftArrow) {
754
- 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));
755
853
  return;
756
854
  }
757
855
  if (key.rightArrow) {
758
- setInput(value, Math.min(value.length, cursor + 1));
856
+ setInput(value, nextGraphemeBoundary(bounds, cursor));
759
857
  return;
760
858
  }
761
859
  if (key.backspace) {
762
860
  if (cursor === 0)
763
861
  return;
764
- 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);
765
864
  return;
766
865
  }
767
866
  if (key.delete) {
768
- if (cursor >= value.length)
867
+ const end = nextGraphemeBoundary(bounds, cursor);
868
+ if (end === cursor)
769
869
  return;
770
- setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
870
+ setInput(value.slice(0, cursor) + value.slice(end), cursor);
771
871
  return;
772
872
  }
773
873
  if (key.home) {
@@ -894,7 +994,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
894
994
  // === Render: hard-wrap every logical line at the input width, then show
895
995
  // the window of visual lines with the caret row always visible (CC's
896
996
  // maxVisibleLines behavior with automatic wrapping).
897
- 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);
898
1001
  const visualLines = wrapToWidth(value, inputWidth);
899
1002
  const caretVisualLine = wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
900
1003
  const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
@@ -922,13 +1025,38 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
922
1025
  if (absoluteLine !== caretVisualLine) {
923
1026
  return (_jsx(Text, { wrap: "truncate-end", children: line }, absoluteLine));
924
1027
  }
925
- // 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.
926
1034
  const col = caretCharCol();
1035
+ const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(line), col);
927
1036
  const before = line.slice(0, col);
928
- const at = line[col] ?? ' ';
929
- const after = line.slice(col + 1);
1037
+ const at = clusterEnd > col ? line.slice(col, clusterEnd) : ' ';
1038
+ const after = line.slice(clusterEnd);
930
1039
  return (_jsxs(Text, { wrap: "truncate-end", children: [before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
931
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]);
932
1060
  const lastNotification = channel.notifications[channel.notifications.length - 1];
933
1061
  // Park the native terminal cursor at the input caret (via the renderer's
934
1062
  // cursor-declaration mechanism). Terminal emulators render IME preedit
@@ -939,27 +1067,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
939
1067
  // relative to the value box the ref attaches to.
940
1068
  const valueBoxRef = useDeclaredCursor({
941
1069
  line: caretVisualLine - windowStart,
942
- 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),
943
1074
  active: !selectionActive,
944
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]);
945
1088
  // 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
946
1089
  // absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
947
1090
  // 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
948
1091
  // 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
949
1092
  const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
950
- 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
951
1100
  .filter(item => item.placement === 'steer')
952
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
953
1102
  .filter(item => item.placement === 'followup')
954
- .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 })) })] }) })] }));
955
1124
  }
956
1125
  /**
957
1126
  * Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
958
1127
  * stringWidth). Used by the input renderer so long lines wrap instead of
959
- * 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).
960
1133
  */
961
1134
  function wrapToWidth(text, width) {
962
1135
  const rows = [];
1136
+ const segmenter = getGraphemeSegmenter();
963
1137
  for (const line of text.split('\n')) {
964
1138
  if (line === '') {
965
1139
  rows.push('');
@@ -967,15 +1141,15 @@ function wrapToWidth(text, width) {
967
1141
  }
968
1142
  let current = '';
969
1143
  let currentWidth = 0;
970
- for (const ch of line) {
971
- const w = stringWidth(ch);
1144
+ for (const { segment } of segmenter.segment(line)) {
1145
+ const w = stringWidth(segment);
972
1146
  if (currentWidth + w > width && current !== '') {
973
1147
  rows.push(current);
974
- current = ch;
1148
+ current = segment;
975
1149
  currentWidth = w;
976
1150
  }
977
1151
  else {
978
- current += ch;
1152
+ current += segment;
979
1153
  currentWidth += w;
980
1154
  }
981
1155
  }
@@ -983,3 +1157,48 @@ function wrapToWidth(text, width) {
983
1157
  }
984
1158
  return rows;
985
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