wave-code 1.0.10 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/bin/wave-code.js +8 -2
  2. package/dist/bundle/wave.mjs +3231 -0
  3. package/package.json +24 -25
  4. package/src/cli.tsx +6 -1
  5. package/src/commands/update.ts +2 -7
  6. package/src/components/App.tsx +3 -7
  7. package/src/components/BtwDisplay.tsx +10 -1
  8. package/src/components/ChatInterface.tsx +21 -52
  9. package/src/components/ConfirmationDetails.tsx +144 -37
  10. package/src/components/DiffDisplay.tsx +335 -301
  11. package/src/components/InputBox.tsx +17 -2
  12. package/src/components/LoadingIndicator.tsx +16 -2
  13. package/src/components/MessageBlockItem.tsx +2 -4
  14. package/src/components/MessageList.tsx +75 -14
  15. package/src/components/ReasoningDisplay.tsx +2 -4
  16. package/src/components/SessionSelector.tsx +82 -11
  17. package/src/components/SkillsManager.tsx +312 -0
  18. package/src/components/StatusCommand.tsx +2 -9
  19. package/src/components/ToolDisplay.tsx +2 -4
  20. package/src/constants/commands.ts +6 -0
  21. package/src/contexts/useChat.tsx +245 -301
  22. package/src/daemon/commands.ts +57 -27
  23. package/src/daemon/jsonRpcClient.ts +1 -1
  24. package/src/daemon/socketClient.ts +2 -2
  25. package/src/hooks/useInputManager.ts +7 -1
  26. package/src/index.ts +57 -10
  27. package/src/managers/inputHandlers.ts +2 -0
  28. package/src/managers/inputReducer.ts +195 -104
  29. package/src/print-cli.ts +25 -17
  30. package/src/reducers/selectorReducer.ts +10 -0
  31. package/src/reducers/skillsManagerReducer.ts +91 -0
  32. package/src/session-selector-cli.tsx +269 -28
  33. package/src/stdio/agentBridge.ts +30 -21
  34. package/src/stdio/daemonServer.ts +3 -1
  35. package/src/stdio/protocol.ts +2 -0
  36. package/src/utils/clipboard.ts +21 -0
  37. package/src/utils/highlightUtils.ts +46 -1
  38. package/src/utils/readPackageJson.ts +29 -0
  39. package/src/utils/streamingText.ts +11 -0
  40. package/src/utils/worktree.ts +191 -43
  41. package/dist/cli.d.ts +0 -6
  42. package/dist/cli.js +0 -53
  43. package/dist/commands/plugin/disable.d.ts +0 -5
  44. package/dist/commands/plugin/disable.js +0 -15
  45. package/dist/commands/plugin/enable.d.ts +0 -5
  46. package/dist/commands/plugin/enable.js +0 -15
  47. package/dist/commands/plugin/install.d.ts +0 -5
  48. package/dist/commands/plugin/install.js +0 -21
  49. package/dist/commands/plugin/list.d.ts +0 -1
  50. package/dist/commands/plugin/list.js +0 -31
  51. package/dist/commands/plugin/marketplace.d.ts +0 -10
  52. package/dist/commands/plugin/marketplace.js +0 -90
  53. package/dist/commands/plugin/uninstall.d.ts +0 -3
  54. package/dist/commands/plugin/uninstall.js +0 -16
  55. package/dist/commands/plugin/update.d.ts +0 -3
  56. package/dist/commands/plugin/update.js +0 -15
  57. package/dist/commands/update.d.ts +0 -1
  58. package/dist/commands/update.js +0 -97
  59. package/dist/components/AgentsManager.d.ts +0 -7
  60. package/dist/components/AgentsManager.js +0 -109
  61. package/dist/components/App.d.ts +0 -9
  62. package/dist/components/App.js +0 -76
  63. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  64. package/dist/components/BackgroundTaskManager.js +0 -99
  65. package/dist/components/BangDisplay.d.ts +0 -8
  66. package/dist/components/BangDisplay.js +0 -17
  67. package/dist/components/BtwDisplay.d.ts +0 -7
  68. package/dist/components/BtwDisplay.js +0 -19
  69. package/dist/components/ChatInterface.d.ts +0 -2
  70. package/dist/components/ChatInterface.js +0 -61
  71. package/dist/components/CommandSelector.d.ts +0 -10
  72. package/dist/components/CommandSelector.js +0 -66
  73. package/dist/components/CompactDisplay.d.ts +0 -8
  74. package/dist/components/CompactDisplay.js +0 -13
  75. package/dist/components/ConfirmationDetails.d.ts +0 -9
  76. package/dist/components/ConfirmationDetails.js +0 -45
  77. package/dist/components/ConfirmationSelector.d.ts +0 -13
  78. package/dist/components/ConfirmationSelector.js +0 -127
  79. package/dist/components/DiffDisplay.d.ts +0 -8
  80. package/dist/components/DiffDisplay.js +0 -201
  81. package/dist/components/DiscoverView.d.ts +0 -2
  82. package/dist/components/DiscoverView.js +0 -37
  83. package/dist/components/FileSelector.d.ts +0 -11
  84. package/dist/components/FileSelector.js +0 -50
  85. package/dist/components/HelpView.d.ts +0 -7
  86. package/dist/components/HelpView.js +0 -74
  87. package/dist/components/HistorySearch.d.ts +0 -8
  88. package/dist/components/HistorySearch.js +0 -66
  89. package/dist/components/InputBox.d.ts +0 -24
  90. package/dist/components/InputBox.js +0 -149
  91. package/dist/components/InstalledView.d.ts +0 -2
  92. package/dist/components/InstalledView.js +0 -45
  93. package/dist/components/LoadingIndicator.d.ts +0 -10
  94. package/dist/components/LoadingIndicator.js +0 -6
  95. package/dist/components/LoginCommand.d.ts +0 -5
  96. package/dist/components/LoginCommand.js +0 -153
  97. package/dist/components/Markdown.d.ts +0 -5
  98. package/dist/components/Markdown.js +0 -127
  99. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  100. package/dist/components/MarketplaceAddForm.js +0 -55
  101. package/dist/components/MarketplaceDetail.d.ts +0 -2
  102. package/dist/components/MarketplaceDetail.js +0 -74
  103. package/dist/components/MarketplaceList.d.ts +0 -8
  104. package/dist/components/MarketplaceList.js +0 -17
  105. package/dist/components/MarketplaceView.d.ts +0 -2
  106. package/dist/components/MarketplaceView.js +0 -41
  107. package/dist/components/McpManager.d.ts +0 -9
  108. package/dist/components/McpManager.js +0 -88
  109. package/dist/components/MessageBlockItem.d.ts +0 -8
  110. package/dist/components/MessageBlockItem.js +0 -15
  111. package/dist/components/MessageList.d.ts +0 -10
  112. package/dist/components/MessageList.js +0 -80
  113. package/dist/components/ModelSelector.d.ts +0 -8
  114. package/dist/components/ModelSelector.js +0 -45
  115. package/dist/components/Notifications.d.ts +0 -7
  116. package/dist/components/Notifications.js +0 -9
  117. package/dist/components/PlanDisplay.d.ts +0 -7
  118. package/dist/components/PlanDisplay.js +0 -6
  119. package/dist/components/PluginDetail.d.ts +0 -2
  120. package/dist/components/PluginDetail.js +0 -87
  121. package/dist/components/PluginList.d.ts +0 -13
  122. package/dist/components/PluginList.js +0 -15
  123. package/dist/components/PluginManagerShell.d.ts +0 -6
  124. package/dist/components/PluginManagerShell.js +0 -92
  125. package/dist/components/PluginManagerTypes.d.ts +0 -35
  126. package/dist/components/PluginManagerTypes.js +0 -1
  127. package/dist/components/QueuedMessageList.d.ts +0 -2
  128. package/dist/components/QueuedMessageList.js +0 -18
  129. package/dist/components/ReasoningDisplay.d.ts +0 -8
  130. package/dist/components/ReasoningDisplay.js +0 -13
  131. package/dist/components/RewindCommand.d.ts +0 -12
  132. package/dist/components/RewindCommand.js +0 -78
  133. package/dist/components/SessionSelector.d.ts +0 -12
  134. package/dist/components/SessionSelector.js +0 -47
  135. package/dist/components/StatusCommand.d.ts +0 -5
  136. package/dist/components/StatusCommand.js +0 -28
  137. package/dist/components/StatusLine.d.ts +0 -6
  138. package/dist/components/StatusLine.js +0 -9
  139. package/dist/components/TaskList.d.ts +0 -5
  140. package/dist/components/TaskList.js +0 -197
  141. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  142. package/dist/components/TaskNotificationMessage.js +0 -12
  143. package/dist/components/ToolDisplay.d.ts +0 -9
  144. package/dist/components/ToolDisplay.js +0 -55
  145. package/dist/components/WorkflowManager.d.ts +0 -5
  146. package/dist/components/WorkflowManager.js +0 -102
  147. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  148. package/dist/components/WorktreeExitPrompt.js +0 -35
  149. package/dist/constants/commands.d.ts +0 -2
  150. package/dist/constants/commands.js +0 -92
  151. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  152. package/dist/contexts/PluginManagerContext.js +0 -9
  153. package/dist/contexts/useAppConfig.d.ts +0 -10
  154. package/dist/contexts/useAppConfig.js +0 -13
  155. package/dist/contexts/useChat.d.ts +0 -101
  156. package/dist/contexts/useChat.js +0 -993
  157. package/dist/contracts/status.d.ts +0 -7
  158. package/dist/contracts/status.js +0 -1
  159. package/dist/daemon/commands.d.ts +0 -49
  160. package/dist/daemon/commands.js +0 -341
  161. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  162. package/dist/daemon/jsonRpcClient.js +0 -129
  163. package/dist/daemon/socketClient.d.ts +0 -13
  164. package/dist/daemon/socketClient.js +0 -26
  165. package/dist/daemon-cli.d.ts +0 -10
  166. package/dist/daemon-cli.js +0 -15
  167. package/dist/hooks/useInputManager.d.ts +0 -75
  168. package/dist/hooks/useInputManager.js +0 -583
  169. package/dist/hooks/usePluginManager.d.ts +0 -4
  170. package/dist/hooks/usePluginManager.js +0 -265
  171. package/dist/hooks/useTasks.d.ts +0 -1
  172. package/dist/hooks/useTasks.js +0 -5
  173. package/dist/index.d.ts +0 -4
  174. package/dist/index.js +0 -447
  175. package/dist/managers/inputHandlers.d.ts +0 -30
  176. package/dist/managers/inputHandlers.js +0 -625
  177. package/dist/managers/inputReducer.d.ts +0 -271
  178. package/dist/managers/inputReducer.js +0 -1121
  179. package/dist/plugin-manager-cli.d.ts +0 -5
  180. package/dist/plugin-manager-cli.js +0 -12
  181. package/dist/print-cli.d.ts +0 -10
  182. package/dist/print-cli.js +0 -209
  183. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  184. package/dist/reducers/agentsManagerReducer.js +0 -54
  185. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  186. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  187. package/dist/reducers/confirmationReducer.d.ts +0 -37
  188. package/dist/reducers/confirmationReducer.js +0 -193
  189. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  190. package/dist/reducers/helpSelectorReducer.js +0 -52
  191. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  192. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  193. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  194. package/dist/reducers/mcpManagerReducer.js +0 -74
  195. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  196. package/dist/reducers/pluginDetailReducer.js +0 -70
  197. package/dist/reducers/questionReducer.d.ts +0 -82
  198. package/dist/reducers/questionReducer.js +0 -375
  199. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  200. package/dist/reducers/rewindSelectorReducer.js +0 -37
  201. package/dist/reducers/selectorReducer.d.ts +0 -23
  202. package/dist/reducers/selectorReducer.js +0 -42
  203. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  204. package/dist/reducers/workflowManagerReducer.js +0 -100
  205. package/dist/session-selector-cli.d.ts +0 -3
  206. package/dist/session-selector-cli.js +0 -25
  207. package/dist/stdio/agentBridge.d.ts +0 -139
  208. package/dist/stdio/agentBridge.js +0 -999
  209. package/dist/stdio/daemonServer.d.ts +0 -67
  210. package/dist/stdio/daemonServer.js +0 -191
  211. package/dist/stdio/index.d.ts +0 -5
  212. package/dist/stdio/index.js +0 -5
  213. package/dist/stdio/jsonRpcConnection.d.ts +0 -39
  214. package/dist/stdio/jsonRpcConnection.js +0 -135
  215. package/dist/stdio/protocol.d.ts +0 -41
  216. package/dist/stdio/protocol.js +0 -26
  217. package/dist/stdio/stdioServer.d.ts +0 -36
  218. package/dist/stdio/stdioServer.js +0 -51
  219. package/dist/stdio-cli.d.ts +0 -8
  220. package/dist/stdio-cli.js +0 -15
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.js +0 -1
  223. package/dist/utils/bracketedPaste.d.ts +0 -39
  224. package/dist/utils/bracketedPaste.js +0 -122
  225. package/dist/utils/clipboard.d.ts +0 -21
  226. package/dist/utils/clipboard.js +0 -347
  227. package/dist/utils/constants.d.ts +0 -16
  228. package/dist/utils/constants.js +0 -18
  229. package/dist/utils/highlightUtils.d.ts +0 -1
  230. package/dist/utils/highlightUtils.js +0 -93
  231. package/dist/utils/inputUtils.d.ts +0 -17
  232. package/dist/utils/inputUtils.js +0 -67
  233. package/dist/utils/logger.d.ts +0 -71
  234. package/dist/utils/logger.js +0 -259
  235. package/dist/utils/markdownTable.d.ts +0 -34
  236. package/dist/utils/markdownTable.js +0 -302
  237. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  238. package/dist/utils/rewindCheckpoints.js +0 -17
  239. package/dist/utils/throttle.d.ts +0 -14
  240. package/dist/utils/throttle.js +0 -55
  241. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  242. package/dist/utils/toolParameterTransforms.js +0 -73
  243. package/dist/utils/usageSummary.d.ts +0 -28
  244. package/dist/utils/usageSummary.js +0 -121
  245. package/dist/utils/version.d.ts +0 -1
  246. package/dist/utils/version.js +0 -4
  247. package/dist/utils/worktree.d.ts +0 -35
  248. package/dist/utils/worktree.js +0 -219
