@shareai-lab/kode 1.0.71 → 1.0.75

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 (108) hide show
  1. package/README.md +160 -1
  2. package/README.zh-CN.md +65 -1
  3. package/cli.js +5 -10
  4. package/package.json +6 -2
  5. package/src/ProjectOnboarding.tsx +47 -29
  6. package/src/Tool.ts +33 -4
  7. package/src/commands/agents.tsx +3401 -0
  8. package/src/commands/help.tsx +2 -2
  9. package/src/commands/resume.tsx +2 -1
  10. package/src/commands/terminalSetup.ts +4 -4
  11. package/src/commands.ts +3 -0
  12. package/src/components/ApproveApiKey.tsx +1 -1
  13. package/src/components/Config.tsx +10 -6
  14. package/src/components/ConsoleOAuthFlow.tsx +5 -4
  15. package/src/components/CustomSelect/select-option.tsx +28 -2
  16. package/src/components/CustomSelect/select.tsx +14 -5
  17. package/src/components/CustomSelect/theme.ts +45 -0
  18. package/src/components/Help.tsx +4 -4
  19. package/src/components/InvalidConfigDialog.tsx +1 -1
  20. package/src/components/LogSelector.tsx +1 -1
  21. package/src/components/MCPServerApprovalDialog.tsx +1 -1
  22. package/src/components/Message.tsx +2 -0
  23. package/src/components/ModelListManager.tsx +10 -6
  24. package/src/components/ModelSelector.tsx +201 -23
  25. package/src/components/ModelStatusDisplay.tsx +7 -5
  26. package/src/components/PromptInput.tsx +146 -96
  27. package/src/components/SentryErrorBoundary.ts +9 -3
  28. package/src/components/StickerRequestForm.tsx +16 -0
  29. package/src/components/StructuredDiff.tsx +36 -29
  30. package/src/components/TextInput.tsx +13 -0
  31. package/src/components/TodoItem.tsx +47 -0
  32. package/src/components/TrustDialog.tsx +1 -1
  33. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +5 -1
  34. package/src/components/messages/AssistantToolUseMessage.tsx +14 -4
  35. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  36. package/src/components/messages/TaskToolMessage.tsx +58 -0
  37. package/src/components/permissions/FallbackPermissionRequest.tsx +2 -4
  38. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +1 -1
  39. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +5 -3
  40. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +1 -1
  41. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +5 -3
  42. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +2 -4
  43. package/src/components/permissions/PermissionRequest.tsx +3 -5
  44. package/src/constants/macros.ts +2 -0
  45. package/src/constants/modelCapabilities.ts +179 -0
  46. package/src/constants/models.ts +90 -0
  47. package/src/constants/product.ts +1 -1
  48. package/src/context.ts +7 -7
  49. package/src/entrypoints/cli.tsx +23 -3
  50. package/src/entrypoints/mcp.ts +10 -10
  51. package/src/hooks/useCanUseTool.ts +1 -1
  52. package/src/hooks/useTextInput.ts +5 -2
  53. package/src/hooks/useUnifiedCompletion.ts +1405 -0
  54. package/src/messages.ts +1 -0
  55. package/src/query.ts +3 -0
  56. package/src/screens/ConfigureNpmPrefix.tsx +1 -1
  57. package/src/screens/Doctor.tsx +1 -1
  58. package/src/screens/REPL.tsx +11 -12
  59. package/src/services/adapters/base.ts +38 -0
  60. package/src/services/adapters/chatCompletions.ts +90 -0
  61. package/src/services/adapters/responsesAPI.ts +170 -0
  62. package/src/services/claude.ts +198 -62
  63. package/src/services/customCommands.ts +43 -22
  64. package/src/services/gpt5ConnectionTest.ts +340 -0
  65. package/src/services/mcpClient.ts +1 -1
  66. package/src/services/mentionProcessor.ts +273 -0
  67. package/src/services/modelAdapterFactory.ts +69 -0
  68. package/src/services/openai.ts +534 -14
  69. package/src/services/responseStateManager.ts +90 -0
  70. package/src/services/systemReminder.ts +113 -12
  71. package/src/test/testAdapters.ts +96 -0
  72. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +120 -56
  73. package/src/tools/BashTool/BashTool.tsx +4 -31
  74. package/src/tools/BashTool/BashToolResultMessage.tsx +1 -1
  75. package/src/tools/BashTool/OutputLine.tsx +1 -0
  76. package/src/tools/FileEditTool/FileEditTool.tsx +4 -5
  77. package/src/tools/FileReadTool/FileReadTool.tsx +43 -10
  78. package/src/tools/MCPTool/MCPTool.tsx +2 -1
  79. package/src/tools/MultiEditTool/MultiEditTool.tsx +2 -2
  80. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +15 -23
  81. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +1 -1
  82. package/src/tools/TaskTool/TaskTool.tsx +170 -86
  83. package/src/tools/TaskTool/prompt.ts +61 -25
  84. package/src/tools/ThinkTool/ThinkTool.tsx +1 -3
  85. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +65 -41
  86. package/src/tools/lsTool/lsTool.tsx +5 -2
  87. package/src/tools.ts +16 -16
  88. package/src/types/conversation.ts +51 -0
  89. package/src/types/logs.ts +58 -0
  90. package/src/types/modelCapabilities.ts +64 -0
  91. package/src/types/notebook.ts +87 -0
  92. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  93. package/src/utils/agentLoader.ts +284 -0
  94. package/src/utils/ask.tsx +1 -0
  95. package/src/utils/commands.ts +1 -1
  96. package/src/utils/commonUnixCommands.ts +161 -0
  97. package/src/utils/config.ts +173 -2
  98. package/src/utils/conversationRecovery.ts +1 -0
  99. package/src/utils/debugLogger.ts +13 -13
  100. package/src/utils/exampleCommands.ts +1 -0
  101. package/src/utils/fuzzyMatcher.ts +328 -0
  102. package/src/utils/messages.tsx +6 -5
  103. package/src/utils/model.ts +120 -42
  104. package/src/utils/responseState.ts +23 -0
  105. package/src/utils/secureFile.ts +559 -0
  106. package/src/utils/terminal.ts +1 -0
  107. package/src/utils/theme.ts +11 -0
  108. package/src/hooks/useSlashCommandTypeahead.ts +0 -137
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Input Method Inspired Fuzzy Matching Algorithm
3
+ *
4
+ * Multi-algorithm weighted scoring system inspired by:
5
+ * - Sogou/Baidu Pinyin input method algorithms
6
+ * - Double-pinyin abbreviation matching
7
+ * - Terminal completion best practices (fzf, zsh, fish)
8
+ *
9
+ * Designed specifically for command/terminal completion scenarios
10
+ * where users type abbreviations like "nde" expecting "node"
11
+ */
12
+
13
+ export interface MatchResult {
14
+ score: number
15
+ algorithm: string // Which algorithm contributed most to the score
16
+ confidence: number // 0-1 confidence level
17
+ }
18
+
19
+ export interface FuzzyMatcherConfig {
20
+ // Algorithm weights (must sum to 1.0)
21
+ weights: {
22
+ prefix: number // Direct prefix matching ("nod" → "node")
23
+ substring: number // Substring matching ("ode" → "node")
24
+ abbreviation: number // Key chars matching ("nde" → "node")
25
+ editDistance: number // Typo tolerance ("noda" → "node")
26
+ popularity: number // Common command boost
27
+ }
28
+
29
+ // Scoring parameters
30
+ minScore: number // Minimum score threshold
31
+ maxEditDistance: number // Maximum edits allowed
32
+ popularCommands: string[] // Commands to boost
33
+ }
34
+
35
+ const DEFAULT_CONFIG: FuzzyMatcherConfig = {
36
+ weights: {
37
+ prefix: 0.35, // Strong weight for prefix matching
38
+ substring: 0.20, // Good for partial matches
39
+ abbreviation: 0.30, // Key for "nde"→"node" cases
40
+ editDistance: 0.10, // Typo tolerance
41
+ popularity: 0.05 // Slight bias for common commands
42
+ },
43
+ minScore: 10, // Lower threshold for better matching
44
+ maxEditDistance: 2,
45
+ popularCommands: [
46
+ 'node', 'npm', 'git', 'ls', 'cd', 'cat', 'grep', 'find', 'cp', 'mv',
47
+ 'python', 'java', 'docker', 'curl', 'wget', 'vim', 'nano'
48
+ ]
49
+ }
50
+
51
+ export class FuzzyMatcher {
52
+ private config: FuzzyMatcherConfig
53
+
54
+ constructor(config: Partial<FuzzyMatcherConfig> = {}) {
55
+ this.config = { ...DEFAULT_CONFIG, ...config }
56
+
57
+ // Normalize weights to sum to 1.0
58
+ const weightSum = Object.values(this.config.weights).reduce((a, b) => a + b, 0)
59
+ if (Math.abs(weightSum - 1.0) > 0.01) {
60
+ Object.keys(this.config.weights).forEach(key => {
61
+ this.config.weights[key as keyof typeof this.config.weights] /= weightSum
62
+ })
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Calculate fuzzy match score for a candidate against a query
68
+ */
69
+ match(candidate: string, query: string): MatchResult {
70
+ const text = candidate.toLowerCase()
71
+ const pattern = query.toLowerCase()
72
+
73
+ // Quick perfect match exits
74
+ if (text === pattern) {
75
+ return { score: 1000, algorithm: 'exact', confidence: 1.0 }
76
+ }
77
+ if (text.startsWith(pattern)) {
78
+ return {
79
+ score: 900 + (10 - pattern.length),
80
+ algorithm: 'prefix-exact',
81
+ confidence: 0.95
82
+ }
83
+ }
84
+
85
+ // Run all algorithms
86
+ const scores = {
87
+ prefix: this.prefixScore(text, pattern),
88
+ substring: this.substringScore(text, pattern),
89
+ abbreviation: this.abbreviationScore(text, pattern),
90
+ editDistance: this.editDistanceScore(text, pattern),
91
+ popularity: this.popularityScore(text)
92
+ }
93
+
94
+ // Weighted combination
95
+ const rawScore = Object.entries(scores).reduce((total, [algorithm, score]) => {
96
+ const weight = this.config.weights[algorithm as keyof typeof this.config.weights]
97
+ return total + (score * weight)
98
+ }, 0)
99
+
100
+ // Length penalty (prefer shorter commands)
101
+ const lengthPenalty = Math.max(0, text.length - 6) * 1.5
102
+ const finalScore = Math.max(0, rawScore - lengthPenalty)
103
+
104
+ // Determine primary algorithm and confidence
105
+ const maxAlgorithm = Object.entries(scores).reduce((max, [alg, score]) =>
106
+ score > max.score ? { algorithm: alg, score } : max,
107
+ { algorithm: 'none', score: 0 }
108
+ )
109
+
110
+ const confidence = Math.min(1.0, finalScore / 100)
111
+
112
+ return {
113
+ score: finalScore,
114
+ algorithm: maxAlgorithm.algorithm,
115
+ confidence
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Algorithm 1: Prefix Matching (like pinyin prefix)
121
+ * Handles cases like "nod" → "node"
122
+ */
123
+ private prefixScore(text: string, pattern: string): number {
124
+ if (!text.startsWith(pattern)) return 0
125
+
126
+ // Score based on prefix length vs total length
127
+ const coverage = pattern.length / text.length
128
+ return 100 * coverage
129
+ }
130
+
131
+ /**
132
+ * Algorithm 2: Substring Matching (like pinyin contains)
133
+ * Handles cases like "ode" → "node", "py3" → "python3"
134
+ */
135
+ private substringScore(text: string, pattern: string): number {
136
+ // Direct substring match
137
+ const index = text.indexOf(pattern)
138
+ if (index !== -1) {
139
+ // Earlier position and better coverage = higher score
140
+ const positionFactor = Math.max(0, 10 - index) / 10
141
+ const coverageFactor = pattern.length / text.length
142
+ return 80 * positionFactor * coverageFactor
143
+ }
144
+
145
+ // Special handling for numeric suffixes (py3 → python3)
146
+ // Check if pattern ends with a number and try prefix match + number
147
+ const numMatch = pattern.match(/^(.+?)(\d+)$/)
148
+ if (numMatch) {
149
+ const [, prefix, num] = numMatch
150
+ // Check if text starts with prefix and ends with the same number
151
+ if (text.startsWith(prefix) && text.endsWith(num)) {
152
+ // Good match for patterns like "py3" → "python3"
153
+ const coverageFactor = pattern.length / text.length
154
+ return 70 * coverageFactor + 20 // Bonus for numeric suffix match
155
+ }
156
+ }
157
+
158
+ return 0
159
+ }
160
+
161
+ /**
162
+ * Algorithm 3: Abbreviation Matching (key innovation)
163
+ * Handles cases like "nde" → "node", "pyt3" → "python3", "gp5" → "gpt-5"
164
+ */
165
+ private abbreviationScore(text: string, pattern: string): number {
166
+ let score = 0
167
+ let textPos = 0
168
+ let perfectStart = false
169
+ let consecutiveMatches = 0
170
+ let wordBoundaryMatches = 0
171
+
172
+ // Split text by hyphens to handle word boundaries better
173
+ const textWords = text.split('-')
174
+ const textClean = text.replace(/-/g, '').toLowerCase()
175
+
176
+ for (let i = 0; i < pattern.length; i++) {
177
+ const char = pattern[i]
178
+ let charFound = false
179
+
180
+ // Try to find in clean text (no hyphens)
181
+ for (let j = textPos; j < textClean.length; j++) {
182
+ if (textClean[j] === char) {
183
+ charFound = true
184
+
185
+ // Check if this character is at a word boundary in original text
186
+ let originalPos = 0
187
+ let cleanPos = 0
188
+ for (let k = 0; k < text.length; k++) {
189
+ if (text[k] === '-') continue
190
+ if (cleanPos === j) {
191
+ originalPos = k
192
+ break
193
+ }
194
+ cleanPos++
195
+ }
196
+
197
+ // Consecutive character bonus
198
+ if (j === textPos) {
199
+ consecutiveMatches++
200
+ } else {
201
+ consecutiveMatches = 1
202
+ }
203
+
204
+ // Position-sensitive scoring
205
+ if (i === 0 && j === 0) {
206
+ score += 50 // Perfect first character
207
+ perfectStart = true
208
+ } else if (originalPos === 0 || text[originalPos - 1] === '-') {
209
+ score += 35 // Word boundary match
210
+ wordBoundaryMatches++
211
+ } else if (j <= 2) {
212
+ score += 20 // Early position
213
+ } else if (j <= 6) {
214
+ score += 10 // Mid position
215
+ } else {
216
+ score += 5 // Late position
217
+ }
218
+
219
+ // Consecutive character bonus
220
+ if (consecutiveMatches > 1) {
221
+ score += consecutiveMatches * 5
222
+ }
223
+
224
+ textPos = j + 1
225
+ break
226
+ }
227
+ }
228
+
229
+ if (!charFound) return 0 // Invalid abbreviation
230
+ }
231
+
232
+ // Critical bonuses
233
+ if (perfectStart) score += 30
234
+ if (wordBoundaryMatches >= 2) score += 25 // Multiple word boundaries
235
+ if (textPos <= textClean.length * 0.8) score += 15 // Compact abbreviation
236
+
237
+ // Special bonus for number matching at end
238
+ const lastPatternChar = pattern[pattern.length - 1]
239
+ const lastTextChar = text[text.length - 1]
240
+ if (/\d/.test(lastPatternChar) && lastPatternChar === lastTextChar) {
241
+ score += 25
242
+ }
243
+
244
+ return score
245
+ }
246
+
247
+ /**
248
+ * Algorithm 4: Edit Distance (typo tolerance)
249
+ * Handles cases like "noda" → "node"
250
+ */
251
+ private editDistanceScore(text: string, pattern: string): number {
252
+ if (pattern.length > text.length + this.config.maxEditDistance) return 0
253
+
254
+ // Simplified Levenshtein distance
255
+ const dp: number[][] = []
256
+ const m = pattern.length
257
+ const n = text.length
258
+
259
+ // Initialize DP table
260
+ for (let i = 0; i <= m; i++) {
261
+ dp[i] = []
262
+ for (let j = 0; j <= n; j++) {
263
+ if (i === 0) dp[i][j] = j
264
+ else if (j === 0) dp[i][j] = i
265
+ else {
266
+ const cost = pattern[i-1] === text[j-1] ? 0 : 1
267
+ dp[i][j] = Math.min(
268
+ dp[i-1][j] + 1, // deletion
269
+ dp[i][j-1] + 1, // insertion
270
+ dp[i-1][j-1] + cost // substitution
271
+ )
272
+ }
273
+ }
274
+ }
275
+
276
+ const distance = dp[m][n]
277
+ if (distance > this.config.maxEditDistance) return 0
278
+
279
+ return Math.max(0, 30 - distance * 10)
280
+ }
281
+
282
+ /**
283
+ * Algorithm 5: Command Popularity (like frequency in input method)
284
+ * Boost common commands that users frequently type
285
+ */
286
+ private popularityScore(text: string): number {
287
+ if (this.config.popularCommands.includes(text)) {
288
+ return 40
289
+ }
290
+
291
+ // Short commands are often more commonly used
292
+ if (text.length <= 5) return 10
293
+
294
+ return 0
295
+ }
296
+
297
+ /**
298
+ * Batch match multiple candidates and return sorted results
299
+ */
300
+ matchMany(candidates: string[], query: string): Array<{candidate: string, result: MatchResult}> {
301
+ return candidates
302
+ .map(candidate => ({
303
+ candidate,
304
+ result: this.match(candidate, query)
305
+ }))
306
+ .filter(item => item.result.score >= this.config.minScore)
307
+ .sort((a, b) => b.result.score - a.result.score)
308
+ }
309
+ }
310
+
311
+ // Export convenience functions
312
+ export const defaultMatcher = new FuzzyMatcher()
313
+
314
+ export function matchCommand(command: string, query: string): MatchResult {
315
+ return defaultMatcher.match(command, query)
316
+ }
317
+
318
+ // Import the advanced matcher
319
+ import { matchManyAdvanced } from './advancedFuzzyMatcher'
320
+
321
+ export function matchCommands(commands: string[], query: string): Array<{command: string, score: number}> {
322
+ // Use the advanced matcher for better results
323
+ return matchManyAdvanced(commands, query, 5) // Lower threshold for better matching
324
+ .map(item => ({
325
+ command: item.candidate,
326
+ score: item.score
327
+ }))
328
+ }
@@ -355,7 +355,7 @@ export async function processUserInput(
355
355
  if (input.includes('!`') || input.includes('@')) {
356
356
  try {
357
357
  // Import functions from customCommands service to avoid code duplication
358
- const { executeBashCommands, resolveFileReferences } = await import(
358
+ const { executeBashCommands } = await import(
359
359
  '../services/customCommands'
360
360
  )
361
361
 
@@ -366,11 +366,12 @@ export async function processUserInput(
366
366
  processedInput = await executeBashCommands(processedInput)
367
367
  }
368
368
 
369
- // Resolve file references if present
369
+ // Process mentions for system reminder integration
370
+ // Note: We don't call resolveFileReferences here anymore -
371
+ // @file mentions should trigger Read tool usage via reminders, not embed content
370
372
  if (input.includes('@')) {
371
- // Note: This function is not exported from customCommands.ts, so we need to expose it
372
- // For now, we'll keep the local implementation until we refactor the service
373
- processedInput = await resolveFileReferences(processedInput)
373
+ const { processMentions } = await import('../services/mentionProcessor')
374
+ await processMentions(input)
374
375
  }
375
376
  } catch (error) {
376
377
  console.warn('Dynamic content processing failed:', error)
@@ -164,8 +164,9 @@ export class ModelManager {
164
164
  contextOverflow: boolean
165
165
  usagePercentage: number
166
166
  } {
167
- const activeProfiles = this.modelProfiles.filter(p => p.isActive)
168
- if (activeProfiles.length === 0) {
167
+ // Use ALL configured models, not just active ones
168
+ const allProfiles = this.getAllConfiguredModels()
169
+ if (allProfiles.length === 0) {
169
170
  return {
170
171
  success: false,
171
172
  modelName: null,
@@ -175,14 +176,10 @@ export class ModelManager {
175
176
  }
176
177
  }
177
178
 
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
179
+ // Sort by createdAt for consistent cycling order (don't use lastUsed)
180
+ // Using lastUsed causes the order to change each time, preventing proper cycling
181
+ allProfiles.sort((a, b) => {
182
+ return a.createdAt - b.createdAt // Oldest first for consistent order
186
183
  })
187
184
 
188
185
  const currentMainModelName = this.config.modelPointers?.main
@@ -192,8 +189,11 @@ export class ModelManager {
192
189
  const previousModelName = currentModel?.name || null
193
190
 
194
191
  if (!currentMainModelName) {
195
- // No current main model, select first active
196
- const firstModel = activeProfiles[0]
192
+ // No current main model, select first available (activate if needed)
193
+ const firstModel = allProfiles[0]
194
+ if (!firstModel.isActive) {
195
+ firstModel.isActive = true
196
+ }
197
197
  this.setPointer('main', firstModel.modelName)
198
198
  this.updateLastUsed(firstModel.modelName)
199
199
 
@@ -210,13 +210,16 @@ export class ModelManager {
210
210
  }
211
211
  }
212
212
 
213
- // Find current model index
214
- const currentIndex = activeProfiles.findIndex(
213
+ // Find current model index in ALL models
214
+ const currentIndex = allProfiles.findIndex(
215
215
  p => p.modelName === currentMainModelName,
216
216
  )
217
217
  if (currentIndex === -1) {
218
- // Current model not found, select first
219
- const firstModel = activeProfiles[0]
218
+ // Current model not found, select first available (activate if needed)
219
+ const firstModel = allProfiles[0]
220
+ if (!firstModel.isActive) {
221
+ firstModel.isActive = true
222
+ }
220
223
  this.setPointer('main', firstModel.modelName)
221
224
  this.updateLastUsed(firstModel.modelName)
222
225
 
@@ -234,7 +237,7 @@ export class ModelManager {
234
237
  }
235
238
 
236
239
  // Check if only one model is available
237
- if (activeProfiles.length === 1) {
240
+ if (allProfiles.length === 1) {
238
241
  return {
239
242
  success: false,
240
243
  modelName: null,
@@ -244,9 +247,15 @@ export class ModelManager {
244
247
  }
245
248
  }
246
249
 
247
- // Get next model in cycle
248
- const nextIndex = (currentIndex + 1) % activeProfiles.length
249
- const nextModel = activeProfiles[nextIndex]
250
+ // Get next model in cycle (from ALL models)
251
+ const nextIndex = (currentIndex + 1) % allProfiles.length
252
+ const nextModel = allProfiles[nextIndex]
253
+
254
+ // Activate the model if it's not already active
255
+ const wasInactive = !nextModel.isActive
256
+ if (!nextModel.isActive) {
257
+ nextModel.isActive = true
258
+ }
250
259
 
251
260
  // Analyze context compatibility for next model
252
261
  const analysis = this.analyzeContextCompatibility(
@@ -257,6 +266,11 @@ export class ModelManager {
257
266
  // Always switch to next model, but return context status
258
267
  this.setPointer('main', nextModel.modelName)
259
268
  this.updateLastUsed(nextModel.modelName)
269
+
270
+ // Save configuration if we activated a new model
271
+ if (wasInactive) {
272
+ this.saveConfig()
273
+ }
260
274
 
261
275
  return {
262
276
  success: true,
@@ -278,29 +292,43 @@ export class ModelManager {
278
292
  blocked?: boolean
279
293
  message?: string
280
294
  } {
295
+ // Use the enhanced context check method for consistency
281
296
  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.`,
297
+
298
+ if (!result.success) {
299
+ const allModels = this.getAllConfiguredModels()
300
+ if (allModels.length === 0) {
301
+ return {
302
+ success: false,
303
+ modelName: null,
304
+ blocked: false,
305
+ message: '❌ No models configured. Use /model to add models.',
306
+ }
307
+ } else if (allModels.length === 1) {
308
+ return {
309
+ success: false,
310
+ modelName: null,
311
+ blocked: false,
312
+ message: `⚠️ Only one model configured (${allModels[0].modelName}). Use /model to add more models for switching.`,
313
+ }
294
314
  }
295
315
  }
296
-
316
+
317
+ // Convert the detailed result to the simple interface
318
+ const currentModel = this.findModelProfile(this.config.modelPointers?.main)
319
+ const allModels = this.getAllConfiguredModels()
320
+ const currentIndex = allModels.findIndex(m => m.modelName === currentModel?.modelName)
321
+ const totalModels = allModels.length
322
+
297
323
  return {
298
324
  success: result.success,
299
325
  modelName: result.modelName,
300
326
  blocked: result.contextOverflow,
301
- message: result.contextOverflow
302
- ? `Context usage: ${result.usagePercentage.toFixed(1)}%`
303
- : undefined,
327
+ message: result.success
328
+ ? result.contextOverflow
329
+ ? `⚠️ Context usage: ${result.usagePercentage.toFixed(1)}% - ${result.modelName}`
330
+ : `✅ Switched to ${result.modelName} (${currentIndex + 1}/${totalModels})${currentModel?.provider ? ` [${currentModel.provider}]` : ''}`
331
+ : `❌ Failed to switch models`,
304
332
  }
305
333
  }
306
334
 
@@ -368,9 +396,9 @@ export class ModelManager {
368
396
  requiresCompression: boolean
369
397
  estimatedTokensAfterSwitch: number
370
398
  } {
371
- const modelName = this.switchToNextModel(currentContextTokens)
399
+ const result = this.switchToNextModel(currentContextTokens)
372
400
 
373
- if (!modelName) {
401
+ if (!result.success || !result.modelName) {
374
402
  return {
375
403
  modelName: null,
376
404
  contextAnalysis: null,
@@ -382,7 +410,7 @@ export class ModelManager {
382
410
  const newModel = this.getModel('main')
383
411
  if (!newModel) {
384
412
  return {
385
- modelName,
413
+ modelName: result.modelName,
386
414
  contextAnalysis: null,
387
415
  requiresCompression: false,
388
416
  estimatedTokensAfterSwitch: currentContextTokens,
@@ -395,7 +423,7 @@ export class ModelManager {
395
423
  )
396
424
 
397
425
  return {
398
- modelName,
426
+ modelName: result.modelName,
399
427
  contextAnalysis: analysis,
400
428
  requiresCompression: analysis.severity === 'critical',
401
429
  estimatedTokensAfterSwitch: currentContextTokens,
@@ -563,19 +591,69 @@ export class ModelManager {
563
591
  }
564
592
 
565
593
  /**
566
- * Get all available models for pointer assignment
594
+ * Get all active models for pointer assignment
567
595
  */
568
596
  getAvailableModels(): ModelProfile[] {
569
597
  return this.modelProfiles.filter(p => p.isActive)
570
598
  }
571
599
 
572
600
  /**
573
- * Get all available model names (modelName field)
601
+ * Get all configured models (both active and inactive) for switching
602
+ */
603
+ getAllConfiguredModels(): ModelProfile[] {
604
+ return this.modelProfiles
605
+ }
606
+
607
+ /**
608
+ * Get all available model names (modelName field) - active only
574
609
  */
575
610
  getAllAvailableModelNames(): string[] {
576
611
  return this.getAvailableModels().map(p => p.modelName)
577
612
  }
578
613
 
614
+ /**
615
+ * Get all configured model names (both active and inactive)
616
+ */
617
+ getAllConfiguredModelNames(): string[] {
618
+ return this.getAllConfiguredModels().map(p => p.modelName)
619
+ }
620
+
621
+ /**
622
+ * Debug method to get detailed model switching information
623
+ */
624
+ getModelSwitchingDebugInfo(): {
625
+ totalModels: number
626
+ activeModels: number
627
+ inactiveModels: number
628
+ currentMainModel: string | null
629
+ availableModels: Array<{
630
+ name: string
631
+ modelName: string
632
+ provider: string
633
+ isActive: boolean
634
+ lastUsed?: number
635
+ }>
636
+ modelPointers: Record<string, string | undefined>
637
+ } {
638
+ const availableModels = this.getAvailableModels()
639
+ const currentMainModelName = this.config.modelPointers?.main
640
+
641
+ return {
642
+ totalModels: this.modelProfiles.length,
643
+ activeModels: availableModels.length,
644
+ inactiveModels: this.modelProfiles.length - availableModels.length,
645
+ currentMainModel: currentMainModelName || null,
646
+ availableModels: this.modelProfiles.map(p => ({
647
+ name: p.name,
648
+ modelName: p.modelName,
649
+ provider: p.provider,
650
+ isActive: p.isActive,
651
+ lastUsed: p.lastUsed,
652
+ })),
653
+ modelPointers: this.config.modelPointers || {},
654
+ }
655
+ }
656
+
579
657
  /**
580
658
  * Remove a model profile
581
659
  */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Response state management for Responses API
3
+ * Tracks previous_response_id for conversation chaining
4
+ */
5
+
6
+ // Store the last response ID for each conversation
7
+ const responseIdCache = new Map<string, string>()
8
+
9
+ export function getLastResponseId(conversationId: string): string | undefined {
10
+ return responseIdCache.get(conversationId)
11
+ }
12
+
13
+ export function setLastResponseId(conversationId: string, responseId: string): void {
14
+ responseIdCache.set(conversationId, responseId)
15
+ }
16
+
17
+ export function clearResponseId(conversationId: string): void {
18
+ responseIdCache.delete(conversationId)
19
+ }
20
+
21
+ export function clearAllResponseIds(): void {
22
+ responseIdCache.clear()
23
+ }