@shareai-lab/kode 1.1.14 → 1.1.16-dev.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 (289) hide show
  1. package/cli.js +77 -82
  2. package/dist/entrypoints/cli.js +59 -38
  3. package/dist/entrypoints/cli.js.map +3 -3
  4. package/dist/index.js +5 -26
  5. package/dist/package.json +4 -1
  6. package/package.json +11 -104
  7. package/dist/test/testAdapters.js +0 -88
  8. package/dist/test/testAdapters.js.map +0 -1
  9. package/src/ProjectOnboarding.tsx +0 -198
  10. package/src/Tool.ts +0 -83
  11. package/src/commands/agents.tsx +0 -3416
  12. package/src/commands/approvedTools.ts +0 -53
  13. package/src/commands/bug.tsx +0 -20
  14. package/src/commands/clear.ts +0 -43
  15. package/src/commands/compact.ts +0 -120
  16. package/src/commands/config.tsx +0 -19
  17. package/src/commands/cost.ts +0 -18
  18. package/src/commands/ctx_viz.ts +0 -209
  19. package/src/commands/doctor.ts +0 -24
  20. package/src/commands/help.tsx +0 -19
  21. package/src/commands/init.ts +0 -37
  22. package/src/commands/listen.ts +0 -42
  23. package/src/commands/login.tsx +0 -51
  24. package/src/commands/logout.tsx +0 -40
  25. package/src/commands/mcp.ts +0 -41
  26. package/src/commands/model.tsx +0 -40
  27. package/src/commands/modelstatus.tsx +0 -20
  28. package/src/commands/onboarding.tsx +0 -34
  29. package/src/commands/pr_comments.ts +0 -59
  30. package/src/commands/refreshCommands.ts +0 -54
  31. package/src/commands/release-notes.ts +0 -34
  32. package/src/commands/resume.tsx +0 -31
  33. package/src/commands/review.ts +0 -49
  34. package/src/commands/terminalSetup.ts +0 -221
  35. package/src/commands.ts +0 -139
  36. package/src/components/ApproveApiKey.tsx +0 -93
  37. package/src/components/AsciiLogo.tsx +0 -13
  38. package/src/components/AutoUpdater.tsx +0 -148
  39. package/src/components/Bug.tsx +0 -367
  40. package/src/components/Config.tsx +0 -293
  41. package/src/components/ConsoleOAuthFlow.tsx +0 -327
  42. package/src/components/Cost.tsx +0 -23
  43. package/src/components/CostThresholdDialog.tsx +0 -46
  44. package/src/components/CustomSelect/option-map.ts +0 -42
  45. package/src/components/CustomSelect/select-option.tsx +0 -78
  46. package/src/components/CustomSelect/select.tsx +0 -152
  47. package/src/components/CustomSelect/theme.ts +0 -45
  48. package/src/components/CustomSelect/use-select-state.ts +0 -414
  49. package/src/components/CustomSelect/use-select.ts +0 -35
  50. package/src/components/FallbackToolUseRejectedMessage.tsx +0 -15
  51. package/src/components/FileEditToolUpdatedMessage.tsx +0 -66
  52. package/src/components/Help.tsx +0 -215
  53. package/src/components/HighlightedCode.tsx +0 -33
  54. package/src/components/InvalidConfigDialog.tsx +0 -113
  55. package/src/components/Link.tsx +0 -32
  56. package/src/components/LogSelector.tsx +0 -86
  57. package/src/components/Logo.tsx +0 -170
  58. package/src/components/MCPServerApprovalDialog.tsx +0 -100
  59. package/src/components/MCPServerDialogCopy.tsx +0 -25
  60. package/src/components/MCPServerMultiselectDialog.tsx +0 -109
  61. package/src/components/Message.tsx +0 -221
  62. package/src/components/MessageResponse.tsx +0 -15
  63. package/src/components/MessageSelector.tsx +0 -211
  64. package/src/components/ModeIndicator.tsx +0 -88
  65. package/src/components/ModelConfig.tsx +0 -301
  66. package/src/components/ModelListManager.tsx +0 -227
  67. package/src/components/ModelSelector.tsx +0 -3387
  68. package/src/components/ModelStatusDisplay.tsx +0 -230
  69. package/src/components/Onboarding.tsx +0 -274
  70. package/src/components/PressEnterToContinue.tsx +0 -11
  71. package/src/components/PromptInput.tsx +0 -760
  72. package/src/components/SentryErrorBoundary.ts +0 -39
  73. package/src/components/Spinner.tsx +0 -129
  74. package/src/components/StickerRequestForm.tsx +0 -16
  75. package/src/components/StructuredDiff.tsx +0 -191
  76. package/src/components/TextInput.tsx +0 -259
  77. package/src/components/TodoItem.tsx +0 -47
  78. package/src/components/TokenWarning.tsx +0 -31
  79. package/src/components/ToolUseLoader.tsx +0 -40
  80. package/src/components/TrustDialog.tsx +0 -106
  81. package/src/components/binary-feedback/BinaryFeedback.tsx +0 -63
  82. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +0 -111
  83. package/src/components/binary-feedback/BinaryFeedbackView.tsx +0 -172
  84. package/src/components/binary-feedback/utils.ts +0 -220
  85. package/src/components/messages/AssistantBashOutputMessage.tsx +0 -22
  86. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +0 -49
  87. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +0 -19
  88. package/src/components/messages/AssistantTextMessage.tsx +0 -144
  89. package/src/components/messages/AssistantThinkingMessage.tsx +0 -40
  90. package/src/components/messages/AssistantToolUseMessage.tsx +0 -132
  91. package/src/components/messages/TaskProgressMessage.tsx +0 -32
  92. package/src/components/messages/TaskToolMessage.tsx +0 -58
  93. package/src/components/messages/UserBashInputMessage.tsx +0 -28
  94. package/src/components/messages/UserCommandMessage.tsx +0 -30
  95. package/src/components/messages/UserKodingInputMessage.tsx +0 -28
  96. package/src/components/messages/UserPromptMessage.tsx +0 -35
  97. package/src/components/messages/UserTextMessage.tsx +0 -39
  98. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +0 -12
  99. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +0 -36
  100. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +0 -31
  101. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +0 -57
  102. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +0 -35
  103. package/src/components/messages/UserToolResultMessage/utils.tsx +0 -56
  104. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +0 -121
  105. package/src/components/permissions/FallbackPermissionRequest.tsx +0 -153
  106. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +0 -182
  107. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +0 -77
  108. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +0 -164
  109. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +0 -83
  110. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +0 -240
  111. package/src/components/permissions/PermissionRequest.tsx +0 -101
  112. package/src/components/permissions/PermissionRequestTitle.tsx +0 -69
  113. package/src/components/permissions/hooks.ts +0 -44
  114. package/src/components/permissions/toolUseOptions.ts +0 -59
  115. package/src/components/permissions/utils.ts +0 -23
  116. package/src/constants/betas.ts +0 -5
  117. package/src/constants/claude-asterisk-ascii-art.tsx +0 -238
  118. package/src/constants/figures.ts +0 -4
  119. package/src/constants/keys.ts +0 -3
  120. package/src/constants/macros.ts +0 -11
  121. package/src/constants/modelCapabilities.ts +0 -179
  122. package/src/constants/models.ts +0 -1025
  123. package/src/constants/oauth.ts +0 -18
  124. package/src/constants/product.ts +0 -17
  125. package/src/constants/prompts.ts +0 -168
  126. package/src/constants/releaseNotes.ts +0 -7
  127. package/src/context/PermissionContext.tsx +0 -149
  128. package/src/context.ts +0 -278
  129. package/src/cost-tracker.ts +0 -84
  130. package/src/entrypoints/cli.tsx +0 -1561
  131. package/src/entrypoints/mcp.ts +0 -175
  132. package/src/history.ts +0 -25
  133. package/src/hooks/useApiKeyVerification.ts +0 -59
  134. package/src/hooks/useArrowKeyHistory.ts +0 -55
  135. package/src/hooks/useCanUseTool.ts +0 -138
  136. package/src/hooks/useCancelRequest.ts +0 -39
  137. package/src/hooks/useDoublePress.ts +0 -41
  138. package/src/hooks/useExitOnCtrlCD.ts +0 -31
  139. package/src/hooks/useInterval.ts +0 -25
  140. package/src/hooks/useLogMessages.ts +0 -16
  141. package/src/hooks/useLogStartupTime.ts +0 -12
  142. package/src/hooks/useNotifyAfterTimeout.ts +0 -65
  143. package/src/hooks/usePermissionRequestLogging.ts +0 -44
  144. package/src/hooks/useTerminalSize.ts +0 -49
  145. package/src/hooks/useTextInput.ts +0 -317
  146. package/src/hooks/useUnifiedCompletion.ts +0 -1405
  147. package/src/index.ts +0 -34
  148. package/src/messages.ts +0 -38
  149. package/src/permissions.ts +0 -268
  150. package/src/query.ts +0 -720
  151. package/src/screens/ConfigureNpmPrefix.tsx +0 -197
  152. package/src/screens/Doctor.tsx +0 -219
  153. package/src/screens/LogList.tsx +0 -68
  154. package/src/screens/REPL.tsx +0 -813
  155. package/src/screens/ResumeConversation.tsx +0 -68
  156. package/src/services/adapters/base.ts +0 -38
  157. package/src/services/adapters/chatCompletions.ts +0 -90
  158. package/src/services/adapters/responsesAPI.ts +0 -170
  159. package/src/services/browserMocks.ts +0 -66
  160. package/src/services/claude.ts +0 -2197
  161. package/src/services/customCommands.ts +0 -704
  162. package/src/services/fileFreshness.ts +0 -377
  163. package/src/services/gpt5ConnectionTest.ts +0 -340
  164. package/src/services/mcpClient.ts +0 -564
  165. package/src/services/mcpServerApproval.tsx +0 -50
  166. package/src/services/mentionProcessor.ts +0 -273
  167. package/src/services/modelAdapterFactory.ts +0 -69
  168. package/src/services/notifier.ts +0 -40
  169. package/src/services/oauth.ts +0 -357
  170. package/src/services/openai.ts +0 -1359
  171. package/src/services/responseStateManager.ts +0 -90
  172. package/src/services/sentry.ts +0 -3
  173. package/src/services/statsig.ts +0 -172
  174. package/src/services/statsigStorage.ts +0 -86
  175. package/src/services/systemReminder.ts +0 -507
  176. package/src/services/vcr.ts +0 -161
  177. package/src/test/testAdapters.ts +0 -96
  178. package/src/tools/ArchitectTool/ArchitectTool.tsx +0 -135
  179. package/src/tools/ArchitectTool/prompt.ts +0 -15
  180. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +0 -576
  181. package/src/tools/BashTool/BashTool.tsx +0 -243
  182. package/src/tools/BashTool/BashToolResultMessage.tsx +0 -38
  183. package/src/tools/BashTool/OutputLine.tsx +0 -49
  184. package/src/tools/BashTool/prompt.ts +0 -174
  185. package/src/tools/BashTool/utils.ts +0 -56
  186. package/src/tools/FileEditTool/FileEditTool.tsx +0 -319
  187. package/src/tools/FileEditTool/prompt.ts +0 -51
  188. package/src/tools/FileEditTool/utils.ts +0 -58
  189. package/src/tools/FileReadTool/FileReadTool.tsx +0 -404
  190. package/src/tools/FileReadTool/prompt.ts +0 -7
  191. package/src/tools/FileWriteTool/FileWriteTool.tsx +0 -301
  192. package/src/tools/FileWriteTool/prompt.ts +0 -10
  193. package/src/tools/GlobTool/GlobTool.tsx +0 -119
  194. package/src/tools/GlobTool/prompt.ts +0 -8
  195. package/src/tools/GrepTool/GrepTool.tsx +0 -147
  196. package/src/tools/GrepTool/prompt.ts +0 -11
  197. package/src/tools/MCPTool/MCPTool.tsx +0 -107
  198. package/src/tools/MCPTool/prompt.ts +0 -3
  199. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +0 -127
  200. package/src/tools/MemoryReadTool/prompt.ts +0 -3
  201. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +0 -89
  202. package/src/tools/MemoryWriteTool/prompt.ts +0 -3
  203. package/src/tools/MultiEditTool/MultiEditTool.tsx +0 -388
  204. package/src/tools/MultiEditTool/prompt.ts +0 -45
  205. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +0 -298
  206. package/src/tools/NotebookEditTool/prompt.ts +0 -3
  207. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +0 -258
  208. package/src/tools/NotebookReadTool/prompt.ts +0 -3
  209. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +0 -107
  210. package/src/tools/StickerRequestTool/prompt.ts +0 -19
  211. package/src/tools/TaskTool/TaskTool.tsx +0 -438
  212. package/src/tools/TaskTool/constants.ts +0 -1
  213. package/src/tools/TaskTool/prompt.ts +0 -92
  214. package/src/tools/ThinkTool/ThinkTool.tsx +0 -54
  215. package/src/tools/ThinkTool/prompt.ts +0 -12
  216. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +0 -313
  217. package/src/tools/TodoWriteTool/prompt.ts +0 -63
  218. package/src/tools/URLFetcherTool/URLFetcherTool.tsx +0 -178
  219. package/src/tools/URLFetcherTool/cache.ts +0 -55
  220. package/src/tools/URLFetcherTool/htmlToMarkdown.ts +0 -55
  221. package/src/tools/URLFetcherTool/prompt.ts +0 -17
  222. package/src/tools/WebSearchTool/WebSearchTool.tsx +0 -103
  223. package/src/tools/WebSearchTool/prompt.ts +0 -13
  224. package/src/tools/WebSearchTool/searchProviders.ts +0 -66
  225. package/src/tools/lsTool/lsTool.tsx +0 -272
  226. package/src/tools/lsTool/prompt.ts +0 -2
  227. package/src/tools.ts +0 -67
  228. package/src/types/PermissionMode.ts +0 -120
  229. package/src/types/RequestContext.ts +0 -72
  230. package/src/types/common.d.ts +0 -2
  231. package/src/types/conversation.ts +0 -51
  232. package/src/types/logs.ts +0 -58
  233. package/src/types/modelCapabilities.ts +0 -64
  234. package/src/types/notebook.ts +0 -87
  235. package/src/utils/Cursor.ts +0 -436
  236. package/src/utils/PersistentShell.ts +0 -552
  237. package/src/utils/advancedFuzzyMatcher.ts +0 -290
  238. package/src/utils/agentLoader.ts +0 -278
  239. package/src/utils/agentStorage.ts +0 -97
  240. package/src/utils/array.ts +0 -3
  241. package/src/utils/ask.tsx +0 -99
  242. package/src/utils/auth.ts +0 -13
  243. package/src/utils/autoCompactCore.ts +0 -223
  244. package/src/utils/autoUpdater.ts +0 -458
  245. package/src/utils/betas.ts +0 -20
  246. package/src/utils/browser.ts +0 -14
  247. package/src/utils/cleanup.ts +0 -72
  248. package/src/utils/commands.ts +0 -261
  249. package/src/utils/commonUnixCommands.ts +0 -161
  250. package/src/utils/config.ts +0 -945
  251. package/src/utils/conversationRecovery.ts +0 -55
  252. package/src/utils/debugLogger.ts +0 -1235
  253. package/src/utils/diff.ts +0 -42
  254. package/src/utils/env.ts +0 -57
  255. package/src/utils/errors.ts +0 -21
  256. package/src/utils/exampleCommands.ts +0 -109
  257. package/src/utils/execFileNoThrow.ts +0 -51
  258. package/src/utils/expertChatStorage.ts +0 -136
  259. package/src/utils/file.ts +0 -405
  260. package/src/utils/fileRecoveryCore.ts +0 -71
  261. package/src/utils/format.tsx +0 -44
  262. package/src/utils/fuzzyMatcher.ts +0 -328
  263. package/src/utils/generators.ts +0 -62
  264. package/src/utils/git.ts +0 -92
  265. package/src/utils/globalLogger.ts +0 -77
  266. package/src/utils/http.ts +0 -10
  267. package/src/utils/imagePaste.ts +0 -38
  268. package/src/utils/json.ts +0 -13
  269. package/src/utils/log.ts +0 -382
  270. package/src/utils/markdown.ts +0 -213
  271. package/src/utils/messageContextManager.ts +0 -294
  272. package/src/utils/messages.tsx +0 -945
  273. package/src/utils/model.ts +0 -914
  274. package/src/utils/permissions/filesystem.ts +0 -127
  275. package/src/utils/responseState.ts +0 -23
  276. package/src/utils/ripgrep.ts +0 -167
  277. package/src/utils/secureFile.ts +0 -564
  278. package/src/utils/sessionState.ts +0 -49
  279. package/src/utils/state.ts +0 -25
  280. package/src/utils/style.ts +0 -29
  281. package/src/utils/terminal.ts +0 -50
  282. package/src/utils/theme.ts +0 -127
  283. package/src/utils/thinking.ts +0 -144
  284. package/src/utils/todoStorage.ts +0 -431
  285. package/src/utils/tokens.ts +0 -43
  286. package/src/utils/toolExecutionController.ts +0 -163
  287. package/src/utils/unaryLogging.ts +0 -26
  288. package/src/utils/user.ts +0 -37
  289. package/src/utils/validate.ts +0 -165
