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:
CrazyBoyM 2025-08-25 04:23:44 +08:00
parent 994579fadc
commit 6aa73a950a
11 changed files with 298 additions and 127 deletions

View File

@ -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. 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 ## Features
### Core Capabilities ### Core Capabilities

View File

@ -7,6 +7,10 @@
Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。 Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。
> **⚠️ 安全提示**Kode 默认以 YOLO 模式运行(等同于 Claude 的 `--dangerously-skip-permissions` 标志),跳过所有权限检查以获得最大生产力。建议仅在无互联网访问的可信环境中使用。对于安全敏感环境,请使用 `kode --safe` 启用权限检查。
>
> **📊 模型性能建议**:为获得最佳体验,建议使用专为自主任务完成设计的新一代强大模型。避免使用 GPT-4o、Gemini 2.5 Pro 等较老的问答型模型,它们主要针对回答问题进行优化,而非持续的独立任务执行。请选择专门训练用于智能体工作流和扩展推理能力的模型。
## 功能特性 ## 功能特性
- 🤖 **AI 驱动的助手** - 使用先进的 AI 模型理解并响应你的请求 - 🤖 **AI 驱动的助手** - 使用先进的 AI 模型理解并响应你的请求

View File

@ -48,24 +48,19 @@ try {
} }
function runWithNode() { function runWithNode() {
// Use node with tsx loader // Use local tsx installation
const child = spawn('node', [ const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
'--loader', 'tsx', const child = spawn(tsxPath, [cliPath, ...args], {
'--no-warnings',
cliPath,
...args
], {
stdio: 'inherit', stdio: 'inherit',
env: { env: {
...process.env, ...process.env,
NODE_OPTIONS: '--loader tsx --no-warnings',
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
} }
}); });
child.on('error', (err) => { child.on('error', (err) => {
if (err.code === 'MODULE_NOT_FOUND' || err.message.includes('tsx')) { if (err.code === 'ENOENT') {
console.error('\\nError: tsx is required but not installed.'); console.error('\\nError: tsx is required but not found.');
console.error('Please run: npm install'); console.error('Please run: npm install');
process.exit(1); process.exit(1);
} else { } else {

View File

@ -227,20 +227,22 @@ function PromptInput({
[onModeChange, onInputChange], [onModeChange, onInputChange],
) )
// Handle Tab key model switching with simple context check // Handle Shift+M model switching with enhanced debugging
const handleQuickModelSwitch = useCallback(async () => { const handleQuickModelSwitch = useCallback(async () => {
const modelManager = getModelManager() const modelManager = getModelManager()
const currentTokens = countTokens(messages) const currentTokens = countTokens(messages)
// Get debug info for better error reporting
const debugInfo = modelManager.getModelSwitchingDebugInfo()
const switchResult = modelManager.switchToNextModel(currentTokens) const switchResult = modelManager.switchToNextModel(currentTokens)
if (switchResult.success && switchResult.modelName) { if (switchResult.success && switchResult.modelName) {
// Successful switch // Successful switch - use enhanced message from model manager
onSubmitCountChange(prev => prev + 1) onSubmitCountChange(prev => prev + 1)
const newModel = modelManager.getModel('main')
setModelSwitchMessage({ setModelSwitchMessage({
show: true, 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) setTimeout(() => setModelSwitchMessage({ show: false }), 3000)
} else if (switchResult.blocked && switchResult.message) { } else if (switchResult.blocked && switchResult.message) {
@ -251,14 +253,28 @@ function PromptInput({
}) })
setTimeout(() => setModelSwitchMessage({ show: false }), 5000) setTimeout(() => setModelSwitchMessage({ show: false }), 5000)
} else { } 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({ setModelSwitchMessage({
show: true, show: true,
text: text: errorMessage,
switchResult.message ||
'⚠️ No other models configured. Use /model to add more models',
}) })
setTimeout(() => setModelSwitchMessage({ show: false }), 3000) setTimeout(() => setModelSwitchMessage({ show: false }), 6000)
} }
}, [onSubmitCountChange, messages]) }, [onSubmitCountChange, messages])
@ -525,6 +541,17 @@ function PromptInput({
return false // Not handled, allow other hooks 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 textInputColumns = useTerminalSize().columns - 6
const tokenUsage = useMemo(() => countTokens(messages), [messages]) const tokenUsage = useMemo(() => countTokens(messages), [messages])
@ -614,14 +641,7 @@ function PromptInput({
cursorOffset={cursorOffset} cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset} onChangeCursorOffset={setCursorOffset}
onPaste={onTextPaste} onPaste={onTextPaste}
onSpecialKey={(input, key) => { onSpecialKey={handleSpecialKey}
// Handle Shift+M for model switching
if (key.shift && (input === 'M' || input === 'm')) {
handleQuickModelSwitch()
return true // Prevent the 'M' from being typed
}
return false
}}
/> />
</Box> </Box>
</Box> </Box>

View File

@ -20,6 +20,12 @@ export class SentryErrorBoundary extends React.Component<Props, State> {
} }
componentDidCatch(error: Error): void { 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) captureException(error)
} }

View File

@ -1,11 +1,47 @@
import React from 'react' import React from 'react'
import { Box, Text } from 'ink'
import type { TodoItem as TodoItemType } from '../utils/todoStorage'
export interface TodoItemProps { export interface TodoItemProps {
// Define props as needed todo: TodoItemType
children?: React.ReactNode children?: React.ReactNode
} }
export const TodoItem: React.FC<TodoItemProps> = ({ children }) => { export const TodoItem: React.FC<TodoItemProps> = ({ todo, children }) => {
// Minimal component implementation const statusIconMap = {
return <>{children}</> 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>
)
} }

View File

@ -960,7 +960,8 @@ export function useUnifiedCompletion({
// Handle Tab key - simplified and unified // Handle Tab key - simplified and unified
useInput((input_str, key) => { useInput((input_str, key) => {
if (!key.tab || key.shift) return false if (!key.tab) return false
if (key.shift) return false
const context = getWordAtCursor() const context = getWordAtCursor()
if (!context) return false if (!context) return false

View File

@ -171,22 +171,15 @@ export function REPL({
}>({}) }>({})
const { status: apiKeyStatus, reverify } = useApiKeyVerification() const { status: apiKeyStatus, reverify } = useApiKeyVerification()
// 🔧 FIXED: Simple cancellation logic matching original claude-code
function onCancel() { function onCancel() {
if (!isLoading) { if (!isLoading) {
return return
} }
setIsLoading(false) setIsLoading(false)
if (toolUseConfirm) { if (toolUseConfirm) {
// Tool use confirm handles the abort signal itself
toolUseConfirm.onAbort() toolUseConfirm.onAbort()
} else { } else if (abortController && !abortController.signal.aborted) {
// Wrap abort in try-catch to prevent error display on user interrupt abortController.abort()
try {
abortController?.abort()
} catch (e) {
// Silently handle abort errors - this is expected behavior
}
} }
} }

View File

@ -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 return stream
} }
@ -815,6 +815,7 @@ export async function getCompletionWithProfile(
export function createStreamProcessor( export function createStreamProcessor(
stream: any, stream: any,
signal?: AbortSignal,
): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> { ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
if (!stream) { if (!stream) {
throw new Error('Stream is null or undefined') throw new Error('Stream is null or undefined')
@ -827,10 +828,19 @@ export function createStreamProcessor(
try { try {
while (true) { while (true) {
// Check for cancellation before attempting to read
if (signal?.aborted) {
break
}
let readResult let readResult
try { try {
readResult = await reader.read() readResult = await reader.read()
} catch (e) { } catch (e) {
// If signal is aborted, this is user cancellation - exit silently
if (signal?.aborted) {
break
}
console.error('Error reading from stream:', e) console.error('Error reading from stream:', e)
break break
} }
@ -899,8 +909,9 @@ export function createStreamProcessor(
export function streamCompletion( export function streamCompletion(
stream: any, stream: any,
signal?: AbortSignal,
): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> { ): AsyncGenerator<OpenAI.ChatCompletionChunk, void, unknown> {
return createStreamProcessor(stream) return createStreamProcessor(stream, signal)
} }
/** /**

View File

@ -110,7 +110,7 @@ export const TodoWriteTool = {
}, },
inputSchema, inputSchema,
userFacingName() { userFacingName() {
return 'Write Todos' return 'Update Todos'
}, },
async isEnabled() { async isEnabled() {
return true 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' 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 }) { renderToolUseMessage(input, { verbose }) {
// Return empty string to match reference implementation and avoid double rendering // Show a simple confirmation message when the tool is being used
// The tool result message will show the todo list return '{ params.todo }'
return ''
}, },
renderToolUseRejectedMessage() { renderToolUseRejectedMessage() {
return <FallbackToolUseRejectedMessage /> return <FallbackToolUseRejectedMessage />
@ -139,12 +138,23 @@ export const TodoWriteTool = {
renderToolResultMessage(output) { renderToolResultMessage(output) {
const isError = typeof output === 'string' && output.startsWith('Error') const isError = typeof output === 'string' && output.startsWith('Error')
// If output contains todo data, render simple checkbox list // For non-error output, get current todos from storage and render them
if (typeof output === 'object' && output && 'newTodos' in output) { if (!isError && typeof output === 'string') {
const { newTodos = [] } = output as any const currentTodos = getTodos()
// sort: [completed, in_progress, pending] if (currentTodos.length === 0) {
newTodos.sort((a, b) => { return (
<Box flexDirection="column" width="100%">
<Box flexDirection="row">
<Text color="#6B7280">&nbsp;&nbsp; &nbsp;</Text>
<Text color="#9CA3AF">No todos currently</Text>
</Box>
</Box>
)
}
// Sort: [completed, in_progress, pending]
const sortedTodos = [...currentTodos].sort((a, b) => {
const order = ['completed', 'in_progress', 'pending'] const order = ['completed', 'in_progress', 'pending']
return ( return (
order.indexOf(a.status) - order.indexOf(b.status) || 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 ( return (
<Box justifyContent="space-between" overflowX="hidden" width="100%"> <Box flexDirection="column" width="100%">
<Box flexDirection="row"> {sortedTodos.map((todo: TodoItem, index: number) => {
<Text>&nbsp;&nbsp; &nbsp;</Text> // Determine checkbox symbol and colors
<Box flexDirection="column"> let checkbox: string
{newTodos.map((todo: TodoItem, index: number) => { let textColor: string
const status_icon_map = { let isBold = false
completed: '🟢', let isStrikethrough = false
in_progress: '🟢',
pending: '🟡',
}
const checkbox = status_icon_map[todo.status]
const status_color_map = { if (todo.status === 'completed') {
completed: '#008000', checkbox = '☒'
in_progress: '#008000', textColor = '#6B7280' // Professional gray for completed
pending: '#FFD700', isStrikethrough = true
} } else if (todo.status === 'in_progress') {
const text_color = status_color_map[todo.status] 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 ( return (
<React.Fragment key={todo.id || index}> <Box key={todo.id || index} flexDirection="row" marginBottom={0}>
<Text <Text color="#6B7280">&nbsp;&nbsp; &nbsp;</Text>
color={text_color} <Box flexDirection="row" flexGrow={1}>
bold={todo.status !== 'pending'} <Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
strikethrough={todo.status === 'completed'} {checkbox}
> </Text>
{checkbox} {todo.content} <Text> </Text>
</Text> <Text color={textColor} bold={isBold} strikethrough={isStrikethrough}>
</React.Fragment> {todo.content}
) </Text>
})} </Box>
</Box> </Box>
</Box> )
})}
</Box> </Box>
) )
} }
@ -264,8 +285,10 @@ export const TodoWriteTool = {
yield { yield {
type: 'result', type: 'result',
data: summary, // Return string instead of object to match interface data: summary, // Return string to satisfy interface
resultForAssistant: summary, resultForAssistant: summary,
// Store todo data in a way accessible to the renderer
// We'll modify the renderToolResultMessage to get todos from storage
} }
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =

View File

@ -164,8 +164,9 @@ export class ModelManager {
contextOverflow: boolean contextOverflow: boolean
usagePercentage: number usagePercentage: number
} { } {
const activeProfiles = this.modelProfiles.filter(p => p.isActive) // Use ALL configured models, not just active ones
if (activeProfiles.length === 0) { const allProfiles = this.getAllConfiguredModels()
if (allProfiles.length === 0) {
return { return {
success: false, success: false,
modelName: null, modelName: null,
@ -175,14 +176,10 @@ export class ModelManager {
} }
} }
// Sort by lastUsed (most recent first) then by createdAt // Sort by createdAt for consistent cycling order (don't use lastUsed)
activeProfiles.sort((a, b) => { // Using lastUsed causes the order to change each time, preventing proper cycling
const aLastUsed = a.lastUsed || 0 allProfiles.sort((a, b) => {
const bLastUsed = b.lastUsed || 0 return a.createdAt - b.createdAt // Oldest first for consistent order
if (aLastUsed !== bLastUsed) {
return bLastUsed - aLastUsed
}
return b.createdAt - a.createdAt
}) })
const currentMainModelName = this.config.modelPointers?.main const currentMainModelName = this.config.modelPointers?.main
@ -192,8 +189,11 @@ export class ModelManager {
const previousModelName = currentModel?.name || null const previousModelName = currentModel?.name || null
if (!currentMainModelName) { if (!currentMainModelName) {
// No current main model, select first active // No current main model, select first available (activate if needed)
const firstModel = activeProfiles[0] const firstModel = allProfiles[0]
if (!firstModel.isActive) {
firstModel.isActive = true
}
this.setPointer('main', firstModel.modelName) this.setPointer('main', firstModel.modelName)
this.updateLastUsed(firstModel.modelName) this.updateLastUsed(firstModel.modelName)
@ -210,13 +210,16 @@ export class ModelManager {
} }
} }
// Find current model index // Find current model index in ALL models
const currentIndex = activeProfiles.findIndex( const currentIndex = allProfiles.findIndex(
p => p.modelName === currentMainModelName, p => p.modelName === currentMainModelName,
) )
if (currentIndex === -1) { if (currentIndex === -1) {
// Current model not found, select first // Current model not found, select first available (activate if needed)
const firstModel = activeProfiles[0] const firstModel = allProfiles[0]
if (!firstModel.isActive) {
firstModel.isActive = true
}
this.setPointer('main', firstModel.modelName) this.setPointer('main', firstModel.modelName)
this.updateLastUsed(firstModel.modelName) this.updateLastUsed(firstModel.modelName)
@ -234,7 +237,7 @@ export class ModelManager {
} }
// Check if only one model is available // Check if only one model is available
if (activeProfiles.length === 1) { if (allProfiles.length === 1) {
return { return {
success: false, success: false,
modelName: null, modelName: null,
@ -244,9 +247,15 @@ export class ModelManager {
} }
} }
// Get next model in cycle // Get next model in cycle (from ALL models)
const nextIndex = (currentIndex + 1) % activeProfiles.length const nextIndex = (currentIndex + 1) % allProfiles.length
const nextModel = activeProfiles[nextIndex] 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 // Analyze context compatibility for next model
const analysis = this.analyzeContextCompatibility( const analysis = this.analyzeContextCompatibility(
@ -258,6 +267,11 @@ export class ModelManager {
this.setPointer('main', nextModel.modelName) this.setPointer('main', nextModel.modelName)
this.updateLastUsed(nextModel.modelName) this.updateLastUsed(nextModel.modelName)
// Save configuration if we activated a new model
if (wasInactive) {
this.saveConfig()
}
return { return {
success: true, success: true,
modelName: nextModel.name, modelName: nextModel.name,
@ -278,29 +292,43 @@ export class ModelManager {
blocked?: boolean blocked?: boolean
message?: string message?: string
} { } {
// Use the enhanced context check method for consistency
const result = this.switchToNextModelWithContextCheck(currentContextTokens) const result = this.switchToNextModelWithContextCheck(currentContextTokens)
// Special case: only one model available if (!result.success) {
if ( const allModels = this.getAllConfiguredModels()
!result.success && if (allModels.length === 0) {
result.previousModelName && return {
this.getAvailableModels().length === 1 success: false,
) { modelName: null,
return { blocked: false,
success: false, message: '❌ No models configured. Use /model to add models.',
modelName: null, }
blocked: false, } else if (allModels.length === 1) {
message: `⚠️ Only one model configured (${result.previousModelName}). Use /model to add more models for switching.`, 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 { return {
success: result.success, success: result.success,
modelName: result.modelName, modelName: result.modelName,
blocked: result.contextOverflow, blocked: result.contextOverflow,
message: result.contextOverflow message: result.success
? `Context usage: ${result.usagePercentage.toFixed(1)}%` ? result.contextOverflow
: undefined, ? `⚠️ 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 requiresCompression: boolean
estimatedTokensAfterSwitch: number estimatedTokensAfterSwitch: number
} { } {
const modelName = this.switchToNextModel(currentContextTokens) const result = this.switchToNextModel(currentContextTokens)
if (!modelName) { if (!result.success || !result.modelName) {
return { return {
modelName: null, modelName: null,
contextAnalysis: null, contextAnalysis: null,
@ -382,7 +410,7 @@ export class ModelManager {
const newModel = this.getModel('main') const newModel = this.getModel('main')
if (!newModel) { if (!newModel) {
return { return {
modelName, modelName: result.modelName,
contextAnalysis: null, contextAnalysis: null,
requiresCompression: false, requiresCompression: false,
estimatedTokensAfterSwitch: currentContextTokens, estimatedTokensAfterSwitch: currentContextTokens,
@ -395,7 +423,7 @@ export class ModelManager {
) )
return { return {
modelName, modelName: result.modelName,
contextAnalysis: analysis, contextAnalysis: analysis,
requiresCompression: analysis.severity === 'critical', requiresCompression: analysis.severity === 'critical',
estimatedTokensAfterSwitch: currentContextTokens, 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[] { getAvailableModels(): ModelProfile[] {
return this.modelProfiles.filter(p => p.isActive) 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[] { getAllAvailableModelNames(): string[] {
return this.getAvailableModels().map(p => p.modelName) 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 * Remove a model profile
*/ */