ethagent 3.3.4 → 4.0.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 (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,625 +0,0 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
- import { Box, Text, useStdout } from 'ink'
3
- import { theme } from '../../ui/theme.js'
4
- import { readClipboardImage } from '../../utils/clipboard.js'
5
- import { useAppInput } from '../../app/input/AppInputProvider.js'
6
- import type { SlashSuggestion } from '../commands.js'
7
- import {
8
- beginHistoryPreview,
9
- canNavigateHistory as canNavigateHistoryState,
10
- deleteToLineStart,
11
- emptyBuffer,
12
- exitHistoryPreview,
13
- detectActiveFileMention,
14
- moveThroughHistory,
15
- moveVerticalVisual,
16
- replaceActiveFileMention,
17
- type ChatBuffer,
18
- type FileMentionToken,
19
- } from './chatInputState.js'
20
- import {
21
- countPastedTextLineBreaks,
22
- expandPastedTextRefs,
23
- formatPastedTextRef,
24
- LARGE_PASTE_THRESHOLD,
25
- normalizePastedText,
26
- shouldCollapsePastedText,
27
- type PastedTextRef,
28
- } from './chatPaste.js'
29
- import {
30
- expandImageRefs,
31
- formatImageRefMarker,
32
- pruneImageRefs,
33
- type ImageRef,
34
- } from './imageRefs.js'
35
- import { inputWrapWidth, renderWithCursor } from './inputRendering.js'
36
- import {
37
- isFallbackPasteInput,
38
- isSoftBreak,
39
- listFileMentionSuggestions,
40
- summarizeQueuedMessage,
41
- type FileMentionSuggestion,
42
- } from './chatInputHelpers.js'
43
-
44
- export { inputWrapWidth, renderWithCursor } from './inputRendering.js'
45
-
46
- type PromptInputProps = {
47
- onSubmit: (value: string) => void
48
- history: string[]
49
- disabled?: boolean
50
- placeholderHints?: string[]
51
- queuedMessages?: string[]
52
- prefix?: string
53
- slashSuggestions?: SlashSuggestion[]
54
- mode?: 'prompt' | 'bash'
55
- onModeChange?: (mode: 'prompt' | 'bash') => void
56
- footerRight?: React.ReactNode
57
- cwd?: string
58
- seedText?: string | null
59
- onSeedConsumed?: () => void
60
- onImagePaste?: (path: string) => void
61
- }
62
-
63
- const MAX_LENGTH = 32_768
64
- const PLACEHOLDER_ROTATE_MS = 8000
65
- const PASTE_BURST_MS = 100
66
- const PASTE_FLUSH_LIMIT = 4096
67
- const MIN_INPUT_VIEWPORT_LINES = 3
68
- const PROMPT_FOOTER_LINES = 5
69
- const MAX_INLINE_PASTE_LINES = 2
70
- export const ChatInput: React.FC<PromptInputProps> = ({
71
- onSubmit,
72
- history,
73
- disabled,
74
- placeholderHints,
75
- queuedMessages = [],
76
- prefix = '›',
77
- slashSuggestions = [],
78
- mode = 'prompt',
79
- onModeChange,
80
- footerRight,
81
- cwd,
82
- seedText,
83
- onSeedConsumed,
84
- onImagePaste,
85
- }) => {
86
- const { stdout } = useStdout()
87
- const [buffer, setBuffer] = useState<ChatBuffer>(emptyBuffer)
88
- const { value, cursor } = buffer
89
- const [columns, setColumns] = useState<number>(() => stdout.columns ?? process.stdout.columns ?? 80)
90
- const [rows, setRows] = useState<number>(() => stdout.rows ?? process.stdout.rows ?? 24)
91
- const [historyIndex, setHistoryIndex] = useState<number | null>(null)
92
- const [draftBuffer, setDraftBuffer] = useState<ChatBuffer>(emptyBuffer)
93
- const [historyPreviewActive, setHistoryPreviewActive] = useState(false)
94
- const [preferredColumn, setPreferredColumn] = useState<number | null>(null)
95
- const [placeholderIdx, setPlaceholderIdx] = useState(0)
96
- const [suggestionIdx, setSuggestionIdx] = useState(0)
97
- const [fileSuggestionIdx, setFileSuggestionIdx] = useState(0)
98
- const [fileSuggestions, setFileSuggestions] = useState<FileMentionSuggestion[]>([])
99
-
100
- const bufferRef = useRef<ChatBuffer>(buffer)
101
- const historyIndexRef = useRef<number | null>(historyIndex)
102
- const draftBufferRef = useRef<ChatBuffer>(draftBuffer)
103
- const historyPreviewActiveRef = useRef(historyPreviewActive)
104
- const preferredColumnRef = useRef<number | null>(preferredColumn)
105
- const pasteBufferRef = useRef('')
106
- const pasteTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
107
- const pastedTextRefsRef = useRef<Map<number, PastedTextRef>>(new Map())
108
- const nextPastedTextRefIdRef = useRef(1)
109
- const imageRefsRef = useRef<Map<number, ImageRef>>(new Map())
110
- const nextImageRefIdRef = useRef(1)
111
-
112
- useEffect(() => { bufferRef.current = buffer }, [buffer])
113
- useEffect(() => { historyIndexRef.current = historyIndex }, [historyIndex])
114
- useEffect(() => { draftBufferRef.current = draftBuffer }, [draftBuffer])
115
- useEffect(() => { historyPreviewActiveRef.current = historyPreviewActive }, [historyPreviewActive])
116
- useEffect(() => { preferredColumnRef.current = preferredColumn }, [preferredColumn])
117
-
118
- useEffect(() => {
119
- pruneImageRefs(imageRefsRef.current, value)
120
- if (imageRefsRef.current.size === 0) nextImageRefIdRef.current = 1
121
- }, [value])
122
-
123
- useEffect(() => {
124
- const handleResize = () => {
125
- setColumns(stdout.columns ?? process.stdout.columns ?? 80)
126
- setRows(stdout.rows ?? process.stdout.rows ?? 24)
127
- }
128
- stdout.on('resize', handleResize)
129
- return () => {
130
- stdout.off('resize', handleResize)
131
- }
132
- }, [stdout])
133
-
134
- const applyBuffer = useCallback((next: ChatBuffer) => {
135
- bufferRef.current = next
136
- setBuffer(next)
137
- }, [])
138
-
139
- const applyHistoryState = useCallback((next: {
140
- historyIndex: number | null
141
- historyPreviewActive: boolean
142
- draftBuffer: ChatBuffer
143
- preferredColumn: number | null
144
- }) => {
145
- historyIndexRef.current = next.historyIndex
146
- draftBufferRef.current = next.draftBuffer
147
- historyPreviewActiveRef.current = next.historyPreviewActive
148
- preferredColumnRef.current = next.preferredColumn
149
- setHistoryIndex(next.historyIndex)
150
- setDraftBuffer(next.draftBuffer)
151
- setHistoryPreviewActive(next.historyPreviewActive)
152
- setPreferredColumn(next.preferredColumn)
153
- }, [])
154
-
155
- const applyTextEdit = useCallback((next: ChatBuffer) => {
156
- bufferRef.current = next
157
- setBuffer(next)
158
- applyHistoryState(exitHistoryPreview(next))
159
- }, [applyHistoryState])
160
-
161
- useEffect(() => {
162
- if (seedText == null) return
163
- const next: ChatBuffer = { value: seedText, cursor: seedText.length }
164
- bufferRef.current = next
165
- setBuffer(next)
166
- applyHistoryState(exitHistoryPreview(next))
167
- onSeedConsumed?.()
168
- }, [seedText, applyHistoryState, onSeedConsumed])
169
-
170
- useEffect(() => {
171
- if (!placeholderHints || placeholderHints.length < 2) return
172
- const timer = setInterval(() => {
173
- setPlaceholderIdx(i => (i + 1) % placeholderHints.length)
174
- }, PLACEHOLDER_ROTATE_MS)
175
- return () => clearInterval(timer)
176
- }, [placeholderHints])
177
-
178
- const showingSlash = value.startsWith('/') && !value.includes(' ') && slashSuggestions.length > 0
179
- const activeFileMention = useMemo(() => detectActiveFileMention(value, cursor), [value, cursor])
180
- const showingFiles = Boolean(activeFileMention && cwd && fileSuggestions.length > 0)
181
- const filteredSuggestions = useMemo(() => {
182
- if (!showingSlash) return []
183
- const prefixValue = value.slice(1).toLowerCase()
184
- return slashSuggestions.filter(s => s.name.toLowerCase().startsWith(prefixValue)).slice(0, 6)
185
- }, [showingSlash, value, slashSuggestions])
186
-
187
- useEffect(() => {
188
- if (suggestionIdx >= filteredSuggestions.length) setSuggestionIdx(0)
189
- }, [filteredSuggestions.length, suggestionIdx])
190
-
191
- useEffect(() => {
192
- if (!activeFileMention || !cwd) {
193
- setFileSuggestions([])
194
- setFileSuggestionIdx(0)
195
- return
196
- }
197
- let cancelled = false
198
- void (async () => {
199
- const suggestions = await listFileMentionSuggestions(cwd, activeFileMention)
200
- if (cancelled) return
201
- setFileSuggestions(suggestions)
202
- setFileSuggestionIdx(0)
203
- })()
204
- return () => { cancelled = true }
205
- }, [activeFileMention?.query, activeFileMention?.start, cwd])
206
-
207
- const insertText = useCallback((text: string) => {
208
- const prev = bufferRef.current
209
- const nextValue = (prev.value.slice(0, prev.cursor) + text + prev.value.slice(prev.cursor)).slice(0, MAX_LENGTH)
210
- const nextCursor = Math.min(prev.cursor + text.length, nextValue.length)
211
- applyTextEdit({ value: nextValue, cursor: nextCursor })
212
- }, [applyTextEdit])
213
-
214
- const completeFileMention = useCallback(() => {
215
- const picked = fileSuggestions[fileSuggestionIdx]
216
- if (!picked) return false
217
- const next = replaceActiveFileMention(bufferRef.current, picked.path)
218
- applyTextEdit(next)
219
- setPreferredColumn(null)
220
- return true
221
- }, [applyTextEdit, fileSuggestionIdx, fileSuggestions])
222
-
223
- const resetBuffer = useCallback(() => {
224
- applyBuffer(emptyBuffer())
225
- applyHistoryState({
226
- historyIndex: null,
227
- historyPreviewActive: false,
228
- draftBuffer: emptyBuffer(),
229
- preferredColumn: null,
230
- })
231
- pastedTextRefsRef.current.clear()
232
- nextPastedTextRefIdRef.current = 1
233
- imageRefsRef.current.clear()
234
- nextImageRefIdRef.current = 1
235
- }, [applyBuffer, applyHistoryState])
236
-
237
- const handlePaste = useCallback((text: string) => {
238
- const normalized = normalizePastedText(text)
239
- if (shouldCollapsePastedText(normalized, MAX_INLINE_PASTE_LINES)) {
240
- const id = nextPastedTextRefIdRef.current++
241
- pastedTextRefsRef.current.set(id, { id, content: normalized })
242
- insertText(formatPastedTextRef(id, normalized.length))
243
- return
244
- }
245
- insertText(normalized)
246
- }, [insertText])
247
-
248
- const flushPasteBuffer = useCallback(() => {
249
- if (pasteTimerRef.current) {
250
- clearTimeout(pasteTimerRef.current)
251
- pasteTimerRef.current = null
252
- }
253
- const text = pasteBufferRef.current
254
- pasteBufferRef.current = ''
255
- if (text) handlePaste(text)
256
- }, [handlePaste])
257
-
258
- const enqueuePasteChunk = useCallback((text: string) => {
259
- pasteBufferRef.current += text
260
- if (pasteBufferRef.current.length >= PASTE_FLUSH_LIMIT) {
261
- flushPasteBuffer()
262
- return
263
- }
264
- if (pasteTimerRef.current) clearTimeout(pasteTimerRef.current)
265
- pasteTimerRef.current = setTimeout(flushPasteBuffer, PASTE_BURST_MS)
266
- }, [flushPasteBuffer])
267
-
268
- useEffect(() => () => {
269
- if (pasteTimerRef.current) clearTimeout(pasteTimerRef.current)
270
- }, [])
271
-
272
- const submit = useCallback(() => {
273
- const trimmed = value.trim()
274
- if (!trimmed) return
275
- const withText = expandPastedTextRefs(trimmed, pastedTextRefsRef.current)
276
- onSubmit(expandImageRefs(withText, imageRefsRef.current))
277
- resetBuffer()
278
- }, [value, onSubmit, resetBuffer])
279
-
280
- const canNavigateHistory = useCallback(() => {
281
- return canNavigateHistoryState(
282
- bufferRef.current,
283
- history.length,
284
- historyIndexRef.current,
285
- historyPreviewActiveRef.current,
286
- )
287
- }, [history.length])
288
-
289
- const showPreviousHistory = useCallback((force = false) => {
290
- if (!force && !canNavigateHistory()) return
291
- if (history.length === 0) return
292
- const currentBuffer = bufferRef.current
293
- const currentHistoryIndex = historyIndexRef.current
294
- const currentDraftBuffer = draftBufferRef.current
295
- const currentPreferredColumn = preferredColumnRef.current
296
- if (currentHistoryIndex === null) {
297
- const next = beginHistoryPreview(currentBuffer, history, -1, currentPreferredColumn)
298
- if (!next) return
299
- applyHistoryState(next.preview)
300
- applyBuffer(next.buffer)
301
- return
302
- }
303
- const next = moveThroughHistory(history, currentHistoryIndex, -1, currentDraftBuffer, currentPreferredColumn)
304
- applyHistoryState(next.preview)
305
- applyBuffer(next.buffer)
306
- }, [applyBuffer, applyHistoryState, canNavigateHistory, history])
307
-
308
- const showNextHistory = useCallback((force = false) => {
309
- const currentHistoryIndex = historyIndexRef.current
310
- if (!force && currentHistoryIndex === null) return
311
- if (currentHistoryIndex === null) return
312
- const next = moveThroughHistory(
313
- history,
314
- currentHistoryIndex,
315
- 1,
316
- draftBufferRef.current,
317
- preferredColumnRef.current,
318
- )
319
- applyHistoryState(next.preview)
320
- applyBuffer(next.buffer)
321
- }, [applyBuffer, applyHistoryState, history])
322
-
323
- const wrapWidth = inputWrapWidth(columns)
324
- const maxVisibleInputLines = Math.max(MIN_INPUT_VIEWPORT_LINES, Math.floor(rows / 2) - PROMPT_FOOTER_LINES)
325
-
326
- useAppInput((input, key, event) => {
327
- const pastePending = pasteTimerRef.current !== null
328
- if (event.isPasted || pastePending || isFallbackPasteInput(input)) {
329
- if (input) enqueuePasteChunk(input)
330
- return
331
- }
332
- const inputText = input
333
-
334
- const wantsSoftBreak = isSoftBreak(key)
335
-
336
- if (showingFiles) {
337
- if (key.tab || (key.return && !wantsSoftBreak)) {
338
- if (completeFileMention()) return
339
- }
340
- if (key.upArrow) {
341
- setFileSuggestionIdx(i => Math.max(0, i - 1))
342
- return
343
- }
344
- if (key.downArrow) {
345
- setFileSuggestionIdx(i => Math.min(fileSuggestions.length - 1, i + 1))
346
- return
347
- }
348
- }
349
-
350
- if (showingSlash && filteredSuggestions.length > 0) {
351
- if (key.tab || (key.return && filteredSuggestions.length > 0 && !wantsSoftBreak)) {
352
- const picked = filteredSuggestions[suggestionIdx]
353
- if (picked && key.tab) {
354
- const next = picked.completion
355
- applyTextEdit({ value: next, cursor: next.length })
356
- setPreferredColumn(null)
357
- return
358
- }
359
- if (picked && key.return && !wantsSoftBreak) {
360
- const next = picked.completion
361
- if (picked.executeOnEnter) {
362
- onSubmit(next)
363
- resetBuffer()
364
- } else {
365
- applyTextEdit({ value: next, cursor: next.length })
366
- setPreferredColumn(null)
367
- }
368
- return
369
- }
370
- }
371
- if (key.upArrow) {
372
- setSuggestionIdx(i => Math.max(0, i - 1))
373
- return
374
- }
375
- if (key.downArrow) {
376
- setSuggestionIdx(i => Math.min(filteredSuggestions.length - 1, i + 1))
377
- return
378
- }
379
- }
380
-
381
- if (wantsSoftBreak) {
382
- insertText('\n')
383
- return
384
- }
385
- if (key.return) {
386
- submit()
387
- return
388
- }
389
- if (key.escape && mode === 'bash') {
390
- onModeChange?.('prompt')
391
- return
392
- }
393
- if (!value && inputText === '!' && mode === 'prompt' && onModeChange) {
394
- onModeChange('bash')
395
- return
396
- }
397
- if (key.ctrl && inputText === 'u') {
398
- applyTextEdit(deleteToLineStart(bufferRef.current, wrapWidth))
399
- setPreferredColumn(null)
400
- return
401
- }
402
- if (key.meta && inputText === 'v') {
403
- void (async () => {
404
- const image = await readClipboardImage()
405
- if (image.ok) {
406
- const id = nextImageRefIdRef.current++
407
- imageRefsRef.current.set(id, { path: image.path })
408
- insertText(formatImageRefMarker(id))
409
- onImagePaste?.(image.path)
410
- }
411
- })()
412
- return
413
- }
414
- if (key.ctrl && inputText === 'a') {
415
- applyBuffer({ value: bufferRef.current.value, cursor: 0 })
416
- setPreferredColumn(null)
417
- return
418
- }
419
- if (key.ctrl && inputText === 'e') {
420
- const currentBuffer = bufferRef.current
421
- applyBuffer({ value: currentBuffer.value, cursor: currentBuffer.value.length })
422
- setPreferredColumn(null)
423
- return
424
- }
425
- if (key.ctrl && inputText === 'k') {
426
- const currentBuffer = bufferRef.current
427
- applyTextEdit({ value: currentBuffer.value.slice(0, currentBuffer.cursor), cursor: currentBuffer.cursor })
428
- setPreferredColumn(null)
429
- return
430
- }
431
- if (key.ctrl && inputText === 'w') {
432
- const currentBuffer = bufferRef.current
433
- const left = currentBuffer.value.slice(0, currentBuffer.cursor)
434
- const right = currentBuffer.value.slice(currentBuffer.cursor)
435
- const newLeft = left.replace(/\S+\s*$/, '')
436
- applyTextEdit({ value: newLeft + right, cursor: newLeft.length })
437
- setPreferredColumn(null)
438
- return
439
- }
440
- if (key.ctrl && inputText === 'p') {
441
- showPreviousHistory()
442
- return
443
- }
444
- if (key.ctrl && inputText === 'n') {
445
- showNextHistory()
446
- return
447
- }
448
-
449
- if (key.leftArrow) {
450
- const currentBuffer = bufferRef.current
451
- applyBuffer({ value: currentBuffer.value, cursor: Math.max(0, currentBuffer.cursor - 1) })
452
- setPreferredColumn(null)
453
- return
454
- }
455
- if (key.rightArrow) {
456
- const currentBuffer = bufferRef.current
457
- applyBuffer({
458
- value: currentBuffer.value,
459
- cursor: Math.min(currentBuffer.value.length, currentBuffer.cursor + 1),
460
- })
461
- setPreferredColumn(null)
462
- return
463
- }
464
- if (key.upArrow) {
465
- const currentBuffer = bufferRef.current
466
- const nextMove = moveVerticalVisual(
467
- currentBuffer.value,
468
- currentBuffer.cursor,
469
- -1,
470
- wrapWidth,
471
- preferredColumnRef.current,
472
- )
473
- preferredColumnRef.current = nextMove.preferredColumn
474
- setPreferredColumn(nextMove.preferredColumn)
475
- if (nextMove.kind === 'moved') {
476
- applyBuffer({ value: currentBuffer.value, cursor: nextMove.cursor })
477
- return
478
- }
479
- if (nextMove.kind === 'boundary-top') {
480
- if (historyPreviewActiveRef.current || historyIndexRef.current !== null || canNavigateHistory()) {
481
- showPreviousHistory(true)
482
- }
483
- return
484
- }
485
- return
486
- }
487
- if (key.downArrow) {
488
- const currentBuffer = bufferRef.current
489
- const nextMove = moveVerticalVisual(
490
- currentBuffer.value,
491
- currentBuffer.cursor,
492
- 1,
493
- wrapWidth,
494
- preferredColumnRef.current,
495
- )
496
- preferredColumnRef.current = nextMove.preferredColumn
497
- setPreferredColumn(nextMove.preferredColumn)
498
- if (nextMove.kind === 'moved') {
499
- applyBuffer({ value: currentBuffer.value, cursor: nextMove.cursor })
500
- return
501
- }
502
- if (nextMove.kind === 'boundary-bottom') {
503
- if (historyPreviewActiveRef.current || historyIndexRef.current !== null) {
504
- showNextHistory(true)
505
- }
506
- return
507
- }
508
- return
509
- }
510
- if (key.backspace || key.delete) {
511
- const currentBuffer = bufferRef.current
512
- if (currentBuffer.cursor === 0) return
513
- applyTextEdit({
514
- value: currentBuffer.value.slice(0, currentBuffer.cursor - 1) + currentBuffer.value.slice(currentBuffer.cursor),
515
- cursor: Math.max(0, currentBuffer.cursor - 1),
516
- })
517
- return
518
- }
519
- if (key.tab || key.escape) return
520
- if (key.ctrl || key.meta) return
521
- if (key.return || key.backspace || key.delete) return
522
- if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) return
523
- if (key.pageUp || key.pageDown || key.home || key.end) return
524
-
525
- if (inputText) {
526
- insertText(inputText)
527
- setPreferredColumn(null)
528
- }
529
- })
530
-
531
- const showPlaceholder = value.length === 0 && !disabled && placeholderHints && placeholderHints.length > 0
532
- const placeholder = showPlaceholder ? (placeholderHints[placeholderIdx] ?? placeholderHints[0] ?? '') : ''
533
- const promptColor = mode === 'bash' ? theme.accentPeriwinkle : (disabled ? theme.dim : theme.accentPeriwinkle)
534
- const promptChar = mode === 'bash' ? '!' : prefix
535
-
536
- const display = useMemo(
537
- () => renderWithCursor(value, cursor, !disabled, wrapWidth, maxVisibleInputLines),
538
- [value, cursor, disabled, wrapWidth, maxVisibleInputLines],
539
- )
540
- const borderColor = disabled ? theme.border : theme.accentPeriwinkle
541
-
542
- return (
543
- <Box flexDirection="column" width="100%">
544
- {queuedMessages.length > 0 ? (
545
- <Box marginLeft={2} marginBottom={1} flexDirection="column">
546
- <Text color={theme.dim}>
547
- {queuedMessages.length === 1 ? '1 message queued for next turn' : `${queuedMessages.length} messages queued for next turns`}
548
- </Text>
549
- {queuedMessages.slice(0, 3).map((message, i) => (
550
- <Text key={`${i}-${message.slice(0, 24)}`}>
551
- <Text color={theme.accentPeriwinkle}>{i === 0 ? '» ' : ' '}</Text>
552
- <Text color={theme.textSubtle}>{summarizeQueuedMessage(message)}</Text>
553
- </Text>
554
- ))}
555
- {queuedMessages.length > 3 ? (
556
- <Text color={theme.dim}>+{queuedMessages.length - 3} more</Text>
557
- ) : null}
558
- </Box>
559
- ) : null}
560
- <Box
561
- borderStyle="round"
562
- borderColor={borderColor}
563
- paddingX={2}
564
- width="100%"
565
- flexDirection="column"
566
- >
567
- {showPlaceholder ? (
568
- <Text>
569
- <Text color={promptColor}>{promptChar} </Text>
570
- <Text color={theme.dim}>{placeholder}</Text>
571
- </Text>
572
- ) : (
573
- <>
574
- {display.hiddenAbove > 0 ? (
575
- <Text color={theme.dim}>{` ↑ ${display.hiddenAbove} earlier line${display.hiddenAbove === 1 ? '' : 's'}`}</Text>
576
- ) : null}
577
- <Box flexDirection="column" height={display.visibleLineCount} overflowY="hidden">
578
- {display.lines.map(line => (
579
- <Box key={line.visualLineIndex} flexDirection="row">
580
- {line.visualLineIndex === 0 ? (
581
- <Text color={promptColor}>{promptChar} </Text>
582
- ) : (
583
- <Text color={theme.dim}>{' '}</Text>
584
- )}
585
- <Box width={wrapWidth}>{line.node}</Box>
586
- </Box>
587
- ))}
588
- </Box>
589
- {display.hiddenBelow > 0 ? (
590
- <Text color={theme.dim}>{` ↓ ${display.hiddenBelow} later line${display.hiddenBelow === 1 ? '' : 's'}`}</Text>
591
- ) : null}
592
- </>
593
- )}
594
- </Box>
595
- {showingSlash && filteredSuggestions.length > 0 ? (
596
- <Box marginLeft={2} flexDirection="column">
597
- {filteredSuggestions.map((s, i) => (
598
- <Text key={s.name} color={i === suggestionIdx ? theme.accentPeriwinkle : theme.dim}>
599
- {i === suggestionIdx ? '› ' : ' '}/{s.name}
600
- <Text color={theme.dim}> {s.summary}{i === suggestionIdx ? (s.executeOnEnter ? ' · enter runs' : ' · enter fills') : ''}</Text>
601
- </Text>
602
- ))}
603
- </Box>
604
- ) : null}
605
- {showingFiles ? (
606
- <Box marginLeft={2} flexDirection="column">
607
- {fileSuggestions.slice(0, 8).map((s, i) => (
608
- <Text key={s.path} color={i === fileSuggestionIdx ? theme.accentPeriwinkle : theme.dim}>
609
- {i === fileSuggestionIdx ? '› ' : ' '}@{s.path}
610
- <Text color={theme.dim}> {i === fileSuggestionIdx ? 'tab/enter completes' : s.hint}</Text>
611
- </Text>
612
- ))}
613
- {fileSuggestions.length > 8 ? (
614
- <Text color={theme.dim}>+{fileSuggestions.length - 8} more matches</Text>
615
- ) : null}
616
- </Box>
617
- ) : null}
618
- {footerRight ? (
619
- <Box marginLeft={2}>
620
- {footerRight}
621
- </Box>
622
- ) : null}
623
- </Box>
624
- )
625
- }
@@ -1,62 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
- import {
4
- countPastedTextLineBreaks,
5
- LARGE_PASTE_THRESHOLD,
6
- normalizePastedText,
7
- } from './chatPaste.js'
8
- import type { FileMentionToken } from './chatInputState.js'
9
-
10
- const MAX_INLINE_PASTE_LINES = 2
11
-
12
- export function isSoftBreak(key: { return: boolean; meta?: boolean; shift?: boolean }): boolean {
13
- return key.return && Boolean(key.meta || key.shift)
14
- }
15
-
16
- export function isFallbackPasteInput(input: string): boolean {
17
- if (!input) return false
18
- return input.length > LARGE_PASTE_THRESHOLD
19
- || countPastedTextLineBreaks(normalizePastedText(input)) > MAX_INLINE_PASTE_LINES
20
- }
21
-
22
- export function summarizeQueuedMessage(text: string): string {
23
- const normalized = text.replace(/\s+/g, ' ').trim()
24
- if (!normalized) return ''
25
- if (normalized.length <= 72) return normalized
26
- return `${normalized.slice(0, 69)}...`
27
- }
28
-
29
- export type FileMentionSuggestion = {
30
- path: string
31
- hint: string
32
- }
33
-
34
- export async function listFileMentionSuggestions(
35
- cwd: string,
36
- mention: FileMentionToken,
37
- ): Promise<FileMentionSuggestion[]> {
38
- const query = mention.query.replace(/\\/g, '/')
39
- const lastSlash = query.lastIndexOf('/')
40
- const queryDir = lastSlash >= 0 ? query.slice(0, lastSlash + 1) : ''
41
- const basenameQuery = lastSlash >= 0 ? query.slice(lastSlash + 1).toLowerCase() : query.toLowerCase()
42
- const baseDir = path.resolve(cwd, queryDir || '.')
43
-
44
- let entries: Array<{ name: string; isFile: () => boolean }>
45
- try {
46
- entries = await fs.readdir(baseDir, { withFileTypes: true })
47
- } catch {
48
- return []
49
- }
50
-
51
- return entries
52
- .filter(entry => entry.isFile() && entry.name.toLowerCase().startsWith(basenameQuery))
53
- .sort((left, right) => left.name.localeCompare(right.name))
54
- .slice(0, 32)
55
- .map(entry => {
56
- const relative = (queryDir + entry.name).replace(/\\/g, '/')
57
- return {
58
- path: relative,
59
- hint: path.extname(entry.name).slice(1) || 'file',
60
- }
61
- })
62
- }