@@ -1,1405 +0,0 @@
1
- import { useState, useCallback, useEffect, useRef } from 'react'
2
- import { useInput } from 'ink'
3
- import { existsSync, statSync, readdirSync } from 'fs'
4
- import { join, dirname, basename, resolve } from 'path'
5
- import { getCwd } from '../utils/state'
6
- import { getCommand } from '../commands'
7
- import { getActiveAgents } from '../utils/agentLoader'
8
- import { getModelManager } from '../utils/model'
9
- import { glob } from 'glob'
10
- import { matchCommands } from '../utils/fuzzyMatcher'
11
- import {
12
- getCommonSystemCommands,
13
- getCommandPriority,
14
- getEssentialCommands,
15
- getMinimalFallbackCommands
16
- } from '../utils/commonUnixCommands'
17
- import type { Command } from '../commands'
18
-
19
- // Unified suggestion type for all completion types
20
- export interface UnifiedSuggestion {
21
- value: string
22
- displayValue: string
23
- type: 'command' | 'agent' | 'file' | 'ask'
24
- icon?: string
25
- score: number
26
- metadata?: any
27
- // Clean type system for smart matching
28
- isSmartMatch?: boolean // Instead of magic string checking
29
- originalContext?: 'mention' | 'file' | 'command' // Track source context
30
- }
31
-
32
- interface CompletionContext {
33
- type: 'command' | 'agent' | 'file' | null
34
- prefix: string
35
- startPos: number
36
- endPos: number
37
- }
38
-
39
- // Terminal behavior state for preview and cycling
40
- interface TerminalState {
41
- originalWord: string
42
- wordContext: { start: number; end: number } | null
43
- isPreviewMode: boolean
44
- }
45
-
46
- interface Props {
47
- input: string
48
- cursorOffset: number
49
- onInputChange: (value: string) => void
50
- setCursorOffset: (offset: number) => void
51
- commands: Command[]
52
- onSubmit?: (value: string, isSubmittingSlashCommand?: boolean) => void
53
- }
54
-
55
- /**
56
- * Unified completion system - Linus approved
57
- * One hook to rule them all, no bullshit, no complexity
58
- */
59
- // Unified completion state - single source of truth
60
- interface CompletionState {
61
- suggestions: UnifiedSuggestion[]
62
- selectedIndex: number
63
- isActive: boolean
64
- context: CompletionContext | null
65
- preview: {
66
- isActive: boolean
67
- originalInput: string
68
- wordRange: [number, number]
69
- } | null
70
- emptyDirMessage: string
71
- suppressUntil: number // timestamp for suppression
72
- }
73
-
74
- const INITIAL_STATE: CompletionState = {
75
- suggestions: [],
76
- selectedIndex: 0,
77
- isActive: false,
78
- context: null,
79
- preview: null,
80
- emptyDirMessage: '',
81
- suppressUntil: 0
82
- }
83
-
84
- export function useUnifiedCompletion({
85
- input,
86
- cursorOffset,
87
- onInputChange,
88
- setCursorOffset,
89
- commands,
90
- onSubmit,
91
- }: Props) {
92
- // Single state for entire completion system - Linus approved
93
- const [state, setState] = useState<CompletionState>(INITIAL_STATE)
94
-
95
- // State update helpers - clean and simple
96
- const updateState = useCallback((updates: Partial<CompletionState>) => {
97
- setState(prev => ({ ...prev, ...updates }))
98
- }, [])
99
-
100
- const resetCompletion = useCallback(() => {
101
- setState(prev => ({
102
- ...prev,
103
- suggestions: [],
104
- selectedIndex: 0,
105
- isActive: false,
106
- context: null,
107
- preview: null,
108
- emptyDirMessage: ''
109
- }))
110
- }, [])
111
-
112
- const activateCompletion = useCallback((suggestions: UnifiedSuggestion[], context: CompletionContext) => {
113
- setState(prev => ({
114
- ...prev,
115
- suggestions: suggestions, // Keep the order from generateSuggestions (already sorted with weights)
116
- selectedIndex: 0,
117
- isActive: true,
118
- context,
119
- preview: null
120
- }))
121
- }, [])
122
-
123
- // Direct state access - no legacy wrappers needed
124
- const { suggestions, selectedIndex, isActive, emptyDirMessage } = state
125
-
126
- // Find common prefix among suggestions (terminal behavior)
127
- const findCommonPrefix = useCallback((suggestions: UnifiedSuggestion[]): string => {
128
- if (suggestions.length === 0) return ''
129
- if (suggestions.length === 1) return suggestions[0].value
130
-
131
- let prefix = suggestions[0].value
132
-
133
- for (let i = 1; i < suggestions.length; i++) {
134
- const str = suggestions[i].value
135
- let j = 0
136
- while (j < prefix.length && j < str.length && prefix[j] === str[j]) {
137
- j++
138
- }
139
- prefix = prefix.slice(0, j)
140
-
141
- if (prefix.length === 0) return ''
142
- }
143
-
144
- return prefix
145
- }, [])
146
-
147
- // Clean word detection - Linus approved simplicity
148
- const getWordAtCursor = useCallback((): CompletionContext | null => {
149
- if (!input) return null
150
-
151
- // IMPORTANT: Only match the word/prefix BEFORE the cursor
152
- // Don't include text after cursor to avoid confusion
153
- let start = cursorOffset
154
-
155
- // Move start backwards to find word beginning
156
- // Stop at whitespace or special boundaries
157
- while (start > 0) {
158
- const char = input[start - 1]
159
- // Stop at whitespace
160
- if (/\s/.test(char)) break
161
-
162
- // For @mentions, include @ and stop
163
- if (char === '@' && start < cursorOffset) {
164
- start--
165
- break
166
- }
167
-
168
- // For paths, be smarter about / handling
169
- if (char === '/') {
170
- // Look ahead to see what we've collected so far
171
- const collectedSoFar = input.slice(start, cursorOffset)
172
-
173
- // If we already have a path component, this / is part of the path
174
- if (collectedSoFar.includes('/') || collectedSoFar.includes('.')) {
175
- start--
176
- continue
177
- }
178
-
179
- // Check if this is part of a path pattern like ./ or ../ or ~/
180
- if (start > 1) {
181
- const prevChar = input[start - 2]
182
- if (prevChar === '.' || prevChar === '~') {
183
- // It's part of ./ or ../ or ~/ - keep going
184
- start--
185
- continue
186
- }
187
- }
188
-
189
- // Check if this is a standalone / at the beginning (command)
190
- if (start === 1 || (start > 1 && /\s/.test(input[start - 2]))) {
191
- start--
192
- break // It's a command slash
193
- }
194
-
195
- // Otherwise treat as path separator
196
- start--
197
- continue
198
- }
199
-
200
- // Special handling for dots in paths
201
- if (char === '.' && start > 0) {
202
- // Check if this might be start of ./ or ../
203
- const nextChar = start < input.length ? input[start] : ''
204
- if (nextChar === '/' || nextChar === '.') {
205
- start--
206
- continue // Part of a path pattern
207
- }
208
- }
209
-
210
- start--
211
- }
212
-
213
- // The word is from start to cursor position (not beyond)
214
- const word = input.slice(start, cursorOffset)
215
- if (!word) return null
216
-
217
- // Priority-based type detection - no special cases needed
218
- if (word.startsWith('/')) {
219
- const beforeWord = input.slice(0, start).trim()
220
- const isCommand = beforeWord === '' && !word.includes('/', 1)
221
- return {
222
- type: isCommand ? 'command' : 'file',
223
- prefix: isCommand ? word.slice(1) : word,
224
- startPos: start,
225
- endPos: cursorOffset // Use cursor position as end
226
- }
227
- }
228
-
229
- if (word.startsWith('@')) {
230
- const content = word.slice(1) // Remove @
231
-
232
- // Check if this looks like an email (contains @ in the middle)
233
- if (word.includes('@', 1)) {
234
- // This looks like an email, treat as regular text
235
- return null
236
- }
237
-
238
- // Trigger completion for @mentions (agents, ask-models, files)
239
- return {
240
- type: 'agent', // This will trigger mixed agent+file completion
241
- prefix: content,
242
- startPos: start,
243
- endPos: cursorOffset // Use cursor position as end
244
- }
245
- }
246
-
247
- // Everything else defaults to file completion
248
- return {
249
- type: 'file',
250
- prefix: word,
251
- startPos: start,
252
- endPos: cursorOffset // Use cursor position as end
253
- }
254
- }, [input, cursorOffset])
255
-
256
- // System commands cache - populated dynamically from $PATH
257
- const [systemCommands, setSystemCommands] = useState<string[]>([])
258
- const [isLoadingCommands, setIsLoadingCommands] = useState(false)
259
-
260
- // Dynamic command classification based on intrinsic features
261
- const classifyCommand = useCallback((cmd: string): 'core' | 'common' | 'dev' | 'system' => {
262
- const lowerCmd = cmd.toLowerCase()
263
- let score = 0
264
-
265
- // === FEATURE 1: Name Length & Complexity ===
266
- // Short, simple names are usually core commands
267
- if (cmd.length <= 4) score += 40
268
- else if (cmd.length <= 6) score += 20
269
- else if (cmd.length <= 8) score += 10
270
- else if (cmd.length > 15) score -= 30 // Very long names are specialized
271
-
272
- // === FEATURE 2: Character Patterns ===
273
- // Simple alphabetic names are more likely core
274
- if (/^[a-z]+$/.test(lowerCmd)) score += 30
275
-
276
- // Mixed case, numbers, dots suggest specialized tools
277
- if (/[A-Z]/.test(cmd)) score -= 15
278
- if (/\d/.test(cmd)) score -= 20
279
- if (cmd.includes('.')) score -= 25
280
- if (cmd.includes('-')) score -= 10
281
- if (cmd.includes('_')) score -= 15
282
-
283
- // === FEATURE 3: Linguistic Patterns ===
284
- // Single, common English words
285
- const commonWords = ['list', 'copy', 'move', 'find', 'print', 'show', 'edit', 'view']
286
- if (commonWords.some(word => lowerCmd.includes(word.slice(0, 3)))) score += 25
287
-
288
- // Domain-specific prefixes/suffixes
289
- const devPrefixes = ['git', 'npm', 'node', 'py', 'docker', 'kubectl']
290
- if (devPrefixes.some(prefix => lowerCmd.startsWith(prefix))) score += 15
291
-
292
- // System/daemon indicators
293
- const systemIndicators = ['daemon', 'helper', 'responder', 'service', 'd$', 'ctl$']
294
- if (systemIndicators.some(indicator =>
295
- indicator.endsWith('$') ? lowerCmd.endsWith(indicator.slice(0, -1)) : lowerCmd.includes(indicator)
296
- )) score -= 40
297
-
298
- // === FEATURE 4: File Extension Indicators ===
299
- // Commands with extensions are usually scripts/specialized tools
300
- if (/\.(pl|py|sh|rb|js)$/.test(lowerCmd)) score -= 35
301
-
302
- // === FEATURE 5: Path Location Heuristics ===
303
- // Note: We don't have path info here, but can infer from name patterns
304
- // Commands that look like they belong in /usr/local/bin or specialized dirs
305
- const buildToolPatterns = ['bindep', 'render', 'mako', 'webpack', 'babel', 'eslint']
306
- if (buildToolPatterns.some(pattern => lowerCmd.includes(pattern))) score -= 25
307
-
308
- // === FEATURE 6: Vowel/Consonant Patterns ===
309
- // Unix commands often have abbreviated names with few vowels
310
- const vowelRatio = (lowerCmd.match(/[aeiou]/g) || []).length / lowerCmd.length
311
- if (vowelRatio < 0.2) score += 15 // Very few vowels (like 'ls', 'cp', 'mv')
312
- if (vowelRatio > 0.5) score -= 10 // Too many vowels (usually full words)
313
-
314
- // === CLASSIFICATION BASED ON SCORE ===
315
- if (score >= 50) return 'core' // 50+: Core unix commands
316
- if (score >= 20) return 'common' // 20-49: Common dev tools
317
- if (score >= -10) return 'dev' // -10-19: Specialized dev tools
318
- return 'system' // <-10: System/edge commands
319
- }, [])
320
-
321
- // Load system commands from PATH (like real terminal)
322
- const loadSystemCommands = useCallback(async () => {
323
- if (systemCommands.length > 0 || isLoadingCommands) return // Already loaded or loading
324
-
325
- setIsLoadingCommands(true)
326
- try {
327
- const { readdirSync, statSync } = await import('fs')
328
- const pathDirs = (process.env.PATH || '').split(':').filter(Boolean)
329
- const commandSet = new Set<string>()
330
-
331
- // Get essential commands from utils
332
- const essentialCommands = getEssentialCommands()
333
-
334
- // Add essential commands first
335
- essentialCommands.forEach(cmd => commandSet.add(cmd))
336
-
337
- // Scan PATH directories for executables
338
- for (const dir of pathDirs) {
339
- try {
340
- if (readdirSync && statSync) {
341
- const entries = readdirSync(dir)
342
- for (const entry of entries) {
343
- try {
344
- const fullPath = `${dir}/${entry}`
345
- const stats = statSync(fullPath)
346
- // Check if it's executable (rough check)
347
- if (stats.isFile() && (stats.mode & 0o111) !== 0) {
348
- commandSet.add(entry)
349
- }
350
- } catch {
351
- // Skip files we can't stat
352
- }
353
- }
354
- }
355
- } catch {
356
- // Skip directories we can't read
357
- }
358
- }
359
-
360
- const commands = Array.from(commandSet).sort()
361
- setSystemCommands(commands)
362
- } catch (error) {
363
- console.warn('Failed to load system commands, using fallback:', error)
364
- // Use minimal fallback commands from utils if system scan fails
365
- setSystemCommands(getMinimalFallbackCommands())
366
- } finally {
367
- setIsLoadingCommands(false)
368
- }
369
- }, [systemCommands.length, isLoadingCommands])
370
-
371
- // Load commands on first use
372
- useEffect(() => {
373
- loadSystemCommands()
374
- }, [loadSystemCommands])
375
-
376
- // Generate command suggestions (slash commands)
377
- const generateCommandSuggestions = useCallback((prefix: string): UnifiedSuggestion[] => {
378
- const filteredCommands = commands.filter(cmd => !cmd.isHidden)
379
-
380
- if (!prefix) {
381
- // Show all commands when prefix is empty (for single /)
382
- return filteredCommands.map(cmd => ({
383
- value: cmd.userFacingName(),
384
- displayValue: `/${cmd.userFacingName()}`,
385
- type: 'command' as const,
386
- score: 100,
387
- }))
388
- }
389
-
390
- return filteredCommands
391
- .filter(cmd => {
392
- const names = [cmd.userFacingName(), ...(cmd.aliases || [])]
393
- return names.some(name => name.toLowerCase().startsWith(prefix.toLowerCase()))
394
- })
395
- .map(cmd => ({
396
- value: cmd.userFacingName(),
397
- displayValue: `/${cmd.userFacingName()}`,
398
- type: 'command' as const,
399
- score: 100 - prefix.length + (cmd.userFacingName().startsWith(prefix) ? 10 : 0),
400
- }))
401
- }, [commands])
402
-
403
- // Clean Unix command scoring using fuzzy matcher
404
- const calculateUnixCommandScore = useCallback((cmd: string, prefix: string): number => {
405
- const result = matchCommands([cmd], prefix)
406
- return result.length > 0 ? result[0].score : 0
407
- }, [])
408
-
409
- // Clean Unix command suggestions using fuzzy matcher with common commands boost
410
- const generateUnixCommandSuggestions = useCallback((prefix: string): UnifiedSuggestion[] => {
411
- if (!prefix) return []
412
-
413
- // Loading state
414
- if (isLoadingCommands) {
415
- return [{
416
- value: 'loading...',
417
- displayValue: `⏳ Loading system commands...`,
418
- type: 'file' as const,
419
- score: 0,
420
- metadata: { isLoading: true }
421
- }]
422
- }
423
-
424
- // IMPORTANT: Only use commands that exist on the system (intersection)
425
- const commonCommands = getCommonSystemCommands(systemCommands)
426
-
427
- // Deduplicate commands (in case of any duplicates)
428
- const uniqueCommands = Array.from(new Set(commonCommands))
429
-
430
- // Use fuzzy matcher ONLY on the unique intersection
431
- const matches = matchCommands(uniqueCommands, prefix)
432
-
433
- // Boost common commands
434
- const boostedMatches = matches.map(match => {
435
- const priority = getCommandPriority(match.command)
436
- return {
437
- ...match,
438
- score: match.score + priority * 0.5 // Add priority boost
439
- }
440
- }).sort((a, b) => b.score - a.score)
441
-
442
- // Limit results intelligently
443
- let results = boostedMatches.slice(0, 8)
444
-
445
- // If we have very high scores (900+), show fewer
446
- const perfectMatches = boostedMatches.filter(m => m.score >= 900)
447
- if (perfectMatches.length > 0 && perfectMatches.length <= 3) {
448
- results = perfectMatches
449
- }
450
- // If we have good scores (100+), prefer them
451
- else if (boostedMatches.length > 8) {
452
- const goodMatches = boostedMatches.filter(m => m.score >= 100)
453
- if (goodMatches.length <= 5) {
454
- results = goodMatches
455
- }
456
- }
457
-
458
- return results.map(item => ({
459
- value: item.command,
460
- displayValue: `$ ${item.command}`,
461
- type: 'command' as const,
462
- score: item.score,
463
- metadata: { isUnixCommand: true }
464
- }))
465
- }, [systemCommands, isLoadingCommands])
466
-
467
- // Agent suggestions cache
468
- const [agentSuggestions, setAgentSuggestions] = useState<UnifiedSuggestion[]>([])
469
-
470
- // Model suggestions cache
471
- const [modelSuggestions, setModelSuggestions] = useState<UnifiedSuggestion[]>([])
472
-
473
- // Load model suggestions
474
- useEffect(() => {
475
- try {
476
- const modelManager = getModelManager()
477
- const allModels = modelManager.getAllAvailableModelNames()
478
-
479
- const suggestions = allModels.map(modelId => {
480
- // Professional and clear description for expert model consultation
481
- return {
482
- value: `ask-${modelId}`,
483
- displayValue: `🦜 ask-${modelId} :: Consult ${modelId} for expert opinion and specialized analysis`,
484
- type: 'ask' as const,
485
- score: 90, // Higher than agents - put ask-models on top
486
- metadata: { modelId },
487
- }
488
- })
489
-
490
- setModelSuggestions(suggestions)
491
- } catch (error) {
492
- console.warn('[useUnifiedCompletion] Failed to load models:', error)
493
- // No fallback - rely on dynamic loading only
494
- setModelSuggestions([])
495
- }
496
- }, [])
497
-
498
- // Load agent suggestions on mount
499
- useEffect(() => {
500
- getActiveAgents().then(agents => {
501
- // agents is an array of AgentConfig, not an object
502
- const suggestions = agents.map(config => {
503
- // 🧠 智能描述算法 - 适应性长度控制
504
- let shortDesc = config.whenToUse
505
-
506
- // 移除常见的冗余前缀,但保留核心内容
507
- const prefixPatterns = [
508
- /^Use this agent when you need (assistance with: )?/i,
509
- /^Use PROACTIVELY (when|to) /i,
510
- /^Specialized in /i,
511
- /^Implementation specialist for /i,
512
- /^Design validation specialist\.? Use PROACTIVELY to /i,
513
- /^Task validation specialist\.? Use PROACTIVELY to /i,
514
- /^Requirements validation specialist\.? Use PROACTIVELY to /i
515
- ]
516
-
517
- for (const pattern of prefixPatterns) {
518
- shortDesc = shortDesc.replace(pattern, '')
519
- }
520
-
521
- // 🎯 精准断句算法:中英文句号感叹号优先 → 逗号 → 省略
522
- const findSmartBreak = (text: string, maxLength: number) => {
523
- if (text.length <= maxLength) return text
524
-
525
- // 第一优先级:中英文句号、感叹号
526
- const sentenceEndings = /[.!。!]/
527
- const firstSentenceMatch = text.search(sentenceEndings)
528
- if (firstSentenceMatch !== -1) {
529
- const firstSentence = text.slice(0, firstSentenceMatch).trim()
530
- if (firstSentence.length >= 5) {
531
- return firstSentence
532
- }
533
- }
534
-
535
- // 如果第一句过长,找逗号断句
536
- if (text.length > maxLength) {
537
- const commaEndings = /[,,]/
538
- const commas = []
539
- let match
540
- const regex = new RegExp(commaEndings, 'g')
541
- while ((match = regex.exec(text)) !== null) {
542
- commas.push(match.index)
543
- }
544
-
545
- // 找最后一个在maxLength内的逗号
546
- for (let i = commas.length - 1; i >= 0; i--) {
547
- const commaPos = commas[i]
548
- if (commaPos < maxLength) {
549
- const clause = text.slice(0, commaPos).trim()
550
- if (clause.length >= 5) {
551
- return clause
552
- }
553
- }
554
- }
555
- }
556
-
557
- // 最后选择:直接省略
558
- return text.slice(0, maxLength) + '...'
559
- }
560
-
561
- shortDesc = findSmartBreak(shortDesc.trim(), 80) // 增加到80字符限制
562
-
563
- // 如果处理后为空或太短,使用原始描述
564
- if (!shortDesc || shortDesc.length < 5) {
565
- shortDesc = findSmartBreak(config.whenToUse, 80)
566
- }
567
-
568
- return {
569
- value: `run-agent-${config.agentType}`,
570
- displayValue: `👤 run-agent-${config.agentType} :: ${shortDesc}`, // 人类图标 + run-agent前缀 + 简洁描述
571
- type: 'agent' as const,
572
- score: 85, // Lower than ask-models
573
- metadata: config,
574
- }
575
- })
576
- // Agents loaded successfully
577
- setAgentSuggestions(suggestions)
578
- }).catch((error) => {
579
- console.warn('[useUnifiedCompletion] Failed to load agents:', error)
580
- // No fallback - rely on dynamic loading only
581
- setAgentSuggestions([])
582
- })
583
- }, [])
584
-
585
- // Generate agent and model suggestions using fuzzy matching
586
- const generateMentionSuggestions = useCallback((prefix: string): UnifiedSuggestion[] => {
587
- // Combine agent and model suggestions
588
- const allSuggestions = [...agentSuggestions, ...modelSuggestions]
589
-
590
- if (!prefix) {
591
- // Show all suggestions when prefix is empty (for single @)
592
- return allSuggestions.sort((a, b) => {
593
- // Ask models first (higher score), then agents
594
- if (a.type === 'ask' && b.type === 'agent') return -1
595
- if (a.type === 'agent' && b.type === 'ask') return 1
596
- return b.score - a.score
597
- })
598
- }
599
-
600
- // Use fuzzy matching for intelligent completion
601
- const candidates = allSuggestions.map(s => s.value)
602
- const matches = matchCommands(candidates, prefix)
603
-
604
- // Create result mapping with fuzzy scores
605
- const fuzzyResults = matches
606
- .map(match => {
607
- const suggestion = allSuggestions.find(s => s.value === match.command)!
608
- return {
609
- ...suggestion,
610
- score: match.score // Use fuzzy match score instead of simple scoring
611
- }
612
- })
613
- .sort((a, b) => {
614
- // Ask models first (for equal scores), then agents
615
- if (a.type === 'ask' && b.type === 'agent') return -1
616
- if (a.type === 'agent' && b.type === 'ask') return 1
617
- return b.score - a.score
618
- })
619
-
620
- return fuzzyResults
621
- }, [agentSuggestions, modelSuggestions])
622
-
623
- // Unix-style path completion - preserves user input semantics
624
- const generateFileSuggestions = useCallback((prefix: string, isAtReference: boolean = false): UnifiedSuggestion[] => {
625
- try {
626
- const cwd = getCwd()
627
-
628
- // Parse user input preserving original format
629
- const userPath = prefix || '.'
630
- const isAbsolutePath = userPath.startsWith('/')
631
- const isHomePath = userPath.startsWith('~')
632
-
633
- // Resolve search directory - but keep user's path format for output
634
- let searchPath: string
635
- if (isHomePath) {
636
- searchPath = userPath.replace('~', process.env.HOME || '')
637
- } else if (isAbsolutePath) {
638
- searchPath = userPath
639
- } else {
640
- searchPath = resolve(cwd, userPath)
641
- }
642
-
643
- // Determine search directory and filename filter
644
- // If path ends with '/', treat it as directory navigation
645
- const endsWithSlash = userPath.endsWith('/')
646
- const searchStat = existsSync(searchPath) ? statSync(searchPath) : null
647
-
648
- let searchDir: string
649
- let nameFilter: string
650
-
651
- if (endsWithSlash || searchStat?.isDirectory()) {
652
- // User is navigating into a directory or path ends with /
653
- searchDir = searchPath
654
- nameFilter = ''
655
- } else {
656
- // User might be typing a partial filename
657
- searchDir = dirname(searchPath)
658
- nameFilter = basename(searchPath)
659
- }
660
-
661
- if (!existsSync(searchDir)) return []
662
-
663
- // Get directory entries with filter
664
- const showHidden = nameFilter.startsWith('.') || userPath.includes('/.')
665
- const entries = readdirSync(searchDir)
666
- .filter(entry => {
667
- // Filter hidden files unless user explicitly wants them
668
- if (!showHidden && entry.startsWith('.')) return false
669
- // Filter by name if there's a filter
670
- if (nameFilter && !entry.toLowerCase().startsWith(nameFilter.toLowerCase())) return false
671
- return true
672
- })
673
- .sort((a, b) => {
674
- // Sort directories first, then files
675
- const aPath = join(searchDir, a)
676
- const bPath = join(searchDir, b)
677
- const aIsDir = statSync(aPath).isDirectory()
678
- const bIsDir = statSync(bPath).isDirectory()
679
-
680
- if (aIsDir && !bIsDir) return -1
681
- if (!aIsDir && bIsDir) return 1
682
-
683
- // Within same type, sort alphabetically
684
- return a.toLowerCase().localeCompare(b.toLowerCase())
685
- })
686
- .slice(0, 25) // Show more entries for better visibility
687
-
688
- return entries.map(entry => {
689
- const entryPath = join(searchDir, entry)
690
- const isDir = statSync(entryPath).isDirectory()
691
- const icon = isDir ? '📁' : '📄'
692
-
693
- // Unix-style path building - preserve user's original path format
694
- let value: string
695
-
696
- if (userPath.includes('/')) {
697
- // User typed path with separators - maintain structure
698
- if (endsWithSlash) {
699
- // User explicitly ended with / - they're inside the directory
700
- value = userPath + entry + (isDir ? '/' : '')
701
- } else if (searchStat?.isDirectory()) {
702
- // Path is a directory but doesn't end with / - add separator
703
- value = userPath + '/' + entry + (isDir ? '/' : '')
704
- } else {
705
- // User is completing a filename - replace basename
706
- const userDir = userPath.includes('/') ? userPath.substring(0, userPath.lastIndexOf('/')) : ''
707
- value = userDir ? userDir + '/' + entry + (isDir ? '/' : '') : entry + (isDir ? '/' : '')
708
- }
709
- } else {
710
- // User typed simple name - check if it's an existing directory
711
- if (searchStat?.isDirectory()) {
712
- // Existing directory - navigate into it
713
- value = userPath + '/' + entry + (isDir ? '/' : '')
714
- } else {
715
- // Simple completion at current level
716
- value = entry + (isDir ? '/' : '')
717
- }
718
- }
719
-
720
- return {
721
- value,
722
- displayValue: `${icon} ${entry}${isDir ? '/' : ''}`,
723
- type: 'file' as const,
724
- score: isDir ? 80 : 70,
725
- }
726
- })
727
- } catch {
728
- return []
729
- }
730
- }, [])
731
-
732
- // Unified smart matching - single algorithm with different weights
733
- const calculateMatchScore = useCallback((suggestion: UnifiedSuggestion, prefix: string): number => {
734
- const lowerPrefix = prefix.toLowerCase()
735
- const value = suggestion.value.toLowerCase()
736
- const displayValue = suggestion.displayValue.toLowerCase()
737
-
738
- let matchFound = false
739
- let score = 0
740
-
741
- // Check for actual matches first
742
- if (value.startsWith(lowerPrefix)) {
743
- matchFound = true
744
- score = 100 // Highest priority
745
- } else if (value.includes(lowerPrefix)) {
746
- matchFound = true
747
- score = 95
748
- } else if (displayValue.includes(lowerPrefix)) {
749
- matchFound = true
750
- score = 90
751
- } else {
752
- // Word boundary matching for compound names like "general" -> "run-agent-general-purpose"
753
- const words = value.split(/[-_]/)
754
- if (words.some(word => word.startsWith(lowerPrefix))) {
755
- matchFound = true
756
- score = 93
757
- } else {
758
- // Acronym matching (last resort)
759
- const acronym = words.map(word => word[0]).join('')
760
- if (acronym.startsWith(lowerPrefix)) {
761
- matchFound = true
762
- score = 88
763
- }
764
- }
765
- }
766
-
767
- // Only return score if we found a match
768
- if (!matchFound) return 0
769
-
770
- // Type preferences (small bonus)
771
- if (suggestion.type === 'ask') score += 2
772
- if (suggestion.type === 'agent') score += 1
773
-
774
- return score
775
- }, [])
776
-
777
- // Generate smart mention suggestions without data pollution
778
- const generateSmartMentionSuggestions = useCallback((prefix: string, sourceContext: 'file' | 'agent' = 'file'): UnifiedSuggestion[] => {
779
- if (!prefix || prefix.length < 2) return []
780
-
781
- const allSuggestions = [...agentSuggestions, ...modelSuggestions]
782
-
783
- return allSuggestions
784
- .map(suggestion => {
785
- const matchScore = calculateMatchScore(suggestion, prefix)
786
- if (matchScore === 0) return null
787
-
788
- // Clean transformation without data pollution
789
- return {
790
- ...suggestion,
791
- score: matchScore,
792
- isSmartMatch: true,
793
- originalContext: sourceContext,
794
- // Only modify display for clarity, keep value clean
795
- displayValue: `🎯 ${suggestion.displayValue}`
796
- }
797
- })
798
- .filter(Boolean)
799
- .sort((a, b) => b.score - a.score)
800
- .slice(0, 5)
801
- }, [agentSuggestions, modelSuggestions, calculateMatchScore])
802
-
803
- // Generate all suggestions based on context
804
- const generateSuggestions = useCallback((context: CompletionContext): UnifiedSuggestion[] => {
805
- switch (context.type) {
806
- case 'command':
807
- return generateCommandSuggestions(context.prefix)
808
- case 'agent': {
809
- // @ reference: combine mentions and files with clean priority
810
- const mentionSuggestions = generateMentionSuggestions(context.prefix)
811
- const fileSuggestions = generateFileSuggestions(context.prefix, true) // isAtReference=true
812
-
813
- // Apply weights for @ context (agents/models should be prioritized but files visible)
814
- const weightedSuggestions = [
815
- ...mentionSuggestions.map(s => ({
816
- ...s,
817
- // In @ context, agents/models get high priority
818
- weightedScore: s.score + 150
819
- })),
820
- ...fileSuggestions.map(s => ({
821
- ...s,
822
- // Files get lower priority but still visible
823
- weightedScore: s.score + 10 // Small boost to ensure visibility
824
- }))
825
- ]
826
-
827
- // Sort by weighted score - no artificial limits
828
- return weightedSuggestions
829
- .sort((a, b) => b.weightedScore - a.weightedScore)
830
- .map(({ weightedScore, ...suggestion }) => suggestion)
831
- // No limit or very generous limit (e.g., 30 items)
832
- }
833
- case 'file': {
834
- // For normal input, try to match everything intelligently
835
- const fileSuggestions = generateFileSuggestions(context.prefix, false)
836
- const unixSuggestions = generateUnixCommandSuggestions(context.prefix)
837
-
838
- // IMPORTANT: Also try to match agents and models WITHOUT requiring @
839
- // This enables smart matching for inputs like "gp5", "daoqi", etc.
840
- const mentionMatches = generateMentionSuggestions(context.prefix)
841
- .map(s => ({
842
- ...s,
843
- isSmartMatch: true,
844
- // Show that @ will be added when selected
845
- displayValue: `\u2192 ${s.displayValue}` // Arrow to indicate it will transform
846
- }))
847
-
848
- // Apply source-based priority weights with special handling for exact matches
849
- // Priority order: Exact Unix > Unix commands > agents/models > files
850
- const weightedSuggestions = [
851
- ...unixSuggestions.map(s => ({
852
- ...s,
853
- // Unix commands get boost, but exact matches get huge boost
854
- sourceWeight: s.score >= 10000 ? 5000 : 200, // Exact match gets massive boost
855
- weightedScore: s.score >= 10000 ? s.score + 5000 : s.score + 200
856
- })),
857
- ...mentionMatches.map(s => ({
858
- ...s,
859
- // Agents/models get medium priority boost (but less to avoid overriding exact Unix)
860
- sourceWeight: 50,
861
- weightedScore: s.score + 50
862
- })),
863
- ...fileSuggestions.map(s => ({
864
- ...s,
865
- // Files get no boost (baseline)
866
- sourceWeight: 0,
867
- weightedScore: s.score
868
- }))
869
- ]
870
-
871
- // Sort by weighted score and deduplicate
872
- const seen = new Set<string>()
873
- const deduplicatedResults = weightedSuggestions
874
- .sort((a, b) => b.weightedScore - a.weightedScore)
875
- .filter(item => {
876
- // Filter out duplicates based on value
877
- if (seen.has(item.value)) return false
878
- seen.add(item.value)
879
- return true
880
- })
881
- .map(({ weightedScore, sourceWeight, ...suggestion }) => suggestion) // Remove weight fields
882
- // No limit - show all relevant matches
883
-
884
- return deduplicatedResults
885
- }
886
- default:
887
- return []
888
- }
889
- }, [generateCommandSuggestions, generateMentionSuggestions, generateFileSuggestions, generateUnixCommandSuggestions, generateSmartMentionSuggestions])
890
-
891
-
892
- // Complete with a suggestion - 支持万能@引用 + slash命令自动执行
893
- const completeWith = useCallback((suggestion: UnifiedSuggestion, context: CompletionContext) => {
894
- let completion: string
895
-
896
- if (context.type === 'command') {
897
- completion = `/${suggestion.value} `
898
- } else if (context.type === 'agent') {
899
- // 🚀 万能@引用:根据建议类型决定补全格式
900
- if (suggestion.type === 'agent') {
901
- completion = `@${suggestion.value} ` // 代理补全
902
- } else if (suggestion.type === 'ask') {
903
- completion = `@${suggestion.value} ` // Ask模型补全
904
- } else {
905
- // File reference in @mention context - no space for directories to allow expansion
906
- const isDirectory = suggestion.value.endsWith('/')
907
- completion = `@${suggestion.value}${isDirectory ? '' : ' '}` // 文件夹不加空格,文件加空格
908
- }
909
- } else {
910
- // Regular file completion OR smart mention matching
911
- if (suggestion.isSmartMatch) {
912
- // Smart mention - add @ prefix and space
913
- completion = `@${suggestion.value} `
914
- } else {
915
- // Regular file completion - no space for directories to allow expansion
916
- const isDirectory = suggestion.value.endsWith('/')
917
- completion = suggestion.value + (isDirectory ? '' : ' ')
918
- }
919
- }
920
-
921
- // Special handling for absolute paths in file completion
922
- // When completing an absolute path, we should replace the entire current word/path
923
- let actualEndPos: number
924
-
925
- if (context.type === 'file' && suggestion.value.startsWith('/') && !suggestion.isSmartMatch) {
926
- // For absolute paths, find the end of the current path/word
927
- let end = context.startPos
928
- while (end < input.length && input[end] !== ' ' && input[end] !== '\n') {
929
- end++
930
- }
931
- actualEndPos = end
932
- } else {
933
- // Original logic for other cases
934
- const currentWord = input.slice(context.startPos)
935
- const nextSpaceIndex = currentWord.indexOf(' ')
936
- actualEndPos = nextSpaceIndex === -1 ? input.length : context.startPos + nextSpaceIndex
937
- }
938
-
939
- const newInput = input.slice(0, context.startPos) + completion + input.slice(actualEndPos)
940
- onInputChange(newInput)
941
- setCursorOffset(context.startPos + completion.length)
942
-
943
- // Don't auto-execute slash commands - let user press Enter to submit
944
- // This gives users a chance to add arguments or modify the command
945
-
946
- // Completion applied
947
- }, [input, onInputChange, setCursorOffset, onSubmit, commands])
948
-
949
- // Partial complete to common prefix
950
- const partialComplete = useCallback((prefix: string, context: CompletionContext) => {
951
- const completion = context.type === 'command' ? `/${prefix}` :
952
- context.type === 'agent' ? `@${prefix}` :
953
- prefix
954
-
955
- const newInput = input.slice(0, context.startPos) + completion + input.slice(context.endPos)
956
- onInputChange(newInput)
957
- setCursorOffset(context.startPos + completion.length)
958
- }, [input, onInputChange, setCursorOffset])
959
-
960
-
961
- // Handle Tab key - simplified and unified
962
- useInput((input_str, key) => {
963
- if (!key.tab) return false
964
- if (key.shift) return false
965
-
966
- const context = getWordAtCursor()
967
- if (!context) return false
968
-
969
- // If menu is already showing, cycle through suggestions
970
- if (state.isActive && state.suggestions.length > 0) {
971
- const nextIndex = (state.selectedIndex + 1) % state.suggestions.length
972
- const nextSuggestion = state.suggestions[nextIndex]
973
-
974
- if (state.context) {
975
- // Calculate proper word boundaries
976
- const currentWord = input.slice(state.context.startPos)
977
- const wordEnd = currentWord.search(/\s/)
978
- const actualEndPos = wordEnd === -1
979
- ? input.length
980
- : state.context.startPos + wordEnd
981
-
982
- // Apply appropriate prefix based on context type and suggestion type
983
- let preview: string
984
- if (state.context.type === 'command') {
985
- preview = `/${nextSuggestion.value}`
986
- } else if (state.context.type === 'agent') {
987
- // For @mentions, always add @ prefix
988
- preview = `@${nextSuggestion.value}`
989
- } else if (nextSuggestion.isSmartMatch) {
990
- // Smart match from normal input - add @ prefix
991
- preview = `@${nextSuggestion.value}`
992
- } else {
993
- preview = nextSuggestion.value
994
- }
995
-
996
- // Apply preview
997
- const newInput = input.slice(0, state.context.startPos) +
998
- preview +
999
- input.slice(actualEndPos)
1000
-
1001
- onInputChange(newInput)
1002
- setCursorOffset(state.context.startPos + preview.length)
1003
-
1004
- // Update state
1005
- updateState({
1006
- selectedIndex: nextIndex,
1007
- preview: {
1008
- isActive: true,
1009
- originalInput: input,
1010
- wordRange: [state.context.startPos, state.context.startPos + preview.length]
1011
- }
1012
- })
1013
- }
1014
- return true
1015
- }
1016
-
1017
- // Generate new suggestions
1018
- const currentSuggestions = generateSuggestions(context)
1019
-
1020
- if (currentSuggestions.length === 0) {
1021
- return false // Let Tab pass through
1022
- } else if (currentSuggestions.length === 1) {
1023
- // Single match: complete immediately
1024
- completeWith(currentSuggestions[0], context)
1025
- return true
1026
- } else {
1027
- // Show menu and apply first suggestion
1028
- activateCompletion(currentSuggestions, context)
1029
-
1030
- // Immediately apply first suggestion as preview
1031
- const firstSuggestion = currentSuggestions[0]
1032
- const currentWord = input.slice(context.startPos)
1033
- const wordEnd = currentWord.search(/\s/)
1034
- const actualEndPos = wordEnd === -1
1035
- ? input.length
1036
- : context.startPos + wordEnd
1037
-
1038
- let preview: string
1039
- if (context.type === 'command') {
1040
- preview = `/${firstSuggestion.value}`
1041
- } else if (context.type === 'agent') {
1042
- preview = `@${firstSuggestion.value}`
1043
- } else if (firstSuggestion.isSmartMatch) {
1044
- // Smart match from normal input - add @ prefix
1045
- preview = `@${firstSuggestion.value}`
1046
- } else {
1047
- preview = firstSuggestion.value
1048
- }
1049
-
1050
- const newInput = input.slice(0, context.startPos) +
1051
- preview +
1052
- input.slice(actualEndPos)
1053
-
1054
- onInputChange(newInput)
1055
- setCursorOffset(context.startPos + preview.length)
1056
-
1057
- updateState({
1058
- preview: {
1059
- isActive: true,
1060
- originalInput: input,
1061
- wordRange: [context.startPos, context.startPos + preview.length]
1062
- }
1063
- })
1064
-
1065
- return true
1066
- }
1067
- })
1068
-
1069
- // Handle navigation keys - simplified and unified
1070
- useInput((inputChar, key) => {
1071
- // Enter key - confirm selection and end completion (always add space)
1072
- if (key.return && state.isActive && state.suggestions.length > 0) {
1073
- const selectedSuggestion = state.suggestions[state.selectedIndex]
1074
- if (selectedSuggestion && state.context) {
1075
- // For Enter key, always add space even for directories to indicate completion end
1076
- let completion: string
1077
-
1078
- if (state.context.type === 'command') {
1079
- completion = `/${selectedSuggestion.value} `
1080
- } else if (state.context.type === 'agent') {
1081
- if (selectedSuggestion.type === 'agent') {
1082
- completion = `@${selectedSuggestion.value} `
1083
- } else if (selectedSuggestion.type === 'ask') {
1084
- completion = `@${selectedSuggestion.value} `
1085
- } else {
1086
- // File reference in @mention context - always add space on Enter
1087
- completion = `@${selectedSuggestion.value} `
1088
- }
1089
- } else if (selectedSuggestion.isSmartMatch) {
1090
- // Smart match from normal input - add @ prefix
1091
- completion = `@${selectedSuggestion.value} `
1092
- } else {
1093
- // Regular file completion - always add space on Enter
1094
- completion = selectedSuggestion.value + ' '
1095
- }
1096
-
1097
- // Apply completion with forced space
1098
- const currentWord = input.slice(state.context.startPos)
1099
- const nextSpaceIndex = currentWord.indexOf(' ')
1100
- const actualEndPos = nextSpaceIndex === -1 ? input.length : state.context.startPos + nextSpaceIndex
1101
-
1102
- const newInput = input.slice(0, state.context.startPos) + completion + input.slice(actualEndPos)
1103
- onInputChange(newInput)
1104
- setCursorOffset(state.context.startPos + completion.length)
1105
- }
1106
- resetCompletion()
1107
- return true
1108
- }
1109
-
1110
- if (!state.isActive || state.suggestions.length === 0) return false
1111
-
1112
- // Arrow key navigation with preview
1113
- const handleNavigation = (newIndex: number) => {
1114
- const preview = state.suggestions[newIndex].value
1115
-
1116
- if (state.preview?.isActive && state.context) {
1117
- const newInput = input.slice(0, state.context.startPos) +
1118
- preview +
1119
- input.slice(state.preview.wordRange[1])
1120
-
1121
- onInputChange(newInput)
1122
- setCursorOffset(state.context.startPos + preview.length)
1123
-
1124
- updateState({
1125
- selectedIndex: newIndex,
1126
- preview: {
1127
- ...state.preview,
1128
- wordRange: [state.context.startPos, state.context.startPos + preview.length]
1129
- }
1130
- })
1131
- } else {
1132
- updateState({ selectedIndex: newIndex })
1133
- }
1134
- }
1135
-
1136
- if (key.downArrow) {
1137
- const nextIndex = (state.selectedIndex + 1) % state.suggestions.length
1138
- handleNavigation(nextIndex)
1139
- return true
1140
- }
1141
-
1142
- if (key.upArrow) {
1143
- const nextIndex = state.selectedIndex === 0
1144
- ? state.suggestions.length - 1
1145
- : state.selectedIndex - 1
1146
- handleNavigation(nextIndex)
1147
- return true
1148
- }
1149
-
1150
- // Space key - complete and potentially continue for directories
1151
- if (inputChar === ' ' && state.isActive && state.suggestions.length > 0) {
1152
- const selectedSuggestion = state.suggestions[state.selectedIndex]
1153
- const isDirectory = selectedSuggestion.value.endsWith('/')
1154
-
1155
- if (!state.context) return false
1156
-
1157
- // Apply completion if needed
1158
- const currentWordAtContext = input.slice(state.context.startPos,
1159
- state.context.startPos + selectedSuggestion.value.length)
1160
-
1161
- if (currentWordAtContext !== selectedSuggestion.value) {
1162
- completeWith(selectedSuggestion, state.context)
1163
- }
1164
-
1165
- resetCompletion()
1166
-
1167
- if (isDirectory) {
1168
- // Continue completion for directories
1169
- setTimeout(() => {
1170
- const newContext = {
1171
- ...state.context,
1172
- prefix: selectedSuggestion.value,
1173
- endPos: state.context.startPos + selectedSuggestion.value.length
1174
- }
1175
-
1176
- const newSuggestions = generateSuggestions(newContext)
1177
-
1178
- if (newSuggestions.length > 0) {
1179
- activateCompletion(newSuggestions, newContext)
1180
- } else {
1181
- updateState({
1182
- emptyDirMessage: `Directory is empty: ${selectedSuggestion.value}`
1183
- })
1184
- setTimeout(() => updateState({ emptyDirMessage: '' }), 3000)
1185
- }
1186
- }, 50)
1187
- }
1188
-
1189
- return true
1190
- }
1191
-
1192
- // Right arrow key - same as space but different semantics
1193
- if (key.rightArrow) {
1194
- const selectedSuggestion = state.suggestions[state.selectedIndex]
1195
- const isDirectory = selectedSuggestion.value.endsWith('/')
1196
-
1197
- if (!state.context) return false
1198
-
1199
- // Apply completion
1200
- const currentWordAtContext = input.slice(state.context.startPos,
1201
- state.context.startPos + selectedSuggestion.value.length)
1202
-
1203
- if (currentWordAtContext !== selectedSuggestion.value) {
1204
- completeWith(selectedSuggestion, state.context)
1205
- }
1206
-
1207
- resetCompletion()
1208
-
1209
- if (isDirectory) {
1210
- // Continue for directories
1211
- setTimeout(() => {
1212
- const newContext = {
1213
- ...state.context,
1214
- prefix: selectedSuggestion.value,
1215
- endPos: state.context.startPos + selectedSuggestion.value.length
1216
- }
1217
-
1218
- const newSuggestions = generateSuggestions(newContext)
1219
-
1220
- if (newSuggestions.length > 0) {
1221
- activateCompletion(newSuggestions, newContext)
1222
- } else {
1223
- updateState({
1224
- emptyDirMessage: `Directory is empty: ${selectedSuggestion.value}`
1225
- })
1226
- setTimeout(() => updateState({ emptyDirMessage: '' }), 3000)
1227
- }
1228
- }, 50)
1229
- }
1230
-
1231
- return true
1232
- }
1233
-
1234
- if (key.escape) {
1235
- // Restore original text if in preview mode
1236
- if (state.preview?.isActive && state.context) {
1237
- onInputChange(state.preview.originalInput)
1238
- setCursorOffset(state.context.startPos + state.context.prefix.length)
1239
- }
1240
-
1241
- resetCompletion()
1242
- return true
1243
- }
1244
-
1245
- return false
1246
- })
1247
-
1248
- // Handle delete/backspace keys - unified state management
1249
- useInput((input_str, key) => {
1250
- if (key.backspace || key.delete) {
1251
- if (state.isActive) {
1252
- resetCompletion()
1253
- // Smart suppression based on input complexity
1254
- const suppressionTime = input.length > 10 ? 200 : 100
1255
- updateState({
1256
- suppressUntil: Date.now() + suppressionTime
1257
- })
1258
- return true
1259
- }
1260
- }
1261
- return false
1262
- })
1263
-
1264
- // Input tracking with ref to avoid infinite loops
1265
- const lastInputRef = useRef('')
1266
-
1267
- // Smart auto-triggering with cycle prevention
1268
- useEffect(() => {
1269
- // Prevent infinite loops by using ref
1270
- if (lastInputRef.current === input) return
1271
-
1272
- const inputLengthChange = Math.abs(input.length - lastInputRef.current.length)
1273
- const isHistoryNavigation = (
1274
- inputLengthChange > 10 || // Large content change
1275
- (inputLengthChange > 5 && !input.includes(lastInputRef.current.slice(-5))) // Different content
1276
- ) && input !== lastInputRef.current
1277
-
1278
- // Update ref (no state update)
1279
- lastInputRef.current = input
1280
-
1281
- // Skip if in preview mode or suppressed
1282
- if (state.preview?.isActive || Date.now() < state.suppressUntil) {
1283
- return
1284
- }
1285
-
1286
- // Clear suggestions on history navigation
1287
- if (isHistoryNavigation && state.isActive) {
1288
- resetCompletion()
1289
- return
1290
- }
1291
-
1292
- const context = getWordAtCursor()
1293
-
1294
- if (context && shouldAutoTrigger(context)) {
1295
- const newSuggestions = generateSuggestions(context)
1296
-
1297
- if (newSuggestions.length === 0) {
1298
- resetCompletion()
1299
- } else if (newSuggestions.length === 1 && shouldAutoHideSingleMatch(newSuggestions[0], context)) {
1300
- resetCompletion() // Perfect match - hide
1301
- } else {
1302
- activateCompletion(newSuggestions, context)
1303
- }
1304
- } else if (state.context) {
1305
- // Check if context changed significantly
1306
- const contextChanged = !context ||
1307
- state.context.type !== context.type ||
1308
- state.context.startPos !== context.startPos ||
1309
- !context.prefix.startsWith(state.context.prefix)
1310
-
1311
- if (contextChanged) {
1312
- resetCompletion()
1313
- }
1314
- }
1315
- }, [input, cursorOffset])
1316
-
1317
- // Smart triggering - only when it makes sense
1318
- const shouldAutoTrigger = useCallback((context: CompletionContext): boolean => {
1319
- switch (context.type) {
1320
- case 'command':
1321
- // Trigger immediately for slash commands
1322
- return true
1323
- case 'agent':
1324
- // Trigger immediately for agent references
1325
- return true
1326
- case 'file':
1327
- // Be selective about file completion - avoid noise
1328
- const prefix = context.prefix
1329
-
1330
- // Always trigger for clear path patterns
1331
- if (prefix.startsWith('./') || prefix.startsWith('../') ||
1332
- prefix.startsWith('/') || prefix.startsWith('~') ||
1333
- prefix.includes('/')) {
1334
- return true
1335
- }
1336
-
1337
- // Trigger for single dot followed by something (like .g for .gitignore)
1338
- if (prefix.startsWith('.') && prefix.length >= 2) {
1339
- return true
1340
- }
1341
-
1342
- // Skip very short prefixes that are likely code
1343
- return false
1344
- default:
1345
- return false
1346
- }
1347
- }, [])
1348
-
1349
- // Helper function to determine if single suggestion should be auto-hidden
1350
- const shouldAutoHideSingleMatch = useCallback((suggestion: UnifiedSuggestion, context: CompletionContext): boolean => {
1351
- // Extract the actual typed input from context
1352
- const currentInput = input.slice(context.startPos, context.endPos)
1353
- // Check if should auto-hide single match
1354
-
1355
- // For files: more intelligent matching
1356
- if (context.type === 'file') {
1357
- // Special case: if suggestion is a directory (ends with /), don't auto-hide
1358
- // because user might want to continue navigating into it
1359
- if (suggestion.value.endsWith('/')) {
1360
- // Directory suggestion, keeping visible
1361
- return false
1362
- }
1363
-
1364
- // Check exact match
1365
- if (currentInput === suggestion.value) {
1366
- // Exact match, hiding
1367
- return true
1368
- }
1369
-
1370
- // Check if current input is a complete file path and suggestion is just the filename
1371
- // e.g., currentInput: "src/tools/ThinkTool/ThinkTool.tsx", suggestion: "ThinkTool.tsx"
1372
- if (currentInput.endsWith('/' + suggestion.value) || currentInput.endsWith(suggestion.value)) {
1373
- // Path ends with suggestion, hiding
1374
- return true
1375
- }
1376
-
1377
- return false
1378
- }
1379
-
1380
- // For commands: check if /prefix exactly matches /command
1381
- if (context.type === 'command') {
1382
- const fullCommand = `/${suggestion.value}`
1383
- const matches = currentInput === fullCommand
1384
- // Check command match
1385
- return matches
1386
- }
1387
-
1388
- // For agents: check if @prefix exactly matches @agent-name
1389
- if (context.type === 'agent') {
1390
- const fullAgent = `@${suggestion.value}`
1391
- const matches = currentInput === fullAgent
1392
- // Check agent match
1393
- return matches
1394
- }
1395
-
1396
- return false
1397
- }, [input])
1398
-
1399
- return {
1400
- suggestions,
1401
- selectedIndex,
1402
- isActive,
1403
- emptyDirMessage,
1404
- }
1405
- }