@@ -1,1121 +0,0 @@
1
- import { getAtSelectorPosition, getSlashSelectorPosition, getWordEnd, SELECTOR_TRIGGERS, getProjectedState, } from "../utils/inputUtils.js";
2
- import { AVAILABLE_COMMANDS } from "../constants/commands.js";
3
- /**
4
- * True while the /btw overlay is up (a question is on display, loading or
5
- * answered). App's Ctrl+C exit handler checks this so Ctrl+C does not quit
6
- * the app while the overlay owns the keys. Synced from useInputManager via
7
- * an effect.
8
- */
9
- export const btwOverlayActiveRef = { current: false };
10
- export const ESC_DOUBLE_PRESS_TIMEOUT_MS = 1000;
11
- export const initialState = {
12
- inputText: "",
13
- cursorPosition: 0,
14
- showFileSelector: false,
15
- atPosition: -1,
16
- fileSearchQuery: "",
17
- filteredFiles: [],
18
- showCommandSelector: false,
19
- slashPosition: -1,
20
- commandSearchQuery: "",
21
- showHistorySearch: false,
22
- historySearchQuery: "",
23
- longTextCounter: 0,
24
- longTextMap: {},
25
- attachedImages: [],
26
- imageIdCounter: 1,
27
- showBackgroundTaskManager: false,
28
- showMcpManager: false,
29
- showAgentsManager: false,
30
- showRewindManager: false,
31
- showHelp: false,
32
- showStatusCommand: false,
33
- showLoginCommand: false,
34
- showPluginManager: false,
35
- showModelSelector: false,
36
- showWorkflowManager: false,
37
- permissionMode: "default",
38
- selectorJustUsed: false,
39
- history: [],
40
- historyIndex: -1,
41
- originalInputText: "",
42
- originalLongTextMap: {},
43
- isFileSearching: false,
44
- btwState: {
45
- question: "",
46
- isLoading: false,
47
- },
48
- pendingEffect: null,
49
- escClearPending: false,
50
- };
51
- /**
52
- * Insert text at the cursor position, folding text longer than 200 chars
53
- * into a [LongText#N] placeholder. Shared by the INSERT_TEXT_WITH_PLACEHOLDER
54
- * action and multi-char chunk inserts (typed bursts, terminal paste, tmux
55
- * send-keys).
56
- */
57
- function insertTextWithPlaceholder(textToInsert, state) {
58
- let text = textToInsert;
59
- let newLongTextCounter = state.longTextCounter;
60
- const newLongTextMap = { ...state.longTextMap };
61
- if (text.length > 200) {
62
- newLongTextCounter += 1;
63
- const placeholderLabel = `[LongText#${newLongTextCounter}]`;
64
- newLongTextMap[placeholderLabel] = text;
65
- text = placeholderLabel;
66
- }
67
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
68
- const afterCursor = state.inputText.substring(state.cursorPosition);
69
- const newText = beforeCursor + text + afterCursor;
70
- const newCursorPosition = state.cursorPosition + text.length;
71
- const newState = {
72
- ...state,
73
- inputText: newText,
74
- cursorPosition: newCursorPosition,
75
- longTextCounter: newLongTextCounter,
76
- longTextMap: newLongTextMap,
77
- historyIndex: -1,
78
- };
79
- // Sync selectors
80
- const atPos = getAtSelectorPosition(newText, newCursorPosition);
81
- if (atPos !== -1 && !newState.showFileSelector) {
82
- newState.showFileSelector = true;
83
- newState.atPosition = atPos;
84
- newState.isFileSearching = true;
85
- }
86
- const slashPos = getSlashSelectorPosition(newText, newCursorPosition);
87
- if (slashPos !== -1 && !newState.showCommandSelector) {
88
- newState.showCommandSelector = true;
89
- newState.slashPosition = slashPos;
90
- }
91
- if (newState.showFileSelector && newState.atPosition >= 0) {
92
- newState.fileSearchQuery = newText.substring(newState.atPosition + 1, newCursorPosition);
93
- }
94
- else if (newState.showCommandSelector && newState.slashPosition >= 0) {
95
- newState.commandSearchQuery = newText.substring(newState.slashPosition + 1, newCursorPosition);
96
- }
97
- return newState;
98
- }
99
- /**
100
- * Submit the current input text: extract [Image #N] references, route /btw
101
- * and CLI-internal slash commands, otherwise send as a message. Returns null
102
- * when there is nothing to submit (empty text).
103
- */
104
- function submitInput(state) {
105
- if (!state.inputText.trim()) {
106
- return null;
107
- }
108
- const imageRegex = /\[Image #(\d+)\]/g;
109
- const matches = [...state.inputText.matchAll(imageRegex)];
110
- const referencedImages = matches
111
- .map((match) => {
112
- const imageId = parseInt(match[1], 10);
113
- return state.attachedImages.find((img) => img.id === imageId);
114
- })
115
- .filter((img) => img !== undefined)
116
- .map((img) => ({ path: img.path, mimeType: img.mimeType }));
117
- const contentWithPlaceholders = state.inputText
118
- .replace(imageRegex, "")
119
- .trim();
120
- if (contentWithPlaceholders.startsWith("/btw ")) {
121
- const question = contentWithPlaceholders.substring(5).trim();
122
- if (!question) {
123
- // "/btw " with no question text — show usage (aligned with Claude Code)
124
- return {
125
- ...state,
126
- inputText: "",
127
- cursorPosition: 0,
128
- historyIndex: -1,
129
- longTextMap: {},
130
- attachedImages: [],
131
- btwState: {
132
- question: "",
133
- isLoading: false,
134
- answer: "Usage: /btw <your question>",
135
- },
136
- };
137
- }
138
- return {
139
- ...state,
140
- inputText: "",
141
- cursorPosition: 0,
142
- historyIndex: -1,
143
- longTextMap: {},
144
- attachedImages: [],
145
- btwState: {
146
- question,
147
- isLoading: true,
148
- answer: undefined,
149
- },
150
- pendingEffect: { type: "ASK_BTW", question },
151
- };
152
- }
153
- if (contentWithPlaceholders === "/btw") {
154
- // Bare /btw — show usage (aligned with Claude Code)
155
- return {
156
- ...state,
157
- inputText: "",
158
- cursorPosition: 0,
159
- historyIndex: -1,
160
- longTextMap: {},
161
- attachedImages: [],
162
- btwState: {
163
- question: "",
164
- isLoading: false,
165
- answer: "Usage: /btw <your question>",
166
- },
167
- };
168
- }
169
- // Check if the content is a CLI-internal slash command (help, tasks,
170
- // etc.) that should be executed locally rather than sent as a message.
171
- // Agent slash commands and unknown /commands always go to SEND_MESSAGE.
172
- if (contentWithPlaceholders.startsWith("/")) {
173
- const spaceIndex = contentWithPlaceholders.indexOf(" ");
174
- const commandName = spaceIndex === -1
175
- ? contentWithPlaceholders.substring(1)
176
- : contentWithPlaceholders.substring(1, spaceIndex);
177
- const isInternalCommand = AVAILABLE_COMMANDS.some((cmd) => cmd.id === commandName);
178
- if (isInternalCommand) {
179
- const argsText = spaceIndex === -1
180
- ? undefined
181
- : contentWithPlaceholders.substring(spaceIndex + 1).trim() ||
182
- undefined;
183
- return {
184
- ...state,
185
- inputText: "",
186
- cursorPosition: 0,
187
- historyIndex: -1,
188
- longTextMap: {},
189
- attachedImages: [],
190
- pendingEffect: {
191
- type: "EXECUTE_COMMAND",
192
- command: commandName,
193
- args: argsText,
194
- },
195
- };
196
- }
197
- }
198
- return {
199
- ...state,
200
- inputText: "",
201
- cursorPosition: 0,
202
- historyIndex: -1,
203
- longTextMap: {},
204
- attachedImages: [],
205
- pendingEffect: {
206
- type: "SEND_MESSAGE",
207
- content: contentWithPlaceholders,
208
- images: referencedImages.length > 0 ? referencedImages : undefined,
209
- longTextMap: state.longTextMap,
210
- },
211
- };
212
- }
213
- export function inputReducer(state, action) {
214
- switch (action.type) {
215
- case "SET_INPUT_TEXT":
216
- return {
217
- ...state,
218
- inputText: action.payload,
219
- historyIndex: -1,
220
- };
221
- case "SET_CURSOR_POSITION":
222
- return {
223
- ...state,
224
- cursorPosition: Math.max(0, Math.min(state.inputText.length, action.payload)),
225
- };
226
- case "INSERT_TEXT": {
227
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
228
- const afterCursor = state.inputText.substring(state.cursorPosition);
229
- const newText = beforeCursor + action.payload + afterCursor;
230
- const newCursorPosition = state.cursorPosition + action.payload.length;
231
- return {
232
- ...state,
233
- inputText: newText,
234
- cursorPosition: newCursorPosition,
235
- historyIndex: -1,
236
- };
237
- }
238
- case "DELETE_CHAR": {
239
- if (state.cursorPosition > 0) {
240
- const beforeCursor = state.inputText.substring(0, state.cursorPosition - 1);
241
- const afterCursor = state.inputText.substring(state.cursorPosition);
242
- const newText = beforeCursor + afterCursor;
243
- const newCursorPosition = state.cursorPosition - 1;
244
- return {
245
- ...state,
246
- inputText: newText,
247
- cursorPosition: newCursorPosition,
248
- historyIndex: -1,
249
- };
250
- }
251
- return state;
252
- }
253
- case "MOVE_CURSOR": {
254
- const newCursorPosition = Math.max(0, Math.min(state.inputText.length, state.cursorPosition + action.payload));
255
- return { ...state, cursorPosition: newCursorPosition };
256
- }
257
- case "ACTIVATE_FILE_SELECTOR":
258
- return {
259
- ...state,
260
- showFileSelector: true,
261
- atPosition: action.payload,
262
- fileSearchQuery: "",
263
- filteredFiles: [],
264
- isFileSearching: true,
265
- };
266
- case "SET_FILE_SEARCH_QUERY":
267
- return {
268
- ...state,
269
- fileSearchQuery: action.payload,
270
- isFileSearching: true,
271
- };
272
- case "SET_FILTERED_FILES":
273
- return {
274
- ...state,
275
- filteredFiles: action.payload,
276
- isFileSearching: false,
277
- };
278
- case "CANCEL_FILE_SELECTOR":
279
- return {
280
- ...state,
281
- showFileSelector: false,
282
- atPosition: -1,
283
- fileSearchQuery: "",
284
- filteredFiles: [],
285
- selectorJustUsed: true,
286
- isFileSearching: false,
287
- };
288
- case "ACTIVATE_COMMAND_SELECTOR":
289
- return {
290
- ...state,
291
- showCommandSelector: true,
292
- slashPosition: action.payload,
293
- commandSearchQuery: "",
294
- };
295
- case "SET_COMMAND_SEARCH_QUERY":
296
- return { ...state, commandSearchQuery: action.payload };
297
- case "CANCEL_COMMAND_SELECTOR":
298
- return {
299
- ...state,
300
- showCommandSelector: false,
301
- slashPosition: -1,
302
- commandSearchQuery: "",
303
- selectorJustUsed: true,
304
- };
305
- case "ACTIVATE_HISTORY_SEARCH":
306
- return {
307
- ...state,
308
- showHistorySearch: true,
309
- historySearchQuery: "",
310
- };
311
- case "SET_HISTORY_SEARCH_QUERY":
312
- return { ...state, historySearchQuery: action.payload };
313
- case "CANCEL_HISTORY_SEARCH":
314
- return {
315
- ...state,
316
- showHistorySearch: false,
317
- historySearchQuery: "",
318
- selectorJustUsed: true,
319
- };
320
- case "ADD_IMAGE": {
321
- const newImage = {
322
- id: state.imageIdCounter,
323
- path: action.payload.path,
324
- mimeType: action.payload.mimeType,
325
- };
326
- return {
327
- ...state,
328
- attachedImages: [...state.attachedImages, newImage],
329
- imageIdCounter: state.imageIdCounter + 1,
330
- };
331
- }
332
- case "REMOVE_IMAGE":
333
- return {
334
- ...state,
335
- attachedImages: state.attachedImages.filter((img) => img.id !== action.payload),
336
- };
337
- case "CLEAR_IMAGES":
338
- return { ...state, attachedImages: [] };
339
- case "SET_SHOW_BACKGROUND_TASK_MANAGER":
340
- return {
341
- ...state,
342
- showBackgroundTaskManager: action.payload,
343
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
344
- };
345
- case "SET_SHOW_MCP_MANAGER":
346
- return {
347
- ...state,
348
- showMcpManager: action.payload,
349
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
350
- };
351
- case "SET_SHOW_AGENTS_MANAGER":
352
- return {
353
- ...state,
354
- showAgentsManager: action.payload,
355
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
356
- };
357
- case "SET_SHOW_REWIND_MANAGER":
358
- return {
359
- ...state,
360
- showRewindManager: action.payload,
361
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
362
- };
363
- case "SET_SHOW_HELP":
364
- return {
365
- ...state,
366
- showHelp: action.payload,
367
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
368
- };
369
- case "SET_SHOW_STATUS_COMMAND":
370
- return {
371
- ...state,
372
- showStatusCommand: action.payload,
373
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
374
- };
375
- case "SET_SHOW_LOGIN_COMMAND":
376
- return {
377
- ...state,
378
- showLoginCommand: action.payload,
379
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
380
- };
381
- case "SET_SHOW_PLUGIN_MANAGER":
382
- return {
383
- ...state,
384
- showPluginManager: action.payload,
385
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
386
- };
387
- case "SET_SHOW_MODEL_SELECTOR":
388
- return {
389
- ...state,
390
- showModelSelector: action.payload,
391
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
392
- };
393
- case "SET_SHOW_WORKFLOW_MANAGER":
394
- return {
395
- ...state,
396
- showWorkflowManager: action.payload,
397
- selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
398
- };
399
- case "SET_PERMISSION_MODE":
400
- return { ...state, permissionMode: action.payload };
401
- case "SET_SELECTOR_JUST_USED":
402
- return { ...state, selectorJustUsed: action.payload };
403
- case "INSERT_TEXT_WITH_PLACEHOLDER":
404
- return insertTextWithPlaceholder(action.payload, state);
405
- case "CLEAR_LONG_TEXT_MAP":
406
- return { ...state, longTextMap: {} };
407
- case "CLEAR_INPUT":
408
- return {
409
- ...state,
410
- inputText: "",
411
- cursorPosition: 0,
412
- historyIndex: -1,
413
- };
414
- case "ADD_IMAGE_AND_INSERT_PLACEHOLDER": {
415
- const newImage = {
416
- id: state.imageIdCounter,
417
- path: action.payload.path,
418
- mimeType: action.payload.mimeType,
419
- };
420
- const placeholder = `[Image #${newImage.id}]`;
421
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
422
- const afterCursor = state.inputText.substring(state.cursorPosition);
423
- const newText = beforeCursor + placeholder + afterCursor;
424
- const newCursorPosition = state.cursorPosition + placeholder.length;
425
- return {
426
- ...state,
427
- attachedImages: [...state.attachedImages, newImage],
428
- imageIdCounter: state.imageIdCounter + 1,
429
- inputText: newText,
430
- cursorPosition: newCursorPosition,
431
- historyIndex: -1,
432
- };
433
- }
434
- case "SET_HISTORY_ENTRIES":
435
- return { ...state, history: action.payload };
436
- case "NAVIGATE_HISTORY": {
437
- const direction = action.payload;
438
- let newIndex = state.historyIndex;
439
- let newOriginalInputText = state.originalInputText;
440
- let newOriginalLongTextMap = state.originalLongTextMap;
441
- if (direction === "up") {
442
- if (newIndex === -1) {
443
- newOriginalInputText = state.inputText;
444
- newOriginalLongTextMap = state.longTextMap;
445
- }
446
- newIndex = Math.min(state.history.length - 1, newIndex + 1);
447
- }
448
- else {
449
- if (newIndex === -1) {
450
- return state;
451
- }
452
- newIndex = Math.max(-1, newIndex - 1);
453
- }
454
- if (newIndex === -1) {
455
- return {
456
- ...state,
457
- historyIndex: newIndex,
458
- inputText: newOriginalInputText,
459
- longTextMap: newOriginalLongTextMap,
460
- cursorPosition: newOriginalInputText.length,
461
- originalInputText: "",
462
- originalLongTextMap: {},
463
- };
464
- }
465
- else {
466
- const entry = state.history[newIndex];
467
- return {
468
- ...state,
469
- historyIndex: newIndex,
470
- inputText: entry.prompt,
471
- longTextMap: entry.longTextMap || {},
472
- cursorPosition: entry.prompt.length,
473
- originalInputText: newOriginalInputText,
474
- originalLongTextMap: newOriginalLongTextMap,
475
- };
476
- }
477
- }
478
- case "SELECT_HISTORY_ENTRY": {
479
- const entry = action.payload;
480
- return {
481
- ...state,
482
- inputText: entry.prompt,
483
- longTextMap: entry.longTextMap || {},
484
- cursorPosition: entry.prompt.length,
485
- historyIndex: -1,
486
- history: [],
487
- originalInputText: "",
488
- originalLongTextMap: {},
489
- showHistorySearch: false,
490
- historySearchQuery: "",
491
- selectorJustUsed: true,
492
- };
493
- }
494
- case "SELECT_COMMAND": {
495
- const command = action.payload;
496
- if (state.slashPosition >= 0) {
497
- const wordEnd = getWordEnd(state.inputText, state.slashPosition);
498
- const beforeSlash = state.inputText.substring(0, state.slashPosition);
499
- const afterWord = state.inputText.substring(wordEnd);
500
- const newInput = beforeSlash + afterWord;
501
- const newCursorPosition = beforeSlash.length;
502
- return {
503
- ...state,
504
- inputText: newInput,
505
- cursorPosition: newCursorPosition,
506
- showCommandSelector: false,
507
- slashPosition: -1,
508
- commandSearchQuery: "",
509
- selectorJustUsed: true,
510
- pendingEffect: { type: "EXECUTE_COMMAND", command },
511
- };
512
- }
513
- return state;
514
- }
515
- case "INSERT_COMMAND": {
516
- const command = action.payload;
517
- if (state.slashPosition >= 0) {
518
- const wordEnd = getWordEnd(state.inputText, state.slashPosition);
519
- const beforeSlash = state.inputText.substring(0, state.slashPosition);
520
- const afterWord = state.inputText.substring(wordEnd);
521
- const newInput = beforeSlash + `/${command} ` + afterWord;
522
- const newCursorPosition = beforeSlash.length + command.length + 2;
523
- return {
524
- ...state,
525
- inputText: newInput,
526
- cursorPosition: newCursorPosition,
527
- showCommandSelector: false,
528
- slashPosition: -1,
529
- commandSearchQuery: "",
530
- selectorJustUsed: true,
531
- };
532
- }
533
- return state;
534
- }
535
- case "SELECT_FILE": {
536
- const filePath = action.payload;
537
- if (state.atPosition >= 0) {
538
- const wordEnd = getWordEnd(state.inputText, state.atPosition);
539
- const beforeAt = state.inputText.substring(0, state.atPosition);
540
- const afterWord = state.inputText.substring(wordEnd);
541
- const newInput = beforeAt + `@${filePath} ` + afterWord;
542
- const newCursorPosition = beforeAt.length + filePath.length + 2;
543
- return {
544
- ...state,
545
- inputText: newInput,
546
- cursorPosition: newCursorPosition,
547
- showFileSelector: false,
548
- atPosition: -1,
549
- fileSearchQuery: "",
550
- filteredFiles: [],
551
- selectorJustUsed: true,
552
- isFileSearching: false,
553
- };
554
- }
555
- return state;
556
- }
557
- case "RESET_HISTORY_NAVIGATION":
558
- return {
559
- ...state,
560
- historyIndex: -1,
561
- history: [],
562
- originalInputText: "",
563
- originalLongTextMap: {},
564
- };
565
- case "CLEAR_PENDING_EFFECT":
566
- return { ...state, pendingEffect: null };
567
- case "RESET_ESC_CLEAR_PENDING":
568
- return { ...state, escClearPending: false };
569
- case "HANDLE_KEY": {
570
- const { input, key } = action.payload;
571
- const hasQueuedMessages = action.payload.hasQueuedMessages ?? false;
572
- const isIdle = action.payload.isIdle ?? false;
573
- // 0. Raw DEL (\x7f) filtering.
574
- // SSH/tmux auto-repeat backspace coalesces multiple DEL bytes into one
575
- // chunk (e.g. "\x7f\x7f") that ink cannot parse into a key event, so it
576
- // arrives as raw input and would otherwise be treated as a paste and
577
- // inserted literally. Treat each DEL as a synchronous backspace instead
578
- // (aligned with Claude Code Issue #1853).
579
- if (!key.backspace && !key.delete && input.includes("\x7f")) {
580
- const delCount = (input.match(/\x7f/g) || []).length;
581
- if (state.showHistorySearch) {
582
- return {
583
- ...state,
584
- historySearchQuery: state.historySearchQuery.slice(0, -delCount),
585
- };
586
- }
587
- if (state.cursorPosition > 0) {
588
- const newCursorPosition = Math.max(0, state.cursorPosition - delCount);
589
- const newInputText = state.inputText.substring(0, newCursorPosition) +
590
- state.inputText.substring(state.cursorPosition);
591
- const newState = {
592
- ...state,
593
- inputText: newInputText,
594
- cursorPosition: newCursorPosition,
595
- historyIndex: -1,
596
- };
597
- // Deactivate selectors if their trigger character was deleted
598
- if (newState.showFileSelector &&
599
- newCursorPosition <= newState.atPosition) {
600
- newState.showFileSelector = false;
601
- newState.atPosition = -1;
602
- newState.fileSearchQuery = "";
603
- newState.isFileSearching = false;
604
- }
605
- if (newState.showCommandSelector &&
606
- newCursorPosition <= newState.slashPosition) {
607
- newState.showCommandSelector = false;
608
- newState.slashPosition = -1;
609
- newState.commandSearchQuery = "";
610
- }
611
- // Reactivate selectors if cursor is within a trigger word
612
- const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
613
- if (atPos !== -1 && !state.showFileSelector) {
614
- newState.showFileSelector = true;
615
- newState.atPosition = atPos;
616
- newState.isFileSearching = true;
617
- }
618
- const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
619
- if (slashPos !== -1 && !state.showCommandSelector) {
620
- newState.showCommandSelector = true;
621
- newState.slashPosition = slashPos;
622
- }
623
- // Update queries
624
- if (newState.showFileSelector && newState.atPosition >= 0) {
625
- newState.fileSearchQuery = newInputText.substring(newState.atPosition + 1, newCursorPosition);
626
- }
627
- if (newState.showCommandSelector && newState.slashPosition >= 0) {
628
- newState.commandSearchQuery = newInputText.substring(newState.slashPosition + 1, newCursorPosition);
629
- }
630
- return newState;
631
- }
632
- return state;
633
- }
634
- // 1. /btw overlay handling (active while a question is displayed, or
635
- // the bare-/btw usage message). Only Escape dismisses (or aborts the
636
- // in-flight side question while loading); every other key is ignored.
637
- if (state.btwState.question || state.btwState.answer) {
638
- if (key.escape) {
639
- if (state.btwState.isLoading) {
640
- return {
641
- ...state,
642
- btwState: {
643
- question: "",
644
- answer: undefined,
645
- isLoading: false,
646
- },
647
- pendingEffect: { type: "ABORT_BTW" },
648
- };
649
- }
650
- return {
651
- ...state,
652
- btwState: {
653
- question: "",
654
- answer: undefined,
655
- isLoading: false,
656
- },
657
- };
658
- }
659
- // Any other key while the overlay is up is ignored
660
- return state;
661
- }
662
- // 1. Escape Handling
663
- if (key.escape) {
664
- if (state.showFileSelector) {
665
- return {
666
- ...state,
667
- showFileSelector: false,
668
- atPosition: -1,
669
- fileSearchQuery: "",
670
- filteredFiles: [],
671
- selectorJustUsed: true,
672
- isFileSearching: false,
673
- };
674
- }
675
- if (state.showCommandSelector) {
676
- return {
677
- ...state,
678
- showCommandSelector: false,
679
- slashPosition: -1,
680
- commandSearchQuery: "",
681
- selectorJustUsed: true,
682
- };
683
- }
684
- if (state.showHistorySearch) {
685
- return {
686
- ...state,
687
- showHistorySearch: false,
688
- historySearchQuery: "",
689
- selectorJustUsed: true,
690
- };
691
- }
692
- if (state.historyIndex !== -1) {
693
- return {
694
- ...state,
695
- historyIndex: -1,
696
- inputText: state.originalInputText,
697
- longTextMap: state.originalLongTextMap,
698
- cursorPosition: state.originalInputText.length,
699
- originalInputText: "",
700
- originalLongTextMap: {},
701
- };
702
- }
703
- if (!(state.showBackgroundTaskManager ||
704
- state.showMcpManager ||
705
- state.showAgentsManager ||
706
- state.showRewindManager ||
707
- state.showHelp ||
708
- state.showStatusCommand ||
709
- state.showLoginCommand ||
710
- state.showPluginManager ||
711
- state.showModelSelector ||
712
- state.showWorkflowManager)) {
713
- // While AI is running (or any busy state) Esc keeps the abort
714
- // semantics. Only when idle does Esc fall through to the text-level
715
- // double-press clear (aligned with Claude Code's mutual-exclusion
716
- // design: Esc aborts only when a task is running).
717
- if (!isIdle) {
718
- return {
719
- ...state,
720
- escClearPending: false,
721
- pendingEffect: { type: "ABORT_MESSAGE" },
722
- };
723
- }
724
- // Idle: double-press Esc clears the input and saves to history.
725
- if (state.inputText) {
726
- if (state.escClearPending) {
727
- const originalText = state.inputText;
728
- const originalLongTextMap = state.longTextMap;
729
- return {
730
- ...state,
731
- inputText: "",
732
- cursorPosition: 0,
733
- historyIndex: -1,
734
- longTextMap: {},
735
- escClearPending: false,
736
- pendingEffect: {
737
- type: "SAVE_PROMPT_HISTORY",
738
- content: originalText,
739
- longTextMap: originalLongTextMap,
740
- },
741
- };
742
- }
743
- return { ...state, escClearPending: true };
744
- }
745
- return state;
746
- }
747
- return state;
748
- }
749
- // 3. Special Shortcuts
750
- if (key.tab && key.shift) {
751
- const modes = [
752
- "default",
753
- "acceptEdits",
754
- "bypassPermissions",
755
- "plan",
756
- ];
757
- const currentIndex = modes.indexOf(state.permissionMode);
758
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % modes.length;
759
- const nextMode = modes[nextIndex];
760
- return {
761
- ...state,
762
- permissionMode: nextMode,
763
- pendingEffect: { type: "PERMISSION_MODE_CHANGE", mode: nextMode },
764
- };
765
- }
766
- if (key.ctrl && input === "v") {
767
- return { ...state, pendingEffect: { type: "PASTE_IMAGE" } };
768
- }
769
- if (key.ctrl && input === "r") {
770
- return {
771
- ...state,
772
- showHistorySearch: true,
773
- historySearchQuery: "",
774
- };
775
- }
776
- if (key.ctrl && input === "b") {
777
- return {
778
- ...state,
779
- pendingEffect: { type: "BACKGROUND_CURRENT_TASK" },
780
- };
781
- }
782
- // Emacs-style line editing (aligned with Claude Code): Ctrl+A/E move the
783
- // cursor to line start/end, Ctrl+U/K delete to line start/end, Ctrl+W
784
- // deletes the word before the cursor. Skipped while a selector is open.
785
- if (key.ctrl &&
786
- input &&
787
- !state.showFileSelector &&
788
- !state.showCommandSelector &&
789
- !state.showHistorySearch) {
790
- const editKey = input.toLowerCase();
791
- if (editKey === "a") {
792
- return { ...state, cursorPosition: 0 };
793
- }
794
- if (editKey === "e") {
795
- return { ...state, cursorPosition: state.inputText.length };
796
- }
797
- if (editKey === "u") {
798
- return {
799
- ...state,
800
- inputText: state.inputText.substring(state.cursorPosition),
801
- cursorPosition: 0,
802
- historyIndex: -1,
803
- };
804
- }
805
- if (editKey === "k") {
806
- return {
807
- ...state,
808
- inputText: state.inputText.substring(0, state.cursorPosition),
809
- historyIndex: -1,
810
- };
811
- }
812
- if (editKey === "w") {
813
- // Find the start of the word before the cursor (skip trailing
814
- // whitespace, then the word itself).
815
- let start = state.cursorPosition - 1;
816
- while (start >= 0 && /\s/.test(state.inputText[start])) {
817
- start--;
818
- }
819
- while (start >= 0 && !/\s/.test(state.inputText[start])) {
820
- start--;
821
- }
822
- return {
823
- ...state,
824
- inputText: state.inputText.substring(0, start + 1) +
825
- state.inputText.substring(state.cursorPosition),
826
- cursorPosition: start + 1,
827
- historyIndex: -1,
828
- };
829
- }
830
- }
831
- // 4. History Navigation
832
- if (key.upArrow &&
833
- !state.showFileSelector &&
834
- !state.showCommandSelector) {
835
- // If queue has messages, recall from queue first (before history)
836
- if (hasQueuedMessages) {
837
- return {
838
- ...state,
839
- pendingEffect: { type: "RECALL_QUEUED_MESSAGE" },
840
- };
841
- }
842
- if (state.history.length === 0) {
843
- return { ...state, pendingEffect: { type: "FETCH_HISTORY" } };
844
- }
845
- // If history is already loaded, NAVIGATE_HISTORY logic follows
846
- let newIndex = state.historyIndex;
847
- let newOriginalInputText = state.originalInputText;
848
- let newOriginalLongTextMap = state.originalLongTextMap;
849
- if (newIndex === -1) {
850
- newOriginalInputText = state.inputText;
851
- newOriginalLongTextMap = state.longTextMap;
852
- }
853
- newIndex = Math.min(state.history.length - 1, newIndex + 1);
854
- const entry = state.history[newIndex];
855
- return {
856
- ...state,
857
- historyIndex: newIndex,
858
- inputText: entry.prompt,
859
- longTextMap: entry.longTextMap || {},
860
- cursorPosition: entry.prompt.length,
861
- originalInputText: newOriginalInputText,
862
- originalLongTextMap: newOriginalLongTextMap,
863
- };
864
- }
865
- if (key.downArrow &&
866
- !state.showFileSelector &&
867
- !state.showCommandSelector) {
868
- if (state.historyIndex === -1)
869
- return state;
870
- const newIndex = state.historyIndex - 1;
871
- if (newIndex === -1) {
872
- return {
873
- ...state,
874
- historyIndex: -1,
875
- inputText: state.originalInputText,
876
- longTextMap: state.originalLongTextMap,
877
- cursorPosition: state.originalInputText.length,
878
- originalInputText: "",
879
- originalLongTextMap: {},
880
- };
881
- }
882
- else {
883
- const entry = state.history[newIndex];
884
- return {
885
- ...state,
886
- historyIndex: newIndex,
887
- inputText: entry.prompt,
888
- longTextMap: entry.longTextMap || {},
889
- cursorPosition: entry.prompt.length,
890
- };
891
- }
892
- }
893
- // 5. Active Selector Handling (History Search, File, Command)
894
- if (state.showHistorySearch) {
895
- if (key.backspace || key.delete) {
896
- return {
897
- ...state,
898
- historySearchQuery: state.historySearchQuery.slice(0, -1),
899
- };
900
- }
901
- if (input && !key.ctrl && !key.meta && !key.return && !key.tab) {
902
- return {
903
- ...state,
904
- historySearchQuery: state.historySearchQuery + input,
905
- };
906
- }
907
- return state;
908
- }
909
- if (state.showFileSelector || state.showCommandSelector) {
910
- if (key.backspace || key.delete) {
911
- if (state.cursorPosition > 0) {
912
- const newCursorPosition = state.cursorPosition - 1;
913
- const beforeCursor = state.inputText.substring(0, state.cursorPosition - 1);
914
- const afterCursor = state.inputText.substring(state.cursorPosition);
915
- const newInputText = beforeCursor + afterCursor;
916
- const newState = {
917
- ...state,
918
- inputText: newInputText,
919
- cursorPosition: newCursorPosition,
920
- historyIndex: -1,
921
- };
922
- // checkForAtDeletion
923
- if (newState.showFileSelector &&
924
- newCursorPosition <= newState.atPosition) {
925
- newState.showFileSelector = false;
926
- newState.atPosition = -1;
927
- newState.fileSearchQuery = "";
928
- }
929
- // checkForSlashDeletion
930
- if (newState.showCommandSelector &&
931
- newCursorPosition <= newState.slashPosition) {
932
- newState.showCommandSelector = false;
933
- newState.slashPosition = -1;
934
- newState.commandSearchQuery = "";
935
- }
936
- // Update queries
937
- if (newState.showFileSelector && newState.atPosition >= 0) {
938
- newState.fileSearchQuery = newInputText.substring(newState.atPosition + 1, newCursorPosition);
939
- }
940
- if (newState.showCommandSelector && newState.slashPosition >= 0) {
941
- newState.commandSearchQuery = newInputText.substring(newState.slashPosition + 1, newCursorPosition);
942
- }
943
- return newState;
944
- }
945
- }
946
- if (key.leftArrow || key.rightArrow) {
947
- const delta = key.leftArrow ? -1 : 1;
948
- const newCursorPosition = Math.max(0, Math.min(state.inputText.length, state.cursorPosition + delta));
949
- const newState = { ...state, cursorPosition: newCursorPosition };
950
- if (newState.showFileSelector &&
951
- newCursorPosition <= newState.atPosition) {
952
- newState.showFileSelector = false;
953
- newState.atPosition = -1;
954
- }
955
- if (newState.showCommandSelector &&
956
- newCursorPosition <= newState.slashPosition) {
957
- newState.showCommandSelector = false;
958
- newState.slashPosition = -1;
959
- }
960
- return newState;
961
- }
962
- if (input === " ") {
963
- return {
964
- ...state,
965
- showFileSelector: false,
966
- atPosition: -1,
967
- showCommandSelector: false,
968
- slashPosition: -1,
969
- inputText: state.inputText.substring(0, state.cursorPosition) +
970
- " " +
971
- state.inputText.substring(state.cursorPosition),
972
- cursorPosition: state.cursorPosition + 1,
973
- };
974
- }
975
- if (key.return || key.tab || key.upArrow || key.downArrow) {
976
- return state;
977
- }
978
- }
979
- // 6. Return / Submit
980
- if (key.return) {
981
- return submitInput(state) ?? state;
982
- }
983
- // 7. Regular Input
984
- if (input &&
985
- !key.ctrl &&
986
- !("alt" in key && key.alt) &&
987
- !key.meta &&
988
- !key.return &&
989
- !key.escape &&
990
- !key.leftArrow &&
991
- !key.rightArrow &&
992
- !("home" in key && key.home) &&
993
- !("end" in key && key.end)) {
994
- // SSH-coalesced Enter: on slow links, "text" + Enter arrive as one
995
- // chunk ("o\r"). ink's parseKeypress only matches a lone \r, so
996
- // key.return is false here. Text with exactly one trailing \r is a
997
- // coalesced Enter — strip the \r, insert, and submit immediately
998
- // (aligned with Claude Code's useTextInput).
999
- const isCoalescedEnter = input.length > 1 &&
1000
- input.endsWith("\r") &&
1001
- !input.slice(0, -1).includes("\r") &&
1002
- // Backslash+CR is a stale VS Code Shift+Enter binding, not a
1003
- // coalesced Enter — keep it as regular input.
1004
- input[input.length - 2] !== "\\";
1005
- if (isCoalescedEnter) {
1006
- const insertedState = insertTextWithPlaceholder(input.slice(0, -1), state);
1007
- return submitInput(insertedState) ?? insertedState;
1008
- }
1009
- if (input.length > 1) {
1010
- // Multi-char chunk (typed burst, terminal paste, tmux send-keys):
1011
- // insert immediately — no debounce or paste buffer. \r → \n
1012
- // normalizes carriage returns from CRLF terminals.
1013
- return insertTextWithPlaceholder(input.replace(/\r/g, "\n"), state);
1014
- }
1015
- else {
1016
- let char = input;
1017
- if (char === "!" && state.cursorPosition === 0) {
1018
- char = "!";
1019
- }
1020
- const { newInputText, newCursorPosition } = getProjectedState(state.inputText, state.cursorPosition, char);
1021
- const newState = {
1022
- ...state,
1023
- inputText: newInputText,
1024
- cursorPosition: newCursorPosition,
1025
- historyIndex: -1,
1026
- };
1027
- // Selector Activation
1028
- const trigger = SELECTOR_TRIGGERS.find((t) => t.shouldActivate(char, newCursorPosition, newInputText, state.showFileSelector));
1029
- if (trigger) {
1030
- if (trigger.type === "ACTIVATE_FILE_SELECTOR") {
1031
- newState.showFileSelector = true;
1032
- newState.atPosition = newCursorPosition - 1;
1033
- newState.fileSearchQuery = "";
1034
- newState.isFileSearching = true;
1035
- }
1036
- else if (trigger.type === "ACTIVATE_COMMAND_SELECTOR") {
1037
- newState.showCommandSelector = true;
1038
- newState.slashPosition = newCursorPosition - 1;
1039
- newState.commandSearchQuery = "";
1040
- }
1041
- }
1042
- else {
1043
- const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
1044
- if (atPos !== -1 && !state.showFileSelector) {
1045
- newState.showFileSelector = true;
1046
- newState.atPosition = atPos;
1047
- newState.fileSearchQuery = "";
1048
- newState.isFileSearching = true;
1049
- }
1050
- const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
1051
- if (slashPos !== -1 && !state.showCommandSelector) {
1052
- newState.showCommandSelector = true;
1053
- newState.slashPosition = slashPos;
1054
- newState.commandSearchQuery = "";
1055
- }
1056
- }
1057
- // Update queries
1058
- if (newState.showFileSelector && newState.atPosition >= 0) {
1059
- newState.fileSearchQuery = newInputText.substring(newState.atPosition + 1, newCursorPosition);
1060
- }
1061
- if (newState.showCommandSelector && newState.slashPosition >= 0) {
1062
- newState.commandSearchQuery = newInputText.substring(newState.slashPosition + 1, newCursorPosition);
1063
- }
1064
- return newState;
1065
- }
1066
- }
1067
- // 8. Backspace / Delete (Normal Mode)
1068
- if (key.backspace || key.delete) {
1069
- if (state.cursorPosition > 0) {
1070
- const newCursorPosition = state.cursorPosition - 1;
1071
- const beforeCursor = state.inputText.substring(0, state.cursorPosition - 1);
1072
- const afterCursor = state.inputText.substring(state.cursorPosition);
1073
- const newInputText = beforeCursor + afterCursor;
1074
- const newState = {
1075
- ...state,
1076
- inputText: newInputText,
1077
- cursorPosition: newCursorPosition,
1078
- historyIndex: -1,
1079
- };
1080
- // Reactivate selectors if cursor is within word
1081
- const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
1082
- if (atPos !== -1 && !state.showFileSelector) {
1083
- newState.showFileSelector = true;
1084
- newState.atPosition = atPos;
1085
- newState.isFileSearching = true;
1086
- }
1087
- const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
1088
- if (slashPos !== -1 && !state.showCommandSelector) {
1089
- newState.showCommandSelector = true;
1090
- newState.slashPosition = slashPos;
1091
- }
1092
- // Update queries
1093
- if (newState.showFileSelector && newState.atPosition >= 0) {
1094
- newState.fileSearchQuery = newInputText.substring(newState.atPosition + 1, newCursorPosition);
1095
- }
1096
- if (newState.showCommandSelector && newState.slashPosition >= 0) {
1097
- newState.commandSearchQuery = newInputText.substring(newState.slashPosition + 1, newCursorPosition);
1098
- }
1099
- return newState;
1100
- }
1101
- }
1102
- // 9. Cursor Movement (Normal Mode)
1103
- if (key.leftArrow || key.rightArrow) {
1104
- const delta = key.leftArrow ? -1 : 1;
1105
- const newCursorPosition = Math.max(0, Math.min(state.inputText.length, state.cursorPosition + delta));
1106
- return { ...state, cursorPosition: newCursorPosition };
1107
- }
1108
- return state;
1109
- }
1110
- case "SET_BTW_STATE":
1111
- return {
1112
- ...state,
1113
- btwState: {
1114
- ...state.btwState,
1115
- ...action.payload,
1116
- },
1117
- };
1118
- default:
1119
- return state;
1120
- }
1121
- }