@shareai-lab/kode 1.0.73 → 1.0.76

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 CHANGED
@@ -23,6 +23,10 @@ Use `# Your documentation request` to generate and maintain your AGENTS.md file
23
23
 
24
24
  Kode is a powerful AI assistant that lives in your terminal. It can understand your codebase, edit files, run commands, and handle entire workflows for you.
25
25
 
26
+ > **⚠️ Security Notice**: Kode runs in YOLO mode by default (equivalent to Claude's `--dangerously-skip-permissions` flag), bypassing all permission checks for maximum productivity. YOLO mode is recommended only for trusted, secure environments when working on non-critical projects. If you're working with important files or using models of questionable capability, we strongly recommend using `kode --safe` to enable permission checks and manual approval for all operations.
27
+ >
28
+ > **📊 Model Performance**: For optimal performance, we recommend using newer, more capable models designed for autonomous task completion. Avoid older Q&A-focused models like GPT-4o or Gemini 2.5 Pro, which are optimized for answering questions rather than sustained independent task execution. Choose models specifically trained for agentic workflows and extended reasoning capabilities.
29
+
26
30
  ## Features
27
31
 
28
32
  ### Core Capabilities
@@ -64,6 +68,20 @@ Our state-of-the-art completion system provides unparalleled coding assistance:
64
68
 
65
69
  ## Installation
66
70
 
71
+ ### Recommended: Using Bun (Fastest)
72
+
73
+ First install Bun if you haven't already:
74
+ ```bash
75
+ curl -fsSL https://bun.sh/install | bash
76
+ ```
77
+
78
+ Then install Kode:
79
+ ```bash
80
+ bun add -g @shareai-lab/kode
81
+ ```
82
+
83
+ ### Alternative: Using npm
84
+
67
85
  ```bash
68
86
  npm install -g @shareai-lab/kode
69
87
  ```
package/README.zh-CN.md CHANGED
@@ -7,6 +7,10 @@
7
7
 
8
8
  Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。
9
9
 
10
+ > **⚠️ 安全提示**:Kode 默认以 YOLO 模式运行(等同于 Claude 的 `--dangerously-skip-permissions` 标志),跳过所有权限检查以获得最大生产力。YOLO 模式仅建议在安全可信的环境中处理非重要项目时使用。如果您正在处理重要文件或使用能力存疑的模型,我们强烈建议使用 `kode --safe` 启用权限检查和手动审批所有操作。
11
+ >
12
+ > **📊 模型性能建议**:为获得最佳体验,建议使用专为自主任务完成设计的新一代强大模型。避免使用 GPT-4o、Gemini 2.5 Pro 等较老的问答型模型,它们主要针对回答问题进行优化,而非持续的独立任务执行。请选择专门训练用于智能体工作流和扩展推理能力的模型。
13
+
10
14
  ## 功能特性
11
15
 
12
16
  - 🤖 **AI 驱动的助手** - 使用先进的 AI 模型理解并响应你的请求
@@ -21,6 +25,20 @@ Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你
21
25
 
22
26
  ## 安装
23
27
 
28
+ ### 推荐方式:使用 Bun(最快)
29
+
30
+ 首先安装 Bun(如果尚未安装):
31
+ ```bash
32
+ curl -fsSL https://bun.sh/install | bash
33
+ ```
34
+
35
+ 然后安装 Kode:
36
+ ```bash
37
+ bun add -g @shareai-lab/kode
38
+ ```
39
+
40
+ ### 备选方式:使用 npm
41
+
24
42
  ```bash
25
43
  npm install -g @shareai-lab/kode
26
44
  ```
package/cli.js CHANGED
@@ -32,24 +32,19 @@ try {
32
32
  }
33
33
 
34
34
  function runWithNode() {
35
- // Use node with tsx loader
36
- const child = spawn('node', [
37
- '--loader', 'tsx',
38
- '--no-warnings',
39
- cliPath,
40
- ...args
41
- ], {
35
+ // Use local tsx installation
36
+ const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
37
+ const child = spawn(tsxPath, [cliPath, ...args], {
42
38
  stdio: 'inherit',
43
39
  env: {
44
40
  ...process.env,
45
- NODE_OPTIONS: '--loader tsx --no-warnings',
46
41
  YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
47
42
  }
48
43
  });
49
44
 
50
45
  child.on('error', (err) => {
51
- if (err.code === 'MODULE_NOT_FOUND' || err.message.includes('tsx')) {
52
- console.error('\nError: tsx is required but not installed.');
46
+ if (err.code === 'ENOENT') {
47
+ console.error('\nError: tsx is required but not found.');
53
48
  console.error('Please run: npm install');
54
49
  process.exit(1);
55
50
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shareai-lab/kode",
3
- "version": "1.0.73",
3
+ "version": "1.0.76",
4
4
  "bin": {
5
5
  "kode": "cli.js",
6
6
  "kwa": "cli.js",
@@ -227,20 +227,22 @@ function PromptInput({
227
227
  [onModeChange, onInputChange],
228
228
  )
229
229
 
230
- // Handle Tab key model switching with simple context check
230
+ // Handle Shift+M model switching with enhanced debugging
231
231
  const handleQuickModelSwitch = useCallback(async () => {
232
232
  const modelManager = getModelManager()
233
233
  const currentTokens = countTokens(messages)
234
234
 
235
+ // Get debug info for better error reporting
236
+ const debugInfo = modelManager.getModelSwitchingDebugInfo()
237
+
235
238
  const switchResult = modelManager.switchToNextModel(currentTokens)
236
239
 
237
240
  if (switchResult.success && switchResult.modelName) {
238
- // Successful switch
241
+ // Successful switch - use enhanced message from model manager
239
242
  onSubmitCountChange(prev => prev + 1)
240
- const newModel = modelManager.getModel('main')
241
243
  setModelSwitchMessage({
242
244
  show: true,
243
- text: `✅ Switched to ${switchResult.modelName} (${newModel?.provider || 'Unknown'} | Model: ${newModel?.modelName || 'N/A'})`,
245
+ text: switchResult.message || `✅ Switched to ${switchResult.modelName}`,
244
246
  })
245
247
  setTimeout(() => setModelSwitchMessage({ show: false }), 3000)
246
248
  } else if (switchResult.blocked && switchResult.message) {
@@ -251,14 +253,28 @@ function PromptInput({
251
253
  })
252
254
  setTimeout(() => setModelSwitchMessage({ show: false }), 5000)
253
255
  } else {
254
- // No other models available or other error
256
+ // Enhanced error reporting with debug info
257
+ let errorMessage = switchResult.message
258
+
259
+ if (!errorMessage) {
260
+ if (debugInfo.totalModels === 0) {
261
+ errorMessage = '❌ No models configured. Use /model to add models.'
262
+ } else if (debugInfo.activeModels === 0) {
263
+ errorMessage = `❌ No active models (${debugInfo.totalModels} total, all inactive). Use /model to activate models.`
264
+ } else if (debugInfo.activeModels === 1) {
265
+ // Show ALL models including inactive ones for debugging
266
+ const allModelNames = debugInfo.availableModels.map(m => `${m.name}${m.isActive ? '' : ' (inactive)'}`).join(', ')
267
+ errorMessage = `⚠️ Only 1 active model out of ${debugInfo.totalModels} total models: ${allModelNames}. ALL configured models will be activated for switching.`
268
+ } else {
269
+ errorMessage = `❌ Model switching failed (${debugInfo.activeModels} active, ${debugInfo.totalModels} total models available)`
270
+ }
271
+ }
272
+
255
273
  setModelSwitchMessage({
256
274
  show: true,
257
- text:
258
- switchResult.message ||
259
- '⚠️ No other models configured. Use /model to add more models',
275
+ text: errorMessage,
260
276
  })
261
- setTimeout(() => setModelSwitchMessage({ show: false }), 3000)
277
+ setTimeout(() => setModelSwitchMessage({ show: false }), 6000)
262
278
  }
263
279
  }, [onSubmitCountChange, messages])
264
280
 
@@ -525,6 +541,17 @@ function PromptInput({
525
541
  return false // Not handled, allow other hooks
526
542
  })
527
543
 
544
+ // Handle special key combinations before character input
545
+ const handleSpecialKey = useCallback((inputChar: string, key: any): boolean => {
546
+ // Shift+M for model switching - intercept before character input
547
+ if (key.shift && (inputChar === 'M' || inputChar === 'm')) {
548
+ handleQuickModelSwitch()
549
+ return true // Prevent character from being input
550
+ }
551
+
552
+ return false // Not handled, allow normal processing
553
+ }, [handleQuickModelSwitch])
554
+
528
555
  const textInputColumns = useTerminalSize().columns - 6
529
556
  const tokenUsage = useMemo(() => countTokens(messages), [messages])
530
557
 
@@ -614,14 +641,7 @@ function PromptInput({
614
641
  cursorOffset={cursorOffset}
615
642
  onChangeCursorOffset={setCursorOffset}
616
643
  onPaste={onTextPaste}
617
- onSpecialKey={(input, key) => {
618
- // Handle Shift+M for model switching
619
- if (key.shift && (input === 'M' || input === 'm')) {
620
- handleQuickModelSwitch()
621
- return true // Prevent the 'M' from being typed
622
- }
623
- return false
624
- }}
644
+ onSpecialKey={handleSpecialKey}
625
645
  />
626
646
  </Box>
627
647
  </Box>
@@ -20,6 +20,12 @@ export class SentryErrorBoundary extends React.Component<Props, State> {
20
20
  }
21
21
 
22
22
  componentDidCatch(error: Error): void {
23
+ // Don't report user-initiated cancellations to Sentry
24
+ if (error.name === 'AbortError' ||
25
+ error.message?.includes('abort') ||
26
+ error.message?.includes('The operation was aborted')) {
27
+ return
28
+ }
23
29
  captureException(error)
24
30
  }
25
31
 
@@ -1,11 +1,47 @@
1
1
  import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import type { TodoItem as TodoItemType } from '../utils/todoStorage'
2
4
 
3
5
  export interface TodoItemProps {
4
- // Define props as needed
6
+ todo: TodoItemType
5
7
  children?: React.ReactNode
6
8
  }
7
9
 
8
- export const TodoItem: React.FC<TodoItemProps> = ({ children }) => {
9
- // Minimal component implementation
10
- return <>{children}</>
10
+ export const TodoItem: React.FC<TodoItemProps> = ({ todo, children }) => {
11
+ const statusIconMap = {
12
+ completed: '✅',
13
+ in_progress: '🔄',
14
+ pending: '⏸️',
15
+ }
16
+
17
+ const statusColorMap = {
18
+ completed: '#008000',
19
+ in_progress: '#FFA500',
20
+ pending: '#FFD700',
21
+ }
22
+
23
+ const priorityIconMap = {
24
+ high: '🔴',
25
+ medium: '🟡',
26
+ low: '🟢',
27
+ }
28
+
29
+ const icon = statusIconMap[todo.status]
30
+ const color = statusColorMap[todo.status]
31
+ const priorityIcon = todo.priority ? priorityIconMap[todo.priority] : ''
32
+
33
+ return (
34
+ <Box flexDirection="row" gap={1}>
35
+ <Text color={color}>{icon}</Text>
36
+ {priorityIcon && <Text>{priorityIcon}</Text>}
37
+ <Text
38
+ color={color}
39
+ strikethrough={todo.status === 'completed'}
40
+ bold={todo.status === 'in_progress'}
41
+ >
42
+ {todo.content}
43
+ </Text>
44
+ {children}
45
+ </Box>
46
+ )
11
47
  }
@@ -960,7 +960,8 @@ export function useUnifiedCompletion({
960
960
 
961
961
  // Handle Tab key - simplified and unified
962
962
  useInput((input_str, key) => {
963
- if (!key.tab || key.shift) return false
963
+ if (!key.tab) return false
964
+ if (key.shift) return false
964
965
 
965
966
  const context = getWordAtCursor()
966
967
  if (!context) return false
@@ -171,22 +171,15 @@ export function REPL({
171
171
  }>({})
172
172
 
173
173
  const { status: apiKeyStatus, reverify } = useApiKeyVerification()
174
- // 🔧 FIXED: Simple cancellation logic matching original claude-code
175
174
  function onCancel() {
176
175
  if (!isLoading) {
177
176
  return
178
177
  }
179
178
  setIsLoading(false)
180
179
  if (toolUseConfirm) {
181
- // Tool use confirm handles the abort signal itself
182
180
  toolUseConfirm.onAbort()
183
- } else {
184
- // Wrap abort in try-catch to prevent error display on user interrupt
185
- try {
186
- abortController?.abort()
187
- } catch (e) {
188
- // Silently handle abort errors - this is expected behavior
189
- }
181
+ } else if (abortController && !abortController.signal.aborted) {
182
+ abortController.abort()
190
183
  }
191
184
  }
192
185
 
@@ -1395,9 +1395,9 @@ async function queryAnthropicNative(
1395
1395
  tools.map(async tool =>
1396
1396
  ({
1397
1397
  name: tool.name,
1398
- description: typeof tool.description === 'function'
1399
- ? await tool.description()
1400
- : tool.description,
1398
+ description: await tool.prompt({
1399
+ safeMode: options?.safeMode,
1400
+ }),
1401
1401
  input_schema: zodToJsonSchema(tool.inputSchema),
1402
1402
  }) as unknown as Anthropic.Beta.Messages.BetaTool,
1403
1403
  )
@@ -664,7 +664,7 @@ export async function getCompletionWithProfile(
664
664
  )
665
665
  }
666
666
 
667
- const stream = createStreamProcessor(response.body as any)
667
+ const stream = createStreamProcessor(response.body as any, signal)
668
668
  return stream
669
669
  }
670
670
 
@@ -815,6 +815,7 @@ export async function getCompletionWithProfile(
815
815
 
816
816
  export function createStreamProcessor(
817
817
  stream: any,
818
+ signal?: AbortSignal,
818
819
  ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
819
820
  if (!stream) {
820
821
  throw new Error('Stream is null or undefined')
@@ -827,10 +828,19 @@ export function createStreamProcessor(
827
828
 
828
829
  try {
829
830
  while (true) {
831
+ // Check for cancellation before attempting to read
832
+ if (signal?.aborted) {
833
+ break
834
+ }
835
+
830
836
  let readResult
831
837
  try {
832
838
  readResult = await reader.read()
833
839
  } catch (e) {
840
+ // If signal is aborted, this is user cancellation - exit silently
841
+ if (signal?.aborted) {
842
+ break
843
+ }
834
844
  console.error('Error reading from stream:', e)
835
845
  break
836
846
  }
@@ -899,8 +909,9 @@ export function createStreamProcessor(
899
909
 
900
910
  export function streamCompletion(
901
911
  stream: any,
912
+ signal?: AbortSignal,
902
913
  ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
903
- return createStreamProcessor(stream)
914
+ return createStreamProcessor(stream, signal)
904
915
  }
905
916
 
906
917
  /**
@@ -110,7 +110,7 @@ export const TodoWriteTool = {
110
110
  },
111
111
  inputSchema,
112
112
  userFacingName() {
113
- return 'Write Todos'
113
+ return 'Update Todos'
114
114
  },
115
115
  async isEnabled() {
116
116
  return true
@@ -129,9 +129,8 @@ export const TodoWriteTool = {
129
129
  return 'Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable'
130
130
  },
131
131
  renderToolUseMessage(input, { verbose }) {
132
- // Return empty string to match reference implementation and avoid double rendering
133
- // The tool result message will show the todo list
134
- return ''
132
+ // Show a simple confirmation message when the tool is being used
133
+ return '{ params.todo }'
135
134
  },
136
135
  renderToolUseRejectedMessage() {
137
136
  return <FallbackToolUseRejectedMessage />
@@ -139,12 +138,23 @@ export const TodoWriteTool = {
139
138
  renderToolResultMessage(output) {
140
139
  const isError = typeof output === 'string' && output.startsWith('Error')
141
140
 
142
- // If output contains todo data, render simple checkbox list
143
- if (typeof output === 'object' && output && 'newTodos' in output) {
144
- const { newTodos = [] } = output as any
141
+ // For non-error output, get current todos from storage and render them
142
+ if (!isError && typeof output === 'string') {
143
+ const currentTodos = getTodos()
144
+
145
+ if (currentTodos.length === 0) {
146
+ return (
147
+ <Box flexDirection="column" width="100%">
148
+ <Box flexDirection="row">
149
+ <Text color="#6B7280">&nbsp;&nbsp;⎿ &nbsp;</Text>
150
+ <Text color="#9CA3AF">No todos currently</Text>
151
+ </Box>
152
+ </Box>
153
+ )
154
+ }
145
155
 
146
- // sort: [completed, in_progress, pending]
147
- newTodos.sort((a, b) => {
156
+ // Sort: [completed, in_progress, pending]
157
+ const sortedTodos = [...currentTodos].sort((a, b) => {
148
158
  const order = ['completed', 'in_progress', 'pending']
149
159
  return (
150
160
  order.indexOf(a.status) - order.indexOf(b.status) ||
@@ -152,41 +162,52 @@ export const TodoWriteTool = {
152
162
  )
153
163
  })
154
164
 
155
- // Render each todo item with proper styling
165
+ // Find the next pending task (first pending task after sorting)
166
+ const nextPendingIndex = sortedTodos.findIndex(todo => todo.status === 'pending')
167
+
156
168
  return (
157
- <Box justifyContent="space-between" overflowX="hidden" width="100%">
158
- <Box flexDirection="row">
159
- <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
160
- <Box flexDirection="column">
161
- {newTodos.map((todo: TodoItem, index: number) => {
162
- const status_icon_map = {
163
- completed: '🟢',
164
- in_progress: '🟢',
165
- pending: '🟡',
166
- }
167
- const checkbox = status_icon_map[todo.status]
169
+ <Box flexDirection="column" width="100%">
170
+ {sortedTodos.map((todo: TodoItem, index: number) => {
171
+ // Determine checkbox symbol and colors
172
+ let checkbox: string
173
+ let textColor: string
174
+ let isBold = false
175
+ let isStrikethrough = false
168
176
 
169
- const status_color_map = {
170
- completed: '#008000',
171
- in_progress: '#008000',
172
- pending: '#FFD700',
173
- }
174
- const text_color = status_color_map[todo.status]
177
+ if (todo.status === 'completed') {
178
+ checkbox = ''
179
+ textColor = '#6B7280' // Professional gray for completed
180
+ isStrikethrough = true
181
+ } else if (todo.status === 'in_progress') {
182
+ checkbox = '☐'
183
+ textColor = '#10B981' // Professional green for in progress
184
+ isBold = true
185
+ } else if (todo.status === 'pending') {
186
+ checkbox = '☐'
187
+ // Only the FIRST pending task gets purple highlight
188
+ if (index === nextPendingIndex) {
189
+ textColor = '#8B5CF6' // Professional purple for next pending
190
+ isBold = true
191
+ } else {
192
+ textColor = '#9CA3AF' // Muted gray for other pending
193
+ }
194
+ }
175
195
 
176
- return (
177
- <React.Fragment key={todo.id || index}>
178
- <Text
179
- color={text_color}
180
- bold={todo.status !== 'pending'}
181
- strikethrough={todo.status === 'completed'}
182
- >
183
- {checkbox} {todo.content}
184
- </Text>
185
- </React.Fragment>
186
- )
187
- })}
188
- </Box>
189
- </Box>
196
+ return (
197
+ <Box key={todo.id || index} flexDirection="row" marginBottom={0}>
198
+ <Text color="#6B7280">&nbsp;&nbsp;⎿ &nbsp;</Text>
199
+ <Box flexDirection="row" flexGrow={1}>
200
+ <Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
201
+ {checkbox}
202
+ </Text>
203
+ <Text> </Text>
204
+ <Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
205
+ {todo.content}
206
+ </Text>
207
+ </Box>
208
+ </Box>
209
+ )
210
+ })}
190
211
  </Box>
191
212
  )
192
213
  }
@@ -264,8 +285,10 @@ export const TodoWriteTool = {
264
285
 
265
286
  yield {
266
287
  type: 'result',
267
- data: summary, // Return string instead of object to match interface
288
+ data: summary, // Return string to satisfy interface
268
289
  resultForAssistant: summary,
290
+ // Store todo data in a way accessible to the renderer
291
+ // We'll modify the renderToolResultMessage to get todos from storage
269
292
  }
270
293
  } catch (error) {
271
294
  const errorMessage =
@@ -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
  */