@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.
- package/README.md +160 -1
- package/README.zh-CN.md +65 -1
- package/cli.js +5 -10
- package/package.json +6 -2
- package/src/ProjectOnboarding.tsx +47 -29
- package/src/Tool.ts +33 -4
- package/src/commands/agents.tsx +3401 -0
- package/src/commands/help.tsx +2 -2
- package/src/commands/resume.tsx +2 -1
- package/src/commands/terminalSetup.ts +4 -4
- package/src/commands.ts +3 -0
- package/src/components/ApproveApiKey.tsx +1 -1
- package/src/components/Config.tsx +10 -6
- package/src/components/ConsoleOAuthFlow.tsx +5 -4
- package/src/components/CustomSelect/select-option.tsx +28 -2
- package/src/components/CustomSelect/select.tsx +14 -5
- package/src/components/CustomSelect/theme.ts +45 -0
- package/src/components/Help.tsx +4 -4
- package/src/components/InvalidConfigDialog.tsx +1 -1
- package/src/components/LogSelector.tsx +1 -1
- package/src/components/MCPServerApprovalDialog.tsx +1 -1
- package/src/components/Message.tsx +2 -0
- package/src/components/ModelListManager.tsx +10 -6
- package/src/components/ModelSelector.tsx +201 -23
- package/src/components/ModelStatusDisplay.tsx +7 -5
- package/src/components/PromptInput.tsx +146 -96
- package/src/components/SentryErrorBoundary.ts +9 -3
- package/src/components/StickerRequestForm.tsx +16 -0
- package/src/components/StructuredDiff.tsx +36 -29
- package/src/components/TextInput.tsx +13 -0
- package/src/components/TodoItem.tsx +47 -0
- package/src/components/TrustDialog.tsx +1 -1
- package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +5 -1
- package/src/components/messages/AssistantToolUseMessage.tsx +14 -4
- package/src/components/messages/TaskProgressMessage.tsx +32 -0
- package/src/components/messages/TaskToolMessage.tsx +58 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +2 -4
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +1 -1
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +5 -3
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +1 -1
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +5 -3
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +2 -4
- package/src/components/permissions/PermissionRequest.tsx +3 -5
- package/src/constants/macros.ts +2 -0
- package/src/constants/modelCapabilities.ts +179 -0
- package/src/constants/models.ts +90 -0
- package/src/constants/product.ts +1 -1
- package/src/context.ts +7 -7
- package/src/entrypoints/cli.tsx +23 -3
- package/src/entrypoints/mcp.ts +10 -10
- package/src/hooks/useCanUseTool.ts +1 -1
- package/src/hooks/useTextInput.ts +5 -2
- package/src/hooks/useUnifiedCompletion.ts +1405 -0
- package/src/messages.ts +1 -0
- package/src/query.ts +3 -0
- package/src/screens/ConfigureNpmPrefix.tsx +1 -1
- package/src/screens/Doctor.tsx +1 -1
- package/src/screens/REPL.tsx +11 -12
- package/src/services/adapters/base.ts +38 -0
- package/src/services/adapters/chatCompletions.ts +90 -0
- package/src/services/adapters/responsesAPI.ts +170 -0
- package/src/services/claude.ts +198 -62
- package/src/services/customCommands.ts +43 -22
- package/src/services/gpt5ConnectionTest.ts +340 -0
- package/src/services/mcpClient.ts +1 -1
- package/src/services/mentionProcessor.ts +273 -0
- package/src/services/modelAdapterFactory.ts +69 -0
- package/src/services/openai.ts +534 -14
- package/src/services/responseStateManager.ts +90 -0
- package/src/services/systemReminder.ts +113 -12
- package/src/test/testAdapters.ts +96 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +120 -56
- package/src/tools/BashTool/BashTool.tsx +4 -31
- package/src/tools/BashTool/BashToolResultMessage.tsx +1 -1
- package/src/tools/BashTool/OutputLine.tsx +1 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +4 -5
- package/src/tools/FileReadTool/FileReadTool.tsx +43 -10
- package/src/tools/MCPTool/MCPTool.tsx +2 -1
- package/src/tools/MultiEditTool/MultiEditTool.tsx +2 -2
- package/src/tools/NotebookReadTool/NotebookReadTool.tsx +15 -23
- package/src/tools/StickerRequestTool/StickerRequestTool.tsx +1 -1
- package/src/tools/TaskTool/TaskTool.tsx +170 -86
- package/src/tools/TaskTool/prompt.ts +61 -25
- package/src/tools/ThinkTool/ThinkTool.tsx +1 -3
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +65 -41
- package/src/tools/lsTool/lsTool.tsx +5 -2
- package/src/tools.ts +16 -16
- package/src/types/conversation.ts +51 -0
- package/src/types/logs.ts +58 -0
- package/src/types/modelCapabilities.ts +64 -0
- package/src/types/notebook.ts +87 -0
- package/src/utils/advancedFuzzyMatcher.ts +290 -0
- package/src/utils/agentLoader.ts +284 -0
- package/src/utils/ask.tsx +1 -0
- package/src/utils/commands.ts +1 -1
- package/src/utils/commonUnixCommands.ts +161 -0
- package/src/utils/config.ts +173 -2
- package/src/utils/conversationRecovery.ts +1 -0
- package/src/utils/debugLogger.ts +13 -13
- package/src/utils/exampleCommands.ts +1 -0
- package/src/utils/fuzzyMatcher.ts +328 -0
- package/src/utils/messages.tsx +6 -5
- package/src/utils/model.ts +120 -42
- package/src/utils/responseState.ts +23 -0
- package/src/utils/secureFile.ts +559 -0
- package/src/utils/terminal.ts +1 -0
- package/src/utils/theme.ts +11 -0
- 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
|
+
}
|
package/src/utils/messages.tsx
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
372
|
-
|
|
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)
|
package/src/utils/model.ts
CHANGED
|
@@ -164,8 +164,9 @@ export class ModelManager {
|
|
|
164
164
|
contextOverflow: boolean
|
|
165
165
|
usagePercentage: number
|
|
166
166
|
} {
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
196
|
-
const firstModel =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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) %
|
|
249
|
-
const nextModel =
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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.
|
|
302
|
-
?
|
|
303
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|