@shareai-lab/kode 1.0.73 → 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 +18 -0
- package/README.zh-CN.md +18 -0
- package/cli.js +5 -10
- package/package.json +2 -2
- package/src/components/PromptInput.tsx +37 -17
- package/src/components/SentryErrorBoundary.ts +6 -0
- package/src/components/TodoItem.tsx +40 -4
- package/src/hooks/useUnifiedCompletion.ts +2 -1
- package/src/screens/REPL.tsx +2 -9
- package/src/services/openai.ts +13 -2
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +65 -42
- package/src/utils/model.ts +120 -42
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
|
|
36
|
-
const
|
|
37
|
-
|
|
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 === '
|
|
52
|
-
console.error('\nError: tsx is required but not
|
|
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
|
@@ -227,20 +227,22 @@ function PromptInput({
|
|
|
227
227
|
[onModeChange, onInputChange],
|
|
228
228
|
)
|
|
229
229
|
|
|
230
|
-
// Handle
|
|
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}
|
|
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
|
-
//
|
|
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 }),
|
|
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={
|
|
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
|
-
|
|
6
|
+
todo: TodoItemType
|
|
5
7
|
children?: React.ReactNode
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
export const TodoItem: React.FC<TodoItemProps> = ({ children }) => {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
package/src/screens/REPL.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/services/openai.ts
CHANGED
|
@@ -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 '
|
|
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
|
-
//
|
|
133
|
-
|
|
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
|
-
//
|
|
143
|
-
if (typeof output === '
|
|
144
|
-
const
|
|
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"> ⎿ </Text>
|
|
150
|
+
<Text color="#9CA3AF">No todos currently</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
</Box>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
145
155
|
|
|
146
|
-
//
|
|
147
|
-
|
|
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
|
-
//
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
return (
|
|
197
|
+
<Box key={todo.id || index} flexDirection="row" marginBottom={0}>
|
|
198
|
+
<Text color="#6B7280"> ⎿ </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
|
|
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 =
|
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
|
*/
|