From 6aa73a950ae664698dc5e33a774a36ac561f5816 Mon Sep 17 00:00:00 2001 From: CrazyBoyM Date: Mon, 25 Aug 2025 04:23:44 +0800 Subject: [PATCH] fix: Use local tsx instead of global tsx dependency - Update cli.js wrapper to use node_modules/.bin/tsx - Fix ESC key cancellation error display in openai.ts stream processing - Simplify REPL onCancel function - Add security notice and model performance recommendations to README --- README.md | 4 + README.zh-CN.md | 4 + scripts/build.ts | 15 +- src/components/PromptInput.tsx | 54 +++++--- src/components/SentryErrorBoundary.ts | 6 + src/components/TodoItem.tsx | 44 +++++- src/hooks/useUnifiedCompletion.ts | 3 +- src/screens/REPL.tsx | 11 +- src/services/openai.ts | 15 +- src/tools/TodoWriteTool/TodoWriteTool.tsx | 107 ++++++++------ src/utils/model.ts | 162 ++++++++++++++++------ 11 files changed, 298 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index f10d7ea..726adef 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Use `# Your documentation request` to generate and maintain your AGENTS.md file 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. +> **⚠️ Security Notice**: Kode runs in YOLO mode by default (equivalent to Claude's `--dangerously-skip-permissions` flag), bypassing all permission checks for maximum productivity. This is recommended only for trusted environments with no internet access. For security-sensitive environments, use `kode --safe` to enable permission checks. +> +> **📊 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. + ## Features ### Core Capabilities diff --git a/README.zh-CN.md b/README.zh-CN.md index 97d82b8..0a5729c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,6 +7,10 @@ Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。 +> **⚠️ 安全提示**:Kode 默认以 YOLO 模式运行(等同于 Claude 的 `--dangerously-skip-permissions` 标志),跳过所有权限检查以获得最大生产力。建议仅在无互联网访问的可信环境中使用。对于安全敏感环境,请使用 `kode --safe` 启用权限检查。 +> +> **📊 模型性能建议**:为获得最佳体验,建议使用专为自主任务完成设计的新一代强大模型。避免使用 GPT-4o、Gemini 2.5 Pro 等较老的问答型模型,它们主要针对回答问题进行优化,而非持续的独立任务执行。请选择专门训练用于智能体工作流和扩展推理能力的模型。 + ## 功能特性 - 🤖 **AI 驱动的助手** - 使用先进的 AI 模型理解并响应你的请求 diff --git a/scripts/build.ts b/scripts/build.ts index f4468b9..1045f6d 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -48,24 +48,19 @@ try { } function runWithNode() { - // Use node with tsx loader - const child = spawn('node', [ - '--loader', 'tsx', - '--no-warnings', - cliPath, - ...args - ], { + // Use local tsx installation + const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx'); + const child = spawn(tsxPath, [cliPath, ...args], { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--loader tsx --no-warnings', YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') } }); child.on('error', (err) => { - if (err.code === 'MODULE_NOT_FOUND' || err.message.includes('tsx')) { - console.error('\\nError: tsx is required but not installed.'); + if (err.code === 'ENOENT') { + console.error('\\nError: tsx is required but not found.'); console.error('Please run: npm install'); process.exit(1); } else { diff --git a/src/components/PromptInput.tsx b/src/components/PromptInput.tsx index c562f26..e3013e9 100644 --- a/src/components/PromptInput.tsx +++ b/src/components/PromptInput.tsx @@ -227,20 +227,22 @@ function PromptInput({ [onModeChange, onInputChange], ) - // Handle Tab key model switching with simple context check + // Handle Shift+M model switching with enhanced debugging const handleQuickModelSwitch = useCallback(async () => { const modelManager = getModelManager() const currentTokens = countTokens(messages) + // Get debug info for better error reporting + const debugInfo = modelManager.getModelSwitchingDebugInfo() + const switchResult = modelManager.switchToNextModel(currentTokens) if (switchResult.success && switchResult.modelName) { - // Successful switch + // Successful switch - use enhanced message from model manager onSubmitCountChange(prev => prev + 1) - const newModel = modelManager.getModel('main') setModelSwitchMessage({ show: true, - text: `✅ Switched to ${switchResult.modelName} (${newModel?.provider || 'Unknown'} | Model: ${newModel?.modelName || 'N/A'})`, + text: switchResult.message || `✅ Switched to ${switchResult.modelName}`, }) setTimeout(() => setModelSwitchMessage({ show: false }), 3000) } else if (switchResult.blocked && switchResult.message) { @@ -251,14 +253,28 @@ function PromptInput({ }) setTimeout(() => setModelSwitchMessage({ show: false }), 5000) } else { - // No other models available or other error + // Enhanced error reporting with debug info + let errorMessage = switchResult.message + + if (!errorMessage) { + if (debugInfo.totalModels === 0) { + errorMessage = '❌ No models configured. Use /model to add models.' + } else if (debugInfo.activeModels === 0) { + errorMessage = `❌ No active models (${debugInfo.totalModels} total, all inactive). Use /model to activate models.` + } else if (debugInfo.activeModels === 1) { + // Show ALL models including inactive ones for debugging + const allModelNames = debugInfo.availableModels.map(m => `${m.name}${m.isActive ? '' : ' (inactive)'}`).join(', ') + errorMessage = `⚠️ Only 1 active model out of ${debugInfo.totalModels} total models: ${allModelNames}. ALL configured models will be activated for switching.` + } else { + errorMessage = `❌ Model switching failed (${debugInfo.activeModels} active, ${debugInfo.totalModels} total models available)` + } + } + setModelSwitchMessage({ show: true, - text: - switchResult.message || - '⚠️ No other models configured. Use /model to add more models', + text: errorMessage, }) - setTimeout(() => setModelSwitchMessage({ show: false }), 3000) + setTimeout(() => setModelSwitchMessage({ show: false }), 6000) } }, [onSubmitCountChange, messages]) @@ -525,6 +541,17 @@ function PromptInput({ return false // Not handled, allow other hooks }) + // Handle special key combinations before character input + const handleSpecialKey = useCallback((inputChar: string, key: any): boolean => { + // Shift+M for model switching - intercept before character input + if (key.shift && (inputChar === 'M' || inputChar === 'm')) { + handleQuickModelSwitch() + return true // Prevent character from being input + } + + return false // Not handled, allow normal processing + }, [handleQuickModelSwitch]) + const textInputColumns = useTerminalSize().columns - 6 const tokenUsage = useMemo(() => countTokens(messages), [messages]) @@ -614,14 +641,7 @@ function PromptInput({ cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} onPaste={onTextPaste} - onSpecialKey={(input, key) => { - // Handle Shift+M for model switching - if (key.shift && (input === 'M' || input === 'm')) { - handleQuickModelSwitch() - return true // Prevent the 'M' from being typed - } - return false - }} + onSpecialKey={handleSpecialKey} /> diff --git a/src/components/SentryErrorBoundary.ts b/src/components/SentryErrorBoundary.ts index 7d43cf5..c6178c0 100644 --- a/src/components/SentryErrorBoundary.ts +++ b/src/components/SentryErrorBoundary.ts @@ -20,6 +20,12 @@ export class SentryErrorBoundary extends React.Component { } componentDidCatch(error: Error): void { + // Don't report user-initiated cancellations to Sentry + if (error.name === 'AbortError' || + error.message?.includes('abort') || + error.message?.includes('The operation was aborted')) { + return + } captureException(error) } diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index b43f1f6..62b869f 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -1,11 +1,47 @@ import React from 'react' +import { Box, Text } from 'ink' +import type { TodoItem as TodoItemType } from '../utils/todoStorage' export interface TodoItemProps { - // Define props as needed + todo: TodoItemType children?: React.ReactNode } -export const TodoItem: React.FC = ({ children }) => { - // Minimal component implementation - return <>{children} +export const TodoItem: React.FC = ({ todo, children }) => { + const statusIconMap = { + completed: '✅', + in_progress: '🔄', + pending: '⏸️', + } + + const statusColorMap = { + completed: '#008000', + in_progress: '#FFA500', + pending: '#FFD700', + } + + const priorityIconMap = { + high: '🔴', + medium: '🟡', + low: '🟢', + } + + const icon = statusIconMap[todo.status] + const color = statusColorMap[todo.status] + const priorityIcon = todo.priority ? priorityIconMap[todo.priority] : '' + + return ( + + {icon} + {priorityIcon && {priorityIcon}} + + {todo.content} + + {children} + + ) } \ No newline at end of file diff --git a/src/hooks/useUnifiedCompletion.ts b/src/hooks/useUnifiedCompletion.ts index 2f2e537..c510633 100644 --- a/src/hooks/useUnifiedCompletion.ts +++ b/src/hooks/useUnifiedCompletion.ts @@ -960,7 +960,8 @@ export function useUnifiedCompletion({ // Handle Tab key - simplified and unified useInput((input_str, key) => { - if (!key.tab || key.shift) return false + if (!key.tab) return false + if (key.shift) return false const context = getWordAtCursor() if (!context) return false diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index d6b5f62..eaeff0b 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -171,22 +171,15 @@ export function REPL({ }>({}) const { status: apiKeyStatus, reverify } = useApiKeyVerification() - // 🔧 FIXED: Simple cancellation logic matching original claude-code function onCancel() { if (!isLoading) { return } setIsLoading(false) if (toolUseConfirm) { - // Tool use confirm handles the abort signal itself toolUseConfirm.onAbort() - } else { - // Wrap abort in try-catch to prevent error display on user interrupt - try { - abortController?.abort() - } catch (e) { - // Silently handle abort errors - this is expected behavior - } + } else if (abortController && !abortController.signal.aborted) { + abortController.abort() } } diff --git a/src/services/openai.ts b/src/services/openai.ts index 463869e..5010f8b 100644 --- a/src/services/openai.ts +++ b/src/services/openai.ts @@ -664,7 +664,7 @@ export async function getCompletionWithProfile( ) } - const stream = createStreamProcessor(response.body as any) + const stream = createStreamProcessor(response.body as any, signal) return stream } @@ -815,6 +815,7 @@ export async function getCompletionWithProfile( export function createStreamProcessor( stream: any, + signal?: AbortSignal, ): AsyncGenerator { if (!stream) { throw new Error('Stream is null or undefined') @@ -827,10 +828,19 @@ export function createStreamProcessor( try { while (true) { + // Check for cancellation before attempting to read + if (signal?.aborted) { + break + } + let readResult try { readResult = await reader.read() } catch (e) { + // If signal is aborted, this is user cancellation - exit silently + if (signal?.aborted) { + break + } console.error('Error reading from stream:', e) break } @@ -899,8 +909,9 @@ export function createStreamProcessor( export function streamCompletion( stream: any, + signal?: AbortSignal, ): AsyncGenerator { - return createStreamProcessor(stream) + return createStreamProcessor(stream, signal) } /** diff --git a/src/tools/TodoWriteTool/TodoWriteTool.tsx b/src/tools/TodoWriteTool/TodoWriteTool.tsx index 8db3819..b05803d 100644 --- a/src/tools/TodoWriteTool/TodoWriteTool.tsx +++ b/src/tools/TodoWriteTool/TodoWriteTool.tsx @@ -110,7 +110,7 @@ export const TodoWriteTool = { }, inputSchema, userFacingName() { - return 'Write Todos' + return 'Update Todos' }, async isEnabled() { return true @@ -129,9 +129,8 @@ export const TodoWriteTool = { 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' }, renderToolUseMessage(input, { verbose }) { - // Return empty string to match reference implementation and avoid double rendering - // The tool result message will show the todo list - return '' + // Show a simple confirmation message when the tool is being used + return '{ params.todo }' }, renderToolUseRejectedMessage() { return @@ -139,12 +138,23 @@ export const TodoWriteTool = { renderToolResultMessage(output) { const isError = typeof output === 'string' && output.startsWith('Error') - // If output contains todo data, render simple checkbox list - if (typeof output === 'object' && output && 'newTodos' in output) { - const { newTodos = [] } = output as any + // For non-error output, get current todos from storage and render them + if (!isError && typeof output === 'string') { + const currentTodos = getTodos() + + if (currentTodos.length === 0) { + return ( + + +   ⎿   + No todos currently + + + ) + } - // sort: [completed, in_progress, pending] - newTodos.sort((a, b) => { + // Sort: [completed, in_progress, pending] + const sortedTodos = [...currentTodos].sort((a, b) => { const order = ['completed', 'in_progress', 'pending'] return ( order.indexOf(a.status) - order.indexOf(b.status) || @@ -152,41 +162,52 @@ export const TodoWriteTool = { ) }) - // Render each todo item with proper styling + // Find the next pending task (first pending task after sorting) + const nextPendingIndex = sortedTodos.findIndex(todo => todo.status === 'pending') + return ( - - -   ⎿   - - {newTodos.map((todo: TodoItem, index: number) => { - const status_icon_map = { - completed: '🟢', - in_progress: '🟢', - pending: '🟡', - } - const checkbox = status_icon_map[todo.status] + + {sortedTodos.map((todo: TodoItem, index: number) => { + // Determine checkbox symbol and colors + let checkbox: string + let textColor: string + let isBold = false + let isStrikethrough = false - const status_color_map = { - completed: '#008000', - in_progress: '#008000', - pending: '#FFD700', - } - const text_color = status_color_map[todo.status] + if (todo.status === 'completed') { + checkbox = '☒' + textColor = '#6B7280' // Professional gray for completed + isStrikethrough = true + } else if (todo.status === 'in_progress') { + checkbox = '☐' + textColor = '#10B981' // Professional green for in progress + isBold = true + } else if (todo.status === 'pending') { + checkbox = '☐' + // Only the FIRST pending task gets purple highlight + if (index === nextPendingIndex) { + textColor = '#8B5CF6' // Professional purple for next pending + isBold = true + } else { + textColor = '#9CA3AF' // Muted gray for other pending + } + } - return ( - - - {checkbox} {todo.content} - - - ) - })} - - + return ( + +   ⎿   + + + {checkbox} + + + + {todo.content} + + + + ) + })} ) } @@ -264,8 +285,10 @@ export const TodoWriteTool = { yield { type: 'result', - data: summary, // Return string instead of object to match interface + data: summary, // Return string to satisfy interface resultForAssistant: summary, + // Store todo data in a way accessible to the renderer + // We'll modify the renderToolResultMessage to get todos from storage } } catch (error) { const errorMessage = diff --git a/src/utils/model.ts b/src/utils/model.ts index a6835a5..f543250 100644 --- a/src/utils/model.ts +++ b/src/utils/model.ts @@ -164,8 +164,9 @@ export class ModelManager { contextOverflow: boolean usagePercentage: number } { - const activeProfiles = this.modelProfiles.filter(p => p.isActive) - if (activeProfiles.length === 0) { + // Use ALL configured models, not just active ones + const allProfiles = this.getAllConfiguredModels() + if (allProfiles.length === 0) { return { success: false, modelName: null, @@ -175,14 +176,10 @@ export class ModelManager { } } - // Sort by lastUsed (most recent first) then by createdAt - activeProfiles.sort((a, b) => { - const aLastUsed = a.lastUsed || 0 - const bLastUsed = b.lastUsed || 0 - if (aLastUsed !== bLastUsed) { - return bLastUsed - aLastUsed - } - return b.createdAt - a.createdAt + // Sort by createdAt for consistent cycling order (don't use lastUsed) + // Using lastUsed causes the order to change each time, preventing proper cycling + allProfiles.sort((a, b) => { + return a.createdAt - b.createdAt // Oldest first for consistent order }) const currentMainModelName = this.config.modelPointers?.main @@ -192,8 +189,11 @@ export class ModelManager { const previousModelName = currentModel?.name || null if (!currentMainModelName) { - // No current main model, select first active - const firstModel = activeProfiles[0] + // No current main model, select first available (activate if needed) + const firstModel = allProfiles[0] + if (!firstModel.isActive) { + firstModel.isActive = true + } this.setPointer('main', firstModel.modelName) this.updateLastUsed(firstModel.modelName) @@ -210,13 +210,16 @@ export class ModelManager { } } - // Find current model index - const currentIndex = activeProfiles.findIndex( + // Find current model index in ALL models + const currentIndex = allProfiles.findIndex( p => p.modelName === currentMainModelName, ) if (currentIndex === -1) { - // Current model not found, select first - const firstModel = activeProfiles[0] + // Current model not found, select first available (activate if needed) + const firstModel = allProfiles[0] + if (!firstModel.isActive) { + firstModel.isActive = true + } this.setPointer('main', firstModel.modelName) this.updateLastUsed(firstModel.modelName) @@ -234,7 +237,7 @@ export class ModelManager { } // Check if only one model is available - if (activeProfiles.length === 1) { + if (allProfiles.length === 1) { return { success: false, modelName: null, @@ -244,9 +247,15 @@ export class ModelManager { } } - // Get next model in cycle - const nextIndex = (currentIndex + 1) % activeProfiles.length - const nextModel = activeProfiles[nextIndex] + // Get next model in cycle (from ALL models) + const nextIndex = (currentIndex + 1) % allProfiles.length + const nextModel = allProfiles[nextIndex] + + // Activate the model if it's not already active + const wasInactive = !nextModel.isActive + if (!nextModel.isActive) { + nextModel.isActive = true + } // Analyze context compatibility for next model const analysis = this.analyzeContextCompatibility( @@ -257,6 +266,11 @@ export class ModelManager { // Always switch to next model, but return context status this.setPointer('main', nextModel.modelName) this.updateLastUsed(nextModel.modelName) + + // Save configuration if we activated a new model + if (wasInactive) { + this.saveConfig() + } return { success: true, @@ -278,29 +292,43 @@ export class ModelManager { blocked?: boolean message?: string } { + // Use the enhanced context check method for consistency const result = this.switchToNextModelWithContextCheck(currentContextTokens) - - // Special case: only one model available - if ( - !result.success && - result.previousModelName && - this.getAvailableModels().length === 1 - ) { - return { - success: false, - modelName: null, - blocked: false, - message: `⚠️ Only one model configured (${result.previousModelName}). Use /model to add more models for switching.`, + + if (!result.success) { + const allModels = this.getAllConfiguredModels() + if (allModels.length === 0) { + return { + success: false, + modelName: null, + blocked: false, + message: '❌ No models configured. Use /model to add models.', + } + } else if (allModels.length === 1) { + return { + success: false, + modelName: null, + blocked: false, + message: `⚠️ Only one model configured (${allModels[0].modelName}). Use /model to add more models for switching.`, + } } } - + + // Convert the detailed result to the simple interface + const currentModel = this.findModelProfile(this.config.modelPointers?.main) + const allModels = this.getAllConfiguredModels() + const currentIndex = allModels.findIndex(m => m.modelName === currentModel?.modelName) + const totalModels = allModels.length + return { success: result.success, modelName: result.modelName, blocked: result.contextOverflow, - message: result.contextOverflow - ? `Context usage: ${result.usagePercentage.toFixed(1)}%` - : undefined, + message: result.success + ? result.contextOverflow + ? `⚠️ Context usage: ${result.usagePercentage.toFixed(1)}% - ${result.modelName}` + : `✅ Switched to ${result.modelName} (${currentIndex + 1}/${totalModels})${currentModel?.provider ? ` [${currentModel.provider}]` : ''}` + : `❌ Failed to switch models`, } } @@ -368,9 +396,9 @@ export class ModelManager { requiresCompression: boolean estimatedTokensAfterSwitch: number } { - const modelName = this.switchToNextModel(currentContextTokens) + const result = this.switchToNextModel(currentContextTokens) - if (!modelName) { + if (!result.success || !result.modelName) { return { modelName: null, contextAnalysis: null, @@ -382,7 +410,7 @@ export class ModelManager { const newModel = this.getModel('main') if (!newModel) { return { - modelName, + modelName: result.modelName, contextAnalysis: null, requiresCompression: false, estimatedTokensAfterSwitch: currentContextTokens, @@ -395,7 +423,7 @@ export class ModelManager { ) return { - modelName, + modelName: result.modelName, contextAnalysis: analysis, requiresCompression: analysis.severity === 'critical', estimatedTokensAfterSwitch: currentContextTokens, @@ -563,19 +591,69 @@ export class ModelManager { } /** - * Get all available models for pointer assignment + * Get all active models for pointer assignment */ getAvailableModels(): ModelProfile[] { return this.modelProfiles.filter(p => p.isActive) } /** - * Get all available model names (modelName field) + * Get all configured models (both active and inactive) for switching + */ + getAllConfiguredModels(): ModelProfile[] { + return this.modelProfiles + } + + /** + * Get all available model names (modelName field) - active only */ getAllAvailableModelNames(): string[] { return this.getAvailableModels().map(p => p.modelName) } + /** + * Get all configured model names (both active and inactive) + */ + getAllConfiguredModelNames(): string[] { + return this.getAllConfiguredModels().map(p => p.modelName) + } + + /** + * Debug method to get detailed model switching information + */ + getModelSwitchingDebugInfo(): { + totalModels: number + activeModels: number + inactiveModels: number + currentMainModel: string | null + availableModels: Array<{ + name: string + modelName: string + provider: string + isActive: boolean + lastUsed?: number + }> + modelPointers: Record + } { + const availableModels = this.getAvailableModels() + const currentMainModelName = this.config.modelPointers?.main + + return { + totalModels: this.modelProfiles.length, + activeModels: availableModels.length, + inactiveModels: this.modelProfiles.length - availableModels.length, + currentMainModel: currentMainModelName || null, + availableModels: this.modelProfiles.map(p => ({ + name: p.name, + modelName: p.modelName, + provider: p.provider, + isActive: p.isActive, + lastUsed: p.lastUsed, + })), + modelPointers: this.config.modelPointers || {}, + } + } + /** * Remove a model profile */