commit
893112e43c
@ -419,7 +419,10 @@ export function ModelSelector({
|
||||
function getModelDetails(model: ModelInfo): string {
|
||||
const details = []
|
||||
|
||||
if (model.max_tokens) {
|
||||
// Show context_length if available (Ollama models), otherwise max_tokens
|
||||
if (model.context_length) {
|
||||
details.push(`${formatNumber(model.context_length)} tokens`)
|
||||
} else if (model.max_tokens) {
|
||||
details.push(`${formatNumber(model.max_tokens)} tokens`)
|
||||
}
|
||||
|
||||
@ -1040,13 +1043,15 @@ export function ModelSelector({
|
||||
}
|
||||
|
||||
// Transform Ollama models to our format
|
||||
// Note: max_tokens here is for OUTPUT tokens, not context length
|
||||
const ollamaModels = models.map((model: any) => ({
|
||||
model:
|
||||
model.id ??
|
||||
model.name ??
|
||||
model.modelName ??
|
||||
(typeof model === 'string' ? model : ''),
|
||||
provider: 'ollama',
|
||||
max_tokens: 4096, // Default value
|
||||
max_tokens: DEFAULT_MAX_TOKENS, // Default output tokens (8K is reasonable)
|
||||
supports_vision: false,
|
||||
supports_function_calling: true,
|
||||
supports_reasoning_effort: false,
|
||||
@ -1055,16 +1060,102 @@ export function ModelSelector({
|
||||
// Filter out models with empty names
|
||||
const validModels = ollamaModels.filter(model => model.model)
|
||||
|
||||
setAvailableModels(validModels)
|
||||
// Helper: normalize Ollama server root for /api/show (strip trailing /v1)
|
||||
const normalizeOllamaRoot = (url: string): string => {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
let pathname = u.pathname.replace(/\/+$|^$/, '')
|
||||
if (pathname.endsWith('/v1')) {
|
||||
pathname = pathname.slice(0, -3)
|
||||
}
|
||||
u.pathname = pathname
|
||||
return u.toString().replace(/\/+$/, '')
|
||||
} catch {
|
||||
return url.replace(/\/v1\/?$/, '')
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: extract num_ctx/context_length from /api/show response
|
||||
const extractContextTokens = (data: any): number | null => {
|
||||
if (!data || typeof data !== 'object') return null
|
||||
|
||||
// First check model_info for architecture-specific context_length fields
|
||||
// Example: qwen2.context_length, llama.context_length, etc.
|
||||
if (data.model_info && typeof data.model_info === 'object') {
|
||||
const modelInfo = data.model_info
|
||||
for (const key of Object.keys(modelInfo)) {
|
||||
if (key.endsWith('.context_length') || key.endsWith('_context_length')) {
|
||||
const val = modelInfo[key]
|
||||
if (typeof val === 'number' && isFinite(val) && val > 0) {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to other common fields
|
||||
const candidates = [
|
||||
(data as any)?.parameters?.num_ctx,
|
||||
(data as any)?.model_info?.num_ctx,
|
||||
(data as any)?.config?.num_ctx,
|
||||
(data as any)?.details?.context_length,
|
||||
(data as any)?.context_length,
|
||||
(data as any)?.num_ctx,
|
||||
(data as any)?.max_tokens,
|
||||
(data as any)?.max_new_tokens
|
||||
].filter((v: any) => typeof v === 'number' && isFinite(v) && v > 0)
|
||||
if (candidates.length > 0) {
|
||||
return Math.max(...candidates)
|
||||
}
|
||||
|
||||
// parameters may be a string like "num_ctx=4096 ..."
|
||||
if (typeof (data as any)?.parameters === 'string') {
|
||||
const m = (data as any).parameters.match(/num_ctx\s*[:=]\s*(\d+)/i)
|
||||
if (m) {
|
||||
const n = parseInt(m[1], 10)
|
||||
if (Number.isFinite(n) && n > 0) return n
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Enrich each model via /api/show to get accurate context length
|
||||
// Store context length separately from max_tokens (output limit)
|
||||
const ollamaRoot = normalizeOllamaRoot(ollamaBaseUrl)
|
||||
const enrichedModels = await Promise.all(
|
||||
validModels.map(async (m: any) => {
|
||||
try {
|
||||
const showResp = await fetch(`${ollamaRoot}/api/show`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: m.model })
|
||||
})
|
||||
if (showResp.ok) {
|
||||
const showData = await showResp.json()
|
||||
const ctx = extractContextTokens(showData)
|
||||
if (typeof ctx === 'number' && isFinite(ctx) && ctx > 0) {
|
||||
// Store context_length separately, don't override max_tokens
|
||||
return { ...m, context_length: ctx }
|
||||
}
|
||||
}
|
||||
// Fallback to default if missing
|
||||
return m
|
||||
} catch {
|
||||
return m
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
setAvailableModels(enrichedModels)
|
||||
|
||||
// Only navigate if we have models
|
||||
if (validModels.length > 0) {
|
||||
if (enrichedModels.length > 0) {
|
||||
navigateTo('model')
|
||||
} else {
|
||||
setModelLoadError('No models found in your Ollama installation')
|
||||
}
|
||||
|
||||
return validModels
|
||||
return enrichedModels
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
@ -1403,7 +1494,15 @@ export function ModelSelector({
|
||||
setReasoningEffort(null)
|
||||
}
|
||||
|
||||
// Set context length if available (from Ollama /api/show)
|
||||
if (modelInfo?.context_length) {
|
||||
setContextLength(modelInfo.context_length)
|
||||
} else {
|
||||
setContextLength(DEFAULT_CONTEXT_LENGTH)
|
||||
}
|
||||
|
||||
// Set max tokens based on model info or default
|
||||
// Note: max_tokens is for OUTPUT, not context window
|
||||
if (modelInfo?.max_tokens) {
|
||||
const modelMaxTokens = modelInfo.max_tokens
|
||||
// Check if the model's max tokens matches any of our presets
|
||||
|
||||
@ -2,7 +2,7 @@ import * as fs from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { existsSync } from 'fs'
|
||||
import shellquote from 'shell-quote'
|
||||
import { spawn, execSync, type ChildProcess } from 'child_process'
|
||||
import { spawn, execSync, execFileSync, type ChildProcess } from 'child_process'
|
||||
import { isAbsolute, resolve, join } from 'path'
|
||||
import { logError } from './log'
|
||||
import * as os from 'os'
|
||||
@ -130,12 +130,12 @@ function detectShell(): DetectedShell {
|
||||
|
||||
// 1) Respect SHELL if it points to a bash.exe that exists
|
||||
if (process.env.SHELL && /bash\.exe$/i.test(process.env.SHELL) && existsSync(process.env.SHELL)) {
|
||||
return { bin: process.env.SHELL, args: ['-l'], type: 'msys' }
|
||||
return { bin: process.env.SHELL, args: [], type: 'msys' }
|
||||
}
|
||||
|
||||
// 1.1) Explicit override
|
||||
if (process.env.KODE_BASH && existsSync(process.env.KODE_BASH)) {
|
||||
return { bin: process.env.KODE_BASH, args: ['-l'], type: 'msys' }
|
||||
return { bin: process.env.KODE_BASH, args: [], type: 'msys' }
|
||||
}
|
||||
|
||||
// 2) Common Git Bash/MSYS2 locations
|
||||
@ -165,7 +165,7 @@ function detectShell(): DetectedShell {
|
||||
|
||||
for (const c of candidates) {
|
||||
if (existsSync(c)) {
|
||||
return { bin: c, args: ['-l'], type: 'msys' }
|
||||
return { bin: c, args: [], type: 'msys' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +175,7 @@ function detectShell(): DetectedShell {
|
||||
for (const p of pathEntries) {
|
||||
const candidate = join(p, 'bash.exe')
|
||||
if (existsSync(candidate)) {
|
||||
return { bin: candidate, args: ['-l'], type: 'msys' }
|
||||
return { bin: candidate, args: [], type: 'msys' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,8 +269,15 @@ export class PersistentShell {
|
||||
this.stderrFileBashPath = toBashPath(this.stderrFile, this.shellType)
|
||||
this.cwdFileBashPath = toBashPath(this.cwdFile, this.shellType)
|
||||
|
||||
// Source ~/.bashrc when available (works for bash on POSIX/MSYS/WSL)
|
||||
this.sendToShell('[ -f ~/.bashrc ] && source ~/.bashrc || true')
|
||||
// Source ~/.bashrc when available (avoid login shells on MSYS to prevent cwd resets)
|
||||
if (this.shellType === 'msys') {
|
||||
// Use non-login shell; explicitly source but keep working directory
|
||||
this.sendToShell('[ -f ~/.bashrc ] && source ~/.bashrc || true')
|
||||
// Ensure CWD file reflects current Windows path immediately on MSYS
|
||||
this.sendToShell(`pwd -W > ${quoteForBash(this.cwdFileBashPath)}`)
|
||||
} else {
|
||||
this.sendToShell('[ -f ~/.bashrc ] && source ~/.bashrc || true')
|
||||
}
|
||||
}
|
||||
|
||||
private static instance: PersistentShell | null = null
|
||||
@ -385,30 +392,39 @@ export class PersistentShell {
|
||||
* - This sequence eliminates race conditions between exit code capture and CWD updates
|
||||
* - The pwd() method reads the CWD file directly for current directory info
|
||||
*/
|
||||
const quotedCommand = shellquote.quote([command])
|
||||
const quotedCommand = quoteForBash(command)
|
||||
|
||||
// Check the syntax of the command
|
||||
try {
|
||||
if (this.shellType === 'wsl') {
|
||||
execSync(`wsl.exe -e bash -n -c ${quotedCommand}`, {
|
||||
// On Windows WSL, avoid shell string quoting issues by using argv form
|
||||
execFileSync('wsl.exe', ['-e', 'bash', '-n', '-c', command], {
|
||||
stdio: 'ignore',
|
||||
timeout: 1000,
|
||||
})
|
||||
} else if (this.shellType === 'msys') {
|
||||
// On Windows Git Bash/MSYS, use execFileSync to bypass cmd.exe parsing
|
||||
execFileSync(this.binShell, ['-n', '-c', command], {
|
||||
stdio: 'ignore',
|
||||
timeout: 1000,
|
||||
})
|
||||
} else {
|
||||
// POSIX platforms: keep existing behavior
|
||||
execSync(`${this.binShell} -n -c ${quotedCommand}`, {
|
||||
stdio: 'ignore',
|
||||
timeout: 1000,
|
||||
})
|
||||
}
|
||||
} catch (stderr) {
|
||||
// If there's a syntax error, return an error and log it
|
||||
const errorStr =
|
||||
typeof stderr === 'string' ? stderr : String(stderr || '')
|
||||
} catch (error) {
|
||||
// If there's a syntax error, return an error with the actual exit code
|
||||
const execError = error as any
|
||||
const actualExitCode = execError?.status ?? execError?.code ?? 2 // Default to 2 (syntax error) if no code available
|
||||
const errorStr = execError?.stderr?.toString() || execError?.message || String(error || '')
|
||||
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: errorStr,
|
||||
code: 128,
|
||||
code: actualExitCode,
|
||||
interrupted: false,
|
||||
})
|
||||
}
|
||||
@ -432,8 +448,12 @@ export class PersistentShell {
|
||||
// 2. Capture exit code immediately after command execution to avoid losing it
|
||||
commandParts.push(`EXEC_EXIT_CODE=$?`)
|
||||
|
||||
// 3. Update CWD file
|
||||
commandParts.push(`pwd > ${quoteForBash(this.cwdFileBashPath)}`)
|
||||
// 3. Update CWD file (use Windows path on MSYS to keep Node path checks correct)
|
||||
if (this.shellType === 'msys') {
|
||||
commandParts.push(`pwd -W > ${quoteForBash(this.cwdFileBashPath)}`)
|
||||
} else {
|
||||
commandParts.push(`pwd > ${quoteForBash(this.cwdFileBashPath)}`)
|
||||
}
|
||||
|
||||
// 4. Write the preserved exit code to status file to avoid race with pwd
|
||||
commandParts.push(`echo $EXEC_EXIT_CODE > ${quoteForBash(this.statusFileBashPath)}`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user