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
This commit is contained in:
parent
994579fadc
commit
6aa73a950a
@ -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
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。
|
||||
|
||||
> **⚠️ 安全提示**:Kode 默认以 YOLO 模式运行(等同于 Claude 的 `--dangerously-skip-permissions` 标志),跳过所有权限检查以获得最大生产力。建议仅在无互联网访问的可信环境中使用。对于安全敏感环境,请使用 `kode --safe` 启用权限检查。
|
||||
>
|
||||
> **📊 模型性能建议**:为获得最佳体验,建议使用专为自主任务完成设计的新一代强大模型。避免使用 GPT-4o、Gemini 2.5 Pro 等较老的问答型模型,它们主要针对回答问题进行优化,而非持续的独立任务执行。请选择专门训练用于智能体工作流和扩展推理能力的模型。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🤖 **AI 驱动的助手** - 使用先进的 AI 模型理解并响应你的请求
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -20,6 +20,12 @@ export class SentryErrorBoundary extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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<TodoItemProps> = ({ children }) => {
|
||||
// Minimal component implementation
|
||||
return <>{children}</>
|
||||
export const TodoItem: React.FC<TodoItemProps> = ({ 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 (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text color={color}>{icon}</Text>
|
||||
{priorityIcon && <Text>{priorityIcon}</Text>}
|
||||
<Text
|
||||
color={color}
|
||||
strikethrough={todo.status === 'completed'}
|
||||
bold={todo.status === 'in_progress'}
|
||||
>
|
||||
{todo.content}
|
||||
</Text>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<OpenAI.ChatCompletionChunk, void, unknown> {
|
||||
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<OpenAI.ChatCompletionChunk, void, unknown> {
|
||||
return createStreamProcessor(stream)
|
||||
return createStreamProcessor(stream, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 <FallbackToolUseRejectedMessage />
|
||||
@ -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 (
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box flexDirection="row">
|
||||
<Text color="#6B7280"> ⎿ </Text>
|
||||
<Text color="#9CA3AF">No todos currently</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Box justifyContent="space-between" overflowX="hidden" width="100%">
|
||||
<Box flexDirection="row">
|
||||
<Text> ⎿ </Text>
|
||||
<Box flexDirection="column">
|
||||
{newTodos.map((todo: TodoItem, index: number) => {
|
||||
const status_icon_map = {
|
||||
completed: '🟢',
|
||||
in_progress: '🟢',
|
||||
pending: '🟡',
|
||||
}
|
||||
const checkbox = status_icon_map[todo.status]
|
||||
<Box flexDirection="column" width="100%">
|
||||
{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 (
|
||||
<React.Fragment key={todo.id || index}>
|
||||
<Text
|
||||
color={text_color}
|
||||
bold={todo.status !== 'pending'}
|
||||
strikethrough={todo.status === 'completed'}
|
||||
>
|
||||
{checkbox} {todo.content}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
return (
|
||||
<Box key={todo.id || index} flexDirection="row" marginBottom={0}>
|
||||
<Text color="#6B7280"> ⎿ </Text>
|
||||
<Box flexDirection="row" flexGrow={1}>
|
||||
<Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
|
||||
{checkbox}
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
<Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
|
||||
{todo.content}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -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 =
|
||||
|
||||
@ -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<string, string | undefined>
|
||||
} {
|
||||
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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user