@shareai-lab/kode 1.0.70 → 1.0.71

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 (253) hide show
  1. package/README.md +202 -76
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,836 @@
1
+ import { memoize } from 'lodash-es'
2
+ import { getDynamicConfig, getExperimentValue } from '../services/statsig'
3
+ import { logError } from './log'
4
+ import {
5
+ getGlobalConfig,
6
+ ModelProfile,
7
+ ModelPointerType,
8
+ saveGlobalConfig,
9
+ } from './config'
10
+
11
+ export const USE_BEDROCK = !!process.env.CLAUDE_CODE_USE_BEDROCK
12
+ export const USE_VERTEX = !!process.env.CLAUDE_CODE_USE_VERTEX
13
+
14
+ export interface ModelConfig {
15
+ bedrock: string
16
+ vertex: string
17
+ firstParty: string
18
+ }
19
+
20
+ const DEFAULT_MODEL_CONFIG: ModelConfig = {
21
+ bedrock: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
22
+ vertex: 'claude-3-7-sonnet@20250219',
23
+ firstParty: 'claude-sonnet-4-20250514',
24
+ }
25
+
26
+ /**
27
+ * Helper to get the model config from statsig or defaults
28
+ * Relies on the built-in caching from StatsigClient
29
+ */
30
+ async function getModelConfig(): Promise<ModelConfig> {
31
+ try {
32
+ return await getDynamicConfig<ModelConfig>(
33
+ 'tengu-capable-model-config',
34
+ DEFAULT_MODEL_CONFIG,
35
+ )
36
+ } catch (error) {
37
+ logError(error)
38
+ return DEFAULT_MODEL_CONFIG
39
+ }
40
+ }
41
+
42
+ export const getSlowAndCapableModel = memoize(async (): Promise<string> => {
43
+ const config = await getGlobalConfig()
44
+
45
+ // Use ModelManager for proper model resolution
46
+ const modelManager = new ModelManager(config)
47
+ const model = modelManager.getMainAgentModel()
48
+
49
+ if (model) {
50
+ return model
51
+ }
52
+
53
+ // Final fallback to default model
54
+ const modelConfig = await getModelConfig()
55
+ if (USE_BEDROCK) return modelConfig.bedrock
56
+ if (USE_VERTEX) return modelConfig.vertex
57
+ return modelConfig.firstParty
58
+ })
59
+
60
+ export async function isDefaultSlowAndCapableModel(): Promise<boolean> {
61
+ return (
62
+ !process.env.ANTHROPIC_MODEL ||
63
+ process.env.ANTHROPIC_MODEL === (await getSlowAndCapableModel())
64
+ )
65
+ }
66
+
67
+ /**
68
+ * Get the region for a specific Vertex model
69
+ * Checks for hardcoded model-specific environment variables first,
70
+ * then falls back to CLOUD_ML_REGION env var or default region
71
+ */
72
+ export function getVertexRegionForModel(
73
+ model: string | undefined,
74
+ ): string | undefined {
75
+ if (model?.startsWith('claude-3-5-haiku')) {
76
+ return process.env.VERTEX_REGION_CLAUDE_3_5_HAIKU
77
+ } else if (model?.startsWith('claude-3-5-sonnet')) {
78
+ return process.env.VERTEX_REGION_CLAUDE_3_5_SONNET
79
+ } else if (model?.startsWith('claude-3-7-sonnet')) {
80
+ return process.env.VERTEX_REGION_CLAUDE_3_7_SONNET
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Comprehensive ModelManager class for centralized model selection and management.
86
+ * Provides a clean interface for model selection across the application.
87
+ */
88
+ export class ModelManager {
89
+ private config: any // Using any to handle legacy properties
90
+ private modelProfiles: ModelProfile[]
91
+
92
+ constructor(config: any) {
93
+ this.config = config
94
+ this.modelProfiles = config.modelProfiles || []
95
+ }
96
+
97
+ /**
98
+ * Get the current terminal model (for interactive CLI sessions)
99
+ */
100
+ getCurrentModel(): string | null {
101
+ // Use main pointer from new ModelProfile system
102
+ const mainModelName = this.config.modelPointers?.main
103
+ if (mainModelName) {
104
+ const profile = this.findModelProfile(mainModelName)
105
+ if (profile && profile.isActive) {
106
+ return profile.modelName
107
+ }
108
+ }
109
+
110
+ // Fallback to main agent model
111
+ return this.getMainAgentModel()
112
+ }
113
+
114
+ /**
115
+ * Get the main agent default model (for non-terminal mode and MCP calls)
116
+ */
117
+ getMainAgentModel(): string | null {
118
+ // Use main pointer from new ModelProfile system
119
+ const mainModelName = this.config.modelPointers?.main
120
+ if (mainModelName) {
121
+ const profile = this.findModelProfile(mainModelName)
122
+ if (profile && profile.isActive) {
123
+ return profile.modelName
124
+ }
125
+ }
126
+
127
+ // Fallback to first active profile
128
+ const activeProfile = this.modelProfiles.find(p => p.isActive)
129
+ if (activeProfile) {
130
+ return activeProfile.modelName
131
+ }
132
+
133
+ return null
134
+ }
135
+
136
+ /**
137
+ * Get the task tool default model (for Task tool sub-agents)
138
+ */
139
+ getTaskToolModel(): string | null {
140
+ // Use task pointer from new ModelProfile system
141
+ const taskModelName = this.config.modelPointers?.task
142
+ if (taskModelName) {
143
+ const profile = this.findModelProfile(taskModelName)
144
+ if (profile && profile.isActive) {
145
+ return profile.modelName
146
+ }
147
+ }
148
+
149
+ // Fallback to main agent model
150
+ return this.getMainAgentModel()
151
+ }
152
+
153
+ /**
154
+ * Switch to the next available model with simple context overflow handling
155
+ * If target model can't handle current context, shows warning and reverts after delay
156
+ *
157
+ * @param currentContextTokens - Current conversation token count for validation
158
+ * @returns Object with model name and context status information
159
+ */
160
+ switchToNextModelWithContextCheck(currentContextTokens: number = 0): {
161
+ success: boolean
162
+ modelName: string | null
163
+ previousModelName: string | null
164
+ contextOverflow: boolean
165
+ usagePercentage: number
166
+ } {
167
+ const activeProfiles = this.modelProfiles.filter(p => p.isActive)
168
+ if (activeProfiles.length === 0) {
169
+ return {
170
+ success: false,
171
+ modelName: null,
172
+ previousModelName: null,
173
+ contextOverflow: false,
174
+ usagePercentage: 0,
175
+ }
176
+ }
177
+
178
+ // Sort by lastUsed (most recent first) then by createdAt
179
+ activeProfiles.sort((a, b) => {
180
+ const aLastUsed = a.lastUsed || 0
181
+ const bLastUsed = b.lastUsed || 0
182
+ if (aLastUsed !== bLastUsed) {
183
+ return bLastUsed - aLastUsed
184
+ }
185
+ return b.createdAt - a.createdAt
186
+ })
187
+
188
+ const currentMainModelName = this.config.modelPointers?.main
189
+ const currentModel = currentMainModelName
190
+ ? this.findModelProfile(currentMainModelName)
191
+ : null
192
+ const previousModelName = currentModel?.name || null
193
+
194
+ if (!currentMainModelName) {
195
+ // No current main model, select first active
196
+ const firstModel = activeProfiles[0]
197
+ this.setPointer('main', firstModel.modelName)
198
+ this.updateLastUsed(firstModel.modelName)
199
+
200
+ const analysis = this.analyzeContextCompatibility(
201
+ firstModel,
202
+ currentContextTokens,
203
+ )
204
+ return {
205
+ success: true,
206
+ modelName: firstModel.name,
207
+ previousModelName: null,
208
+ contextOverflow: !analysis.compatible,
209
+ usagePercentage: analysis.usagePercentage,
210
+ }
211
+ }
212
+
213
+ // Find current model index
214
+ const currentIndex = activeProfiles.findIndex(
215
+ p => p.modelName === currentMainModelName,
216
+ )
217
+ if (currentIndex === -1) {
218
+ // Current model not found, select first
219
+ const firstModel = activeProfiles[0]
220
+ this.setPointer('main', firstModel.modelName)
221
+ this.updateLastUsed(firstModel.modelName)
222
+
223
+ const analysis = this.analyzeContextCompatibility(
224
+ firstModel,
225
+ currentContextTokens,
226
+ )
227
+ return {
228
+ success: true,
229
+ modelName: firstModel.name,
230
+ previousModelName,
231
+ contextOverflow: !analysis.compatible,
232
+ usagePercentage: analysis.usagePercentage,
233
+ }
234
+ }
235
+
236
+ // Check if only one model is available
237
+ if (activeProfiles.length === 1) {
238
+ return {
239
+ success: false,
240
+ modelName: null,
241
+ previousModelName,
242
+ contextOverflow: false,
243
+ usagePercentage: 0,
244
+ }
245
+ }
246
+
247
+ // Get next model in cycle
248
+ const nextIndex = (currentIndex + 1) % activeProfiles.length
249
+ const nextModel = activeProfiles[nextIndex]
250
+
251
+ // Analyze context compatibility for next model
252
+ const analysis = this.analyzeContextCompatibility(
253
+ nextModel,
254
+ currentContextTokens,
255
+ )
256
+
257
+ // Always switch to next model, but return context status
258
+ this.setPointer('main', nextModel.modelName)
259
+ this.updateLastUsed(nextModel.modelName)
260
+
261
+ return {
262
+ success: true,
263
+ modelName: nextModel.name,
264
+ previousModelName,
265
+ contextOverflow: !analysis.compatible,
266
+ usagePercentage: analysis.usagePercentage,
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Simple model switching for UI components (compatible interface)
272
+ * @param currentContextTokens - Current conversation token count for validation
273
+ * @returns Compatible interface for PromptInput component
274
+ */
275
+ switchToNextModel(currentContextTokens: number = 0): {
276
+ success: boolean
277
+ modelName: string | null
278
+ blocked?: boolean
279
+ message?: string
280
+ } {
281
+ const result = this.switchToNextModelWithContextCheck(currentContextTokens)
282
+
283
+ // Special case: only one model available
284
+ if (
285
+ !result.success &&
286
+ result.previousModelName &&
287
+ this.getAvailableModels().length === 1
288
+ ) {
289
+ return {
290
+ success: false,
291
+ modelName: null,
292
+ blocked: false,
293
+ message: `⚠️ Only one model configured (${result.previousModelName}). Use /model to add more models for switching.`,
294
+ }
295
+ }
296
+
297
+ return {
298
+ success: result.success,
299
+ modelName: result.modelName,
300
+ blocked: result.contextOverflow,
301
+ message: result.contextOverflow
302
+ ? `Context usage: ${result.usagePercentage.toFixed(1)}%`
303
+ : undefined,
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Revert to previous model (used when context overflow requires rollback)
309
+ */
310
+ revertToPreviousModel(previousModelName: string): boolean {
311
+ const previousModel = this.modelProfiles.find(
312
+ p => p.name === previousModelName && p.isActive,
313
+ )
314
+ if (!previousModel) {
315
+ return false
316
+ }
317
+
318
+ this.setPointer('main', previousModel.modelName)
319
+ this.updateLastUsed(previousModel.modelName)
320
+ return true
321
+ }
322
+
323
+ /**
324
+ * Enhanced context validation with different severity levels
325
+ */
326
+ analyzeContextCompatibility(
327
+ model: ModelProfile,
328
+ contextTokens: number,
329
+ ): {
330
+ compatible: boolean
331
+ severity: 'safe' | 'warning' | 'critical'
332
+ usagePercentage: number
333
+ recommendation: string
334
+ } {
335
+ const usableContext = Math.floor(model.contextLength * 0.8) // Reserve 20% for output
336
+ const usagePercentage = (contextTokens / usableContext) * 100
337
+
338
+ if (usagePercentage <= 70) {
339
+ return {
340
+ compatible: true,
341
+ severity: 'safe',
342
+ usagePercentage,
343
+ recommendation: 'Full context preserved',
344
+ }
345
+ } else if (usagePercentage <= 90) {
346
+ return {
347
+ compatible: true,
348
+ severity: 'warning',
349
+ usagePercentage,
350
+ recommendation: 'Context usage high, consider compression',
351
+ }
352
+ } else {
353
+ return {
354
+ compatible: false,
355
+ severity: 'critical',
356
+ usagePercentage,
357
+ recommendation: 'Auto-compression or message truncation required',
358
+ }
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Switch to next model with enhanced context analysis
364
+ */
365
+ switchToNextModelWithAnalysis(currentContextTokens: number = 0): {
366
+ modelName: string | null
367
+ contextAnalysis: ReturnType<typeof this.analyzeContextCompatibility> | null
368
+ requiresCompression: boolean
369
+ estimatedTokensAfterSwitch: number
370
+ } {
371
+ const modelName = this.switchToNextModel(currentContextTokens)
372
+
373
+ if (!modelName) {
374
+ return {
375
+ modelName: null,
376
+ contextAnalysis: null,
377
+ requiresCompression: false,
378
+ estimatedTokensAfterSwitch: 0,
379
+ }
380
+ }
381
+
382
+ const newModel = this.getModel('main')
383
+ if (!newModel) {
384
+ return {
385
+ modelName,
386
+ contextAnalysis: null,
387
+ requiresCompression: false,
388
+ estimatedTokensAfterSwitch: currentContextTokens,
389
+ }
390
+ }
391
+
392
+ const analysis = this.analyzeContextCompatibility(
393
+ newModel,
394
+ currentContextTokens,
395
+ )
396
+
397
+ return {
398
+ modelName,
399
+ contextAnalysis: analysis,
400
+ requiresCompression: analysis.severity === 'critical',
401
+ estimatedTokensAfterSwitch: currentContextTokens,
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Check if a model can handle the given context size (legacy method)
407
+ */
408
+ canModelHandleContext(model: ModelProfile, contextTokens: number): boolean {
409
+ const analysis = this.analyzeContextCompatibility(model, contextTokens)
410
+ return analysis.compatible
411
+ }
412
+
413
+ /**
414
+ * Find the first model that can handle the given context size
415
+ */
416
+ findModelWithSufficientContext(
417
+ models: ModelProfile[],
418
+ contextTokens: number,
419
+ ): ModelProfile | null {
420
+ return (
421
+ models.find(model => this.canModelHandleContext(model, contextTokens)) ||
422
+ null
423
+ )
424
+ }
425
+
426
+ /**
427
+ * Unified model getter for different contexts
428
+ */
429
+ getModelForContext(
430
+ contextType: 'terminal' | 'main-agent' | 'task-tool',
431
+ ): string | null {
432
+ switch (contextType) {
433
+ case 'terminal':
434
+ return this.getCurrentModel()
435
+ case 'main-agent':
436
+ return this.getMainAgentModel()
437
+ case 'task-tool':
438
+ return this.getTaskToolModel()
439
+ default:
440
+ return this.getMainAgentModel()
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Get all active model profiles
446
+ */
447
+ getActiveModelProfiles(): ModelProfile[] {
448
+ return this.modelProfiles.filter(p => p.isActive)
449
+ }
450
+
451
+ /**
452
+ * Check if any models are configured
453
+ */
454
+ hasConfiguredModels(): boolean {
455
+ return this.getActiveModelProfiles().length > 0
456
+ }
457
+
458
+ // New model pointer system methods
459
+
460
+ /**
461
+ * Get model by pointer type (main, task, reasoning, quick)
462
+ */
463
+ getModel(pointer: ModelPointerType): ModelProfile | null {
464
+ const pointerId = this.config.modelPointers?.[pointer]
465
+ if (!pointerId) {
466
+ return this.getDefaultModel()
467
+ }
468
+
469
+ const profile = this.findModelProfile(pointerId)
470
+ return profile && profile.isActive ? profile : this.getDefaultModel()
471
+ }
472
+
473
+ /**
474
+ * Get model name by pointer type
475
+ */
476
+ getModelName(pointer: ModelPointerType): string | null {
477
+ const profile = this.getModel(pointer)
478
+ return profile ? profile.modelName : null
479
+ }
480
+
481
+ /**
482
+ * Get reasoning model (with fallback)
483
+ */
484
+ getReasoningModel(): string | null {
485
+ return this.getModelName('reasoning') || this.getModelName('main')
486
+ }
487
+
488
+ /**
489
+ * Get quick model (with fallback)
490
+ */
491
+ getQuickModel(): string | null {
492
+ return (
493
+ this.getModelName('quick') ||
494
+ this.getModelName('task') ||
495
+ this.getModelName('main')
496
+ )
497
+ }
498
+
499
+ /**
500
+ * Add a new model profile with duplicate validation
501
+ */
502
+ async addModel(
503
+ config: Omit<ModelProfile, 'createdAt' | 'isActive'>,
504
+ ): Promise<string> {
505
+ // Check for duplicate modelName (actual model identifier)
506
+ const existingByModelName = this.modelProfiles.find(
507
+ p => p.modelName === config.modelName,
508
+ )
509
+ if (existingByModelName) {
510
+ throw new Error(
511
+ `Model with modelName '${config.modelName}' already exists: ${existingByModelName.name}`,
512
+ )
513
+ }
514
+
515
+ // Check for duplicate friendly name
516
+ const existingByName = this.modelProfiles.find(p => p.name === config.name)
517
+ if (existingByName) {
518
+ throw new Error(`Model with name '${config.name}' already exists`)
519
+ }
520
+
521
+ const newModel: ModelProfile = {
522
+ ...config,
523
+ createdAt: Date.now(),
524
+ isActive: true,
525
+ }
526
+
527
+ this.modelProfiles.push(newModel)
528
+
529
+ // If this is the first model, set all pointers to it
530
+ if (this.modelProfiles.length === 1) {
531
+ this.config.modelPointers = {
532
+ main: config.modelName,
533
+ task: config.modelName,
534
+ reasoning: config.modelName,
535
+ quick: config.modelName,
536
+ }
537
+ this.config.defaultModelName = config.modelName
538
+ }
539
+
540
+ this.saveConfig()
541
+ return config.modelName
542
+ }
543
+
544
+ /**
545
+ * Set model pointer assignment
546
+ */
547
+ setPointer(pointer: ModelPointerType, modelName: string): void {
548
+ if (!this.findModelProfile(modelName)) {
549
+ throw new Error(`Model '${modelName}' not found`)
550
+ }
551
+
552
+ if (!this.config.modelPointers) {
553
+ this.config.modelPointers = {
554
+ main: '',
555
+ task: '',
556
+ reasoning: '',
557
+ quick: '',
558
+ }
559
+ }
560
+
561
+ this.config.modelPointers[pointer] = modelName
562
+ this.saveConfig()
563
+ }
564
+
565
+ /**
566
+ * Get all available models for pointer assignment
567
+ */
568
+ getAvailableModels(): ModelProfile[] {
569
+ return this.modelProfiles.filter(p => p.isActive)
570
+ }
571
+
572
+ /**
573
+ * Get all available model names (modelName field)
574
+ */
575
+ getAllAvailableModelNames(): string[] {
576
+ return this.getAvailableModels().map(p => p.modelName)
577
+ }
578
+
579
+ /**
580
+ * Remove a model profile
581
+ */
582
+ removeModel(modelName: string): void {
583
+ this.modelProfiles = this.modelProfiles.filter(
584
+ p => p.modelName !== modelName,
585
+ )
586
+
587
+ // Clean up pointers that reference deleted model
588
+ if (this.config.modelPointers) {
589
+ Object.keys(this.config.modelPointers).forEach(pointer => {
590
+ if (
591
+ this.config.modelPointers[pointer as ModelPointerType] === modelName
592
+ ) {
593
+ this.config.modelPointers[pointer as ModelPointerType] =
594
+ this.config.defaultModelName || ''
595
+ }
596
+ })
597
+ }
598
+
599
+ this.saveConfig()
600
+ }
601
+
602
+ /**
603
+ * Get default model profile
604
+ */
605
+ private getDefaultModel(): ModelProfile | null {
606
+ if (this.config.defaultModelId) {
607
+ const profile = this.findModelProfile(this.config.defaultModelId)
608
+ if (profile && profile.isActive) {
609
+ return profile
610
+ }
611
+ }
612
+ return this.modelProfiles.find(p => p.isActive) || null
613
+ }
614
+
615
+ /**
616
+ * Save configuration changes
617
+ */
618
+ private saveConfig(): void {
619
+ const updatedConfig = {
620
+ ...this.config,
621
+ modelProfiles: this.modelProfiles,
622
+ }
623
+ saveGlobalConfig(updatedConfig)
624
+ }
625
+
626
+ /**
627
+ * Get a fallback model when no specific model is configured
628
+ */
629
+ async getFallbackModel(): Promise<string> {
630
+ const modelConfig = await getModelConfig()
631
+ if (USE_BEDROCK) return modelConfig.bedrock
632
+ if (USE_VERTEX) return modelConfig.vertex
633
+ return modelConfig.firstParty
634
+ }
635
+
636
+ /**
637
+ * 统一的模型解析方法:支持指针、model ID 和真实模型名称
638
+ * @param modelParam - 可以是模型指针 ('main', 'task', etc.)、内部model ID 或真实模型名称 ('gpt-4o', 'claude-3-5-sonnet')
639
+ * @returns ModelProfile 或 null
640
+ */
641
+ resolveModel(modelParam: string | ModelPointerType): ModelProfile | null {
642
+ // 首先检查是否是模型指针
643
+ if (['main', 'task', 'reasoning', 'quick'].includes(modelParam)) {
644
+ const pointerId =
645
+ this.config.modelPointers?.[modelParam as ModelPointerType]
646
+ if (pointerId) {
647
+ // pointerId 可能是内部ID或真实模型名称,尝试两种查找方式
648
+ let profile = this.findModelProfile(pointerId) // 按内部ID查找
649
+ if (!profile) {
650
+ profile = this.findModelProfileByModelName(pointerId) // 按真实模型名查找
651
+ }
652
+ if (profile && profile.isActive) {
653
+ return profile
654
+ }
655
+ }
656
+ // 指针无效时,尝试 fallback 到默认模型
657
+ return this.getDefaultModel()
658
+ }
659
+
660
+ // 不是指针,尝试多种查找方式
661
+ // 1. 尝试按内部 model ID 查找
662
+ let profile = this.findModelProfile(modelParam)
663
+ if (profile && profile.isActive) {
664
+ return profile
665
+ }
666
+
667
+ // 2. 尝试按真实模型名称查找
668
+ profile = this.findModelProfileByModelName(modelParam)
669
+ if (profile && profile.isActive) {
670
+ return profile
671
+ }
672
+
673
+ // 3. 尝试按友好名称查找
674
+ profile = this.findModelProfileByName(modelParam)
675
+ if (profile && profile.isActive) {
676
+ return profile
677
+ }
678
+
679
+ // 所有查找方式都失败,尝试 fallback 到默认模型
680
+ return this.getDefaultModel()
681
+ }
682
+
683
+ /**
684
+ * 解析模型参数并返回完整信息
685
+ */
686
+ resolveModelWithInfo(modelParam: string | ModelPointerType): {
687
+ success: boolean
688
+ profile: ModelProfile | null
689
+ error?: string
690
+ } {
691
+ const isPointer = ['main', 'task', 'reasoning', 'quick'].includes(
692
+ modelParam,
693
+ )
694
+
695
+ if (isPointer) {
696
+ const pointerId =
697
+ this.config.modelPointers?.[modelParam as ModelPointerType]
698
+ if (!pointerId) {
699
+ return {
700
+ success: false,
701
+ profile: null,
702
+ error: `Model pointer '${modelParam}' is not configured. Use /model to set up models.`,
703
+ }
704
+ }
705
+
706
+ // pointerId 可能是内部ID或真实模型名称
707
+ let profile = this.findModelProfile(pointerId)
708
+ if (!profile) {
709
+ profile = this.findModelProfileByModelName(pointerId)
710
+ }
711
+
712
+ if (!profile) {
713
+ return {
714
+ success: false,
715
+ profile: null,
716
+ error: `Model pointer '${modelParam}' points to invalid model '${pointerId}'. Use /model to reconfigure.`,
717
+ }
718
+ }
719
+
720
+ if (!profile.isActive) {
721
+ return {
722
+ success: false,
723
+ profile: null,
724
+ error: `Model '${profile.name}' (pointed by '${modelParam}') is inactive. Use /model to activate it.`,
725
+ }
726
+ }
727
+
728
+ return {
729
+ success: true,
730
+ profile,
731
+ }
732
+ } else {
733
+ // 直接的 model ID 或模型名称,尝试多种查找方式
734
+ let profile = this.findModelProfile(modelParam)
735
+ if (!profile) {
736
+ profile = this.findModelProfileByModelName(modelParam)
737
+ }
738
+ if (!profile) {
739
+ profile = this.findModelProfileByName(modelParam)
740
+ }
741
+
742
+ if (!profile) {
743
+ return {
744
+ success: false,
745
+ profile: null,
746
+ error: `Model '${modelParam}' not found. Use /model to add models.`,
747
+ }
748
+ }
749
+
750
+ if (!profile.isActive) {
751
+ return {
752
+ success: false,
753
+ profile: null,
754
+ error: `Model '${profile.name}' is inactive. Use /model to activate it.`,
755
+ }
756
+ }
757
+
758
+ return {
759
+ success: true,
760
+ profile,
761
+ }
762
+ }
763
+ }
764
+
765
+ // Private helper methods
766
+ private findModelProfile(modelName: string): ModelProfile | null {
767
+ return this.modelProfiles.find(p => p.modelName === modelName) || null
768
+ }
769
+
770
+ private findModelProfileByModelName(modelName: string): ModelProfile | null {
771
+ return this.modelProfiles.find(p => p.modelName === modelName) || null
772
+ }
773
+
774
+ private findModelProfileByName(name: string): ModelProfile | null {
775
+ return this.modelProfiles.find(p => p.name === name) || null
776
+ }
777
+
778
+ private updateLastUsed(modelName: string): void {
779
+ const profile = this.findModelProfile(modelName)
780
+ if (profile) {
781
+ profile.lastUsed = Date.now()
782
+ }
783
+ }
784
+ }
785
+
786
+ // Global ModelManager instance to avoid config read/write race conditions
787
+ let globalModelManager: ModelManager | null = null
788
+
789
+ /**
790
+ * Get the global ModelManager instance (singleton pattern to fix race conditions)
791
+ */
792
+ export const getModelManager = (): ModelManager => {
793
+ try {
794
+ if (!globalModelManager) {
795
+ const config = getGlobalConfig()
796
+ if (!config) {
797
+ console.warn(
798
+ 'No global config available, creating ModelManager with empty config',
799
+ )
800
+ globalModelManager = new ModelManager({
801
+ modelProfiles: [],
802
+ modelPointers: { main: '', task: '', reasoning: '', quick: '' },
803
+ })
804
+ } else {
805
+ globalModelManager = new ModelManager(config)
806
+ }
807
+ }
808
+ return globalModelManager
809
+ } catch (error) {
810
+ console.error('Error creating ModelManager:', error)
811
+ // Return a fallback ModelManager with empty configuration
812
+ return new ModelManager({
813
+ modelProfiles: [],
814
+ modelPointers: { main: '', task: '', reasoning: '', quick: '' },
815
+ })
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Force reload of the global ModelManager instance
821
+ * Used when configuration changes to ensure fresh data
822
+ */
823
+ export const reloadModelManager = (): void => {
824
+ globalModelManager = null
825
+ // Force creation of new instance with fresh config
826
+ getModelManager()
827
+ }
828
+
829
+ /**
830
+ * Get the quick model for fast operations
831
+ */
832
+ export const getQuickModel = (): string => {
833
+ const manager = getModelManager()
834
+ const quickModel = manager.getModel('quick')
835
+ return quickModel?.modelName || 'quick' // Return pointer if model not resolved
836
+ }