Compare commits

...

4 Commits

Author SHA1 Message Date
simosmik
00278a13d8 Release 1.13.1 2025-12-30 17:51:32 +00:00
simosmik
676d2415a0 adding npmignore 2025-12-30 17:49:30 +00:00
simosmik
babe96eedd fix: API would be stringified twice. That is now fixed. 2025-12-29 23:18:38 +00:00
simosmik
60c8bda755 fix: pass model parameter to Claude and Codex SDKs
Previously, the model parameter was accepted by the /api/agent endpoint
and extracted from requests, but was never passed through to the Claude
SDK or Codex SDK, causing all requests to use default models regardless
of user selection.

Changes:
- Add model parameter to queryClaudeSDK() options in routes/agent.js
- Add model to threadOptions in openai-codex.js
- Remove unused /cost slash command and PRICING constants
- Centralize all model definitions in shared/modelConstants.js
- Update API documentation to dynamically load models from constants
2025-12-29 16:19:09 +00:00
13 changed files with 278 additions and 168 deletions

57
.npmignore Normal file
View File

@@ -0,0 +1,57 @@
*.md
!README.md
.env*
.gitignore
.nvmrc
.release-it.json
release.sh
postcss.config.js
vite.config.js
tailwind.config.js
# Database files
authdb/
*.db
*.sqlite
*.sqlite3
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# AI specific
.claude/
.cursor/
.roo/
.taskmaster/
.cline/
.windsurf/
.serena/
CLAUDE.md
.mcp.json
# Task files
tasks.json
tasks/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.13.0",
"version": "1.13.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@siteboon/claude-code-ui",
"version": "1.13.0",
"version": "1.13.1",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.29",

View File

@@ -1,6 +1,6 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.13.0",
"version": "1.13.1",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",

View File

@@ -489,7 +489,7 @@
<span class="endpoint-path"><span class="api-url">http://localhost:3001</span>/api/agent</span>
</div>
<p>Trigger an AI agent (Claude or Cursor) to work on a project.</p>
<p>Trigger an AI agent (Claude, Cursor, or Codex) to work on a project.</p>
<h4>Request Body Parameters</h4>
<table>
@@ -524,7 +524,7 @@
<td><code>provider</code></td>
<td>string</td>
<td><span class="badge badge-optional">Optional</span></td>
<td><code>claude</code> or <code>cursor</code> (default: <code>claude</code>)</td>
<td><code>claude</code>, <code>cursor</code>, or <code>codex</code> (default: <code>claude</code>)</td>
</tr>
<tr>
<td><code>stream</code></td>
@@ -536,7 +536,9 @@
<td><code>model</code></td>
<td>string</td>
<td><span class="badge badge-optional">Optional</span></td>
<td>Model to use (for Cursor)</td>
<td id="model-options-cell">
Model identifier for the AI provider (loading from constants...)
</td>
</tr>
<tr>
<td><code>cleanup</code></td>
@@ -818,31 +820,51 @@ data: {"type":"done"}</code></pre>
</div>
</div>
<script>
<script type="module">
// Import model constants
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '/shared/modelConstants.js';
// Dynamic URL replacement
const apiUrl = window.location.origin;
document.querySelectorAll('.api-url').forEach(el => {
el.textContent = apiUrl;
});
// Dynamically populate model documentation
window.addEventListener('DOMContentLoaded', () => {
const modelCell = document.getElementById('model-options-cell');
if (modelCell) {
const claudeModels = CLAUDE_MODELS.OPTIONS.map(m => `<code>${m.value}</code>`).join(', ');
const cursorModels = CURSOR_MODELS.OPTIONS.slice(0, 8).map(m => `<code>${m.value}</code>`).join(', ');
const codexModels = CODEX_MODELS.OPTIONS.map(m => `<code>${m.value}</code>`).join(', ');
modelCell.innerHTML = `
Model identifier for the AI provider:<br><br>
<strong>Claude:</strong> ${claudeModels} (default: <code>${CLAUDE_MODELS.DEFAULT}</code>)<br><br>
<strong>Cursor:</strong> ${cursorModels}, and more (default: <code>${CURSOR_MODELS.DEFAULT}</code>)<br><br>
<strong>Codex:</strong> ${codexModels} (default: <code>${CODEX_MODELS.DEFAULT}</code>)
`;
}
});
// Tab switching
function showTab(tabName) {
window.showTab = function(tabName) {
const parentBlock = event.target.closest('.example-block');
if (!parentBlock) return;
parentBlock.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
parentBlock.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
const targetTab = parentBlock.querySelector('#' + tabName);
if (targetTab) {
targetTab.classList.add('active');
event.target.classList.add('active');
}
}
};
</script>
<!-- Prism.js -->

View File

@@ -16,6 +16,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { CLAUDE_MODELS } from '../shared/modelConstants.js';
// Session tracking: Map of session IDs to active query instances
const activeSessions = new Map();
@@ -77,7 +78,7 @@ function mapCliOptionsToSDK(options = {}) {
// Map model (default to sonnet)
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
sdkOptions.model = options.model || 'sonnet';
sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
console.log(`Using model: ${sdkOptions.model}`);
// Map system prompt configuration
@@ -397,10 +398,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
ws.send(JSON.stringify({
ws.send({
type: 'session-created',
sessionId: capturedSessionId
}));
});
} else {
console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
}
@@ -410,20 +411,20 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Transform and send message to WebSocket
const transformedMessage = transformMessage(message);
ws.send(JSON.stringify({
ws.send({
type: 'claude-response',
data: transformedMessage
}));
});
// Extract and send token budget updates from result messages
if (message.type === 'result') {
const tokenBudget = extractTokenBudget(message);
if (tokenBudget) {
console.log('Token budget from modelUsage:', tokenBudget);
ws.send(JSON.stringify({
ws.send({
type: 'token-budget',
data: tokenBudget
}));
});
}
}
}
@@ -438,12 +439,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Send completion event
console.log('Streaming complete, sending claude-complete event');
ws.send(JSON.stringify({
ws.send({
type: 'claude-complete',
sessionId: capturedSessionId,
exitCode: 0,
isNewSession: !sessionId && !!command
}));
});
console.log('claude-complete event sent');
} catch (error) {
@@ -458,10 +459,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
await cleanupTempFiles(tempImagePaths, tempDir);
// Send error to WebSocket
ws.send(JSON.stringify({
ws.send({
type: 'claude-error',
error: error.message
}));
});
throw error;
}

View File

@@ -102,29 +102,29 @@ async function spawnCursor(command, options = {}, ws) {
// Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
ws.send(JSON.stringify({
ws.send({
type: 'session-created',
sessionId: capturedSessionId,
model: response.model,
cwd: response.cwd
}));
});
}
}
// Send system info to frontend
ws.send(JSON.stringify({
ws.send({
type: 'cursor-system',
data: response
}));
});
}
break;
case 'user':
// Forward user message
ws.send(JSON.stringify({
ws.send({
type: 'cursor-user',
data: response
}));
});
break;
case 'assistant':
@@ -134,7 +134,7 @@ async function spawnCursor(command, options = {}, ws) {
messageBuffer += textContent;
// Send as Claude-compatible format for frontend
ws.send(JSON.stringify({
ws.send({
type: 'claude-response',
data: {
type: 'content_block_delta',
@@ -143,7 +143,7 @@ async function spawnCursor(command, options = {}, ws) {
text: textContent
}
}
}));
});
}
break;
@@ -153,37 +153,37 @@ async function spawnCursor(command, options = {}, ws) {
// Send final message if we have buffered content
if (messageBuffer) {
ws.send(JSON.stringify({
ws.send({
type: 'claude-response',
data: {
type: 'content_block_stop'
}
}));
});
}
// Send completion event
ws.send(JSON.stringify({
ws.send({
type: 'cursor-result',
sessionId: capturedSessionId || sessionId,
data: response,
success: response.subtype === 'success'
}));
});
break;
default:
// Forward any other message types
ws.send(JSON.stringify({
ws.send({
type: 'cursor-response',
data: response
}));
});
}
} catch (parseError) {
console.log('📄 Non-JSON response:', line);
// If not JSON, send as raw text
ws.send(JSON.stringify({
ws.send({
type: 'cursor-output',
data: line
}));
});
}
}
});
@@ -191,10 +191,10 @@ async function spawnCursor(command, options = {}, ws) {
// Handle stderr
cursorProcess.stderr.on('data', (data) => {
console.error('Cursor CLI stderr:', data.toString());
ws.send(JSON.stringify({
ws.send({
type: 'cursor-error',
error: data.toString()
}));
});
});
// Handle process completion
@@ -205,12 +205,12 @@ async function spawnCursor(command, options = {}, ws) {
const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId);
ws.send(JSON.stringify({
ws.send({
type: 'claude-complete',
sessionId: finalSessionId,
exitCode: code,
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
}));
});
if (code === 0) {
resolve();
@@ -226,12 +226,12 @@ async function spawnCursor(command, options = {}, ws) {
// Clean up process reference on error
const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId);
ws.send(JSON.stringify({
ws.send({
type: 'cursor-error',
error: error.message
}));
});
reject(error);
});

View File

@@ -717,6 +717,32 @@ wss.on('connection', (ws, request) => {
}
});
/**
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
*/
class WebSocketWriter {
constructor(ws) {
this.ws = ws;
this.sessionId = null;
this.isWebSocketWriter = true; // Marker for transport detection
}
send(data) {
if (this.ws.readyState === 1) { // WebSocket.OPEN
// Providers send raw objects, we stringify for WebSocket
this.ws.send(JSON.stringify(data));
}
}
setSessionId(sessionId) {
this.sessionId = sessionId;
}
getSessionId() {
return this.sessionId;
}
}
// Handle chat WebSocket connections
function handleChatConnection(ws) {
console.log('[INFO] Chat WebSocket connected');
@@ -724,6 +750,9 @@ function handleChatConnection(ws) {
// Add to connected clients for project updates
connectedClients.add(ws);
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
const writer = new WebSocketWriter(ws);
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
@@ -734,19 +763,19 @@ function handleChatConnection(ws) {
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
// Use Claude Agents SDK
await queryClaudeSDK(data.command, data.options, ws);
await queryClaudeSDK(data.command, data.options, writer);
} else if (data.type === 'cursor-command') {
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default');
await spawnCursor(data.command, data.options, ws);
await spawnCursor(data.command, data.options, writer);
} else if (data.type === 'codex-command') {
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default');
await queryCodex(data.command, data.options, ws);
await queryCodex(data.command, data.options, writer);
} else if (data.type === 'cursor-resume') {
// Backward compatibility: treat as cursor-command with resume and no prompt
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -754,7 +783,7 @@ function handleChatConnection(ws) {
sessionId: data.sessionId,
resume: true,
cwd: data.options?.cwd
}, ws);
}, writer);
} else if (data.type === 'abort-session') {
console.log('[DEBUG] Abort session request:', data.sessionId);
const provider = data.provider || 'claude';
@@ -769,21 +798,21 @@ function handleChatConnection(ws) {
success = await abortClaudeSDKSession(data.sessionId);
}
ws.send(JSON.stringify({
writer.send({
type: 'session-aborted',
sessionId: data.sessionId,
provider,
success
}));
});
} else if (data.type === 'cursor-abort') {
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
const success = abortCursorSession(data.sessionId);
ws.send(JSON.stringify({
writer.send({
type: 'session-aborted',
sessionId: data.sessionId,
provider: 'cursor',
success
}));
});
} else if (data.type === 'check-session-status') {
// Check if a specific session is currently processing
const provider = data.provider || 'claude';
@@ -799,12 +828,12 @@ function handleChatConnection(ws) {
isActive = isClaudeSDKSessionActive(sessionId);
}
ws.send(JSON.stringify({
writer.send({
type: 'session-status',
sessionId,
provider,
isProcessing: isActive
}));
});
} else if (data.type === 'get-active-sessions') {
// Get all currently active sessions
const activeSessions = {
@@ -812,17 +841,17 @@ function handleChatConnection(ws) {
cursor: getActiveCursorSessions(),
codex: getActiveCodexSessions()
};
ws.send(JSON.stringify({
writer.send({
type: 'active-sessions',
sessions: activeSessions
}));
});
}
} catch (error) {
console.error('[ERROR] Chat WebSocket error:', error.message);
ws.send(JSON.stringify({
writer.send({
type: 'error',
error: error.message
}));
});
}
});

View File

@@ -213,7 +213,8 @@ export async function queryCodex(command, options = {}, ws) {
workingDirectory,
skipGitRepoCheck: true,
sandboxMode,
approvalPolicy
approvalPolicy,
model
};
// Start or resume thread
@@ -359,12 +360,12 @@ export function getActiveCodexSessions() {
*/
function sendMessage(ws, data) {
try {
if (typeof ws.send === 'function') {
// WebSocket
if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
// Writer handles stringification (SSEStreamWriter or WebSocketWriter)
ws.send(data);
} else if (typeof ws.send === 'function') {
// Raw WebSocket - stringify here
ws.send(JSON.stringify(data));
} else if (typeof ws.write === 'function') {
// SSE writer (for agent API)
ws.write(`data: ${JSON.stringify(data)}\n\n`);
}
} catch (error) {
console.error('[Codex] Error sending message:', error);

View File

@@ -10,6 +10,7 @@ import { queryClaudeSDK } from '../claude-sdk.js';
import { spawnCursor } from '../cursor-cli.js';
import { queryCodex } from '../openai-codex.js';
import { Octokit } from '@octokit/rest';
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
const router = express.Router();
@@ -450,6 +451,7 @@ class SSEStreamWriter {
constructor(res) {
this.res = res;
this.sessionId = null;
this.isSSEStreamWriter = true; // Marker for transport detection
}
send(data) {
@@ -457,7 +459,7 @@ class SSEStreamWriter {
return;
}
// Format as SSE
// Format as SSE - providers send raw objects, we stringify
this.res.write(`data: ${JSON.stringify(data)}\n\n`);
}
@@ -634,9 +636,14 @@ class ResponseCollector {
* - true: Returns text/event-stream with incremental updates
* - false: Returns complete JSON response after completion
*
* @param {string} model - (Optional) Model identifier for Cursor provider.
* Only applicable when provider='cursor'.
* Examples: 'gpt-4', 'claude-3-opus', etc.
* @param {string} model - (Optional) Model identifier for providers.
*
* Claude models: 'sonnet' (default), 'opus', 'haiku', 'opusplan', 'sonnet[1m]'
* Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5',
* 'gemini-3-pro', 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high',
* 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
* 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
* Codex models: 'gpt-5.2' (default), 'gpt-5.1-codex-max', 'o3', 'o4-mini'
*
* @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
* Default: true
@@ -939,6 +946,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null, // New session
model: model,
permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
}, writer);
@@ -959,7 +967,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
projectPath: finalProjectPath,
cwd: finalProjectPath,
sessionId: null,
model: model || 'gpt-5.2',
model: model || CODEX_MODELS.DEFAULT,
permissionMode: 'bypassPermissions'
}, writer);
}

View File

@@ -4,6 +4,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import os from 'os';
import matter from 'gray-matter';
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -182,23 +183,15 @@ Custom commands can be created in:
},
'/model': async (args, context) => {
// Read available models from config or defaults
// Read available models from centralized constants
const availableModels = {
claude: [
'claude-sonnet-4.5',
'claude-sonnet-4',
'claude-opus-4',
'claude-sonnet-3.5'
],
cursor: [
'gpt-5',
'sonnet-4',
'opus-4.1'
]
claude: CLAUDE_MODELS.OPTIONS.map(o => o.value),
cursor: CURSOR_MODELS.OPTIONS.map(o => o.value),
codex: CODEX_MODELS.OPTIONS.map(o => o.value)
};
const currentProvider = context?.provider || 'claude';
const currentModel = context?.model || 'claude-sonnet-4.5';
const currentModel = context?.model || CLAUDE_MODELS.DEFAULT;
return {
type: 'builtin',
@@ -216,50 +209,6 @@ Custom commands can be created in:
};
},
'/cost': async (args, context) => {
// Calculate token usage and cost
const sessionId = context?.sessionId;
const tokenUsage = context?.tokenUsage || { used: 0, total: 200000 };
const costPerMillion = {
'claude-sonnet-4.5': { input: 3, output: 15 },
'claude-sonnet-4': { input: 3, output: 15 },
'claude-opus-4': { input: 15, output: 75 },
'gpt-5': { input: 5, output: 15 }
};
const model = context?.model || 'claude-sonnet-4.5';
const rates = costPerMillion[model] || costPerMillion['claude-sonnet-4.5'];
// Estimate 70% input, 30% output
const estimatedInputTokens = Math.floor(tokenUsage.used * 0.7);
const estimatedOutputTokens = Math.floor(tokenUsage.used * 0.3);
const inputCost = (estimatedInputTokens / 1000000) * rates.input;
const outputCost = (estimatedOutputTokens / 1000000) * rates.output;
const totalCost = inputCost + outputCost;
return {
type: 'builtin',
action: 'cost',
data: {
tokenUsage: {
used: tokenUsage.used,
total: tokenUsage.total,
percentage: ((tokenUsage.used / tokenUsage.total) * 100).toFixed(1)
},
cost: {
input: inputCost.toFixed(4),
output: outputCost.toFixed(4),
total: totalCost.toFixed(4),
currency: 'USD'
},
model,
rates
}
};
},
'/status': async (args, context) => {
// Read version from package.json
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');

View File

@@ -6,6 +6,7 @@ import { spawn } from 'child_process';
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import crypto from 'crypto';
import { CURSOR_MODELS } from '../../shared/modelConstants.js';
const router = express.Router();
@@ -33,7 +34,7 @@ router.get('/config', async (req, res) => {
config: {
version: 1,
model: {
modelId: "gpt-5",
modelId: CURSOR_MODELS.DEFAULT,
displayName: "GPT-5"
},
permissions: {

65
shared/modelConstants.js Normal file
View File

@@ -0,0 +1,65 @@
/**
* Centralized Model Definitions
* Single source of truth for all supported AI models
*/
/**
* Claude (Anthropic) Models
*
* Note: Claude uses two different formats:
* - SDK format ('sonnet', 'opus') - used by the UI and claude-sdk.js
* - API format ('claude-sonnet-4.5') - used by slash commands for display
*/
export const CLAUDE_MODELS = {
// Models in SDK format (what the actual SDK accepts)
OPTIONS: [
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'opus', label: 'Opus' },
{ value: 'haiku', label: 'Haiku' },
{ value: 'opusplan', label: 'Opus Plan' },
{ value: 'sonnet[1m]', label: 'Sonnet [1M]' }
],
DEFAULT: 'sonnet'
};
/**
* Cursor Models
*/
export const CURSOR_MODELS = {
OPTIONS: [
{ value: 'gpt-5.2-high', label: 'GPT-5.2 High' },
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro' },
{ value: 'opus-4.5-thinking', label: 'Claude 4.5 Opus (Thinking)' },
{ value: 'gpt-5.2', label: 'GPT-5.2' },
{ value: 'gpt-5.1', label: 'GPT-5.1' },
{ value: 'gpt-5.1-high', label: 'GPT-5.1 High' },
{ value: 'composer-1', label: 'Composer 1' },
{ value: 'auto', label: 'Auto' },
{ value: 'sonnet-4.5', label: 'Claude 4.5 Sonnet' },
{ value: 'sonnet-4.5-thinking', label: 'Claude 4.5 Sonnet (Thinking)' },
{ value: 'opus-4.5', label: 'Claude 4.5 Opus' },
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
{ value: 'gpt-5.1-codex-high', label: 'GPT-5.1 Codex High' },
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
{ value: 'gpt-5.1-codex-max-high', label: 'GPT-5.1 Codex Max High' },
{ value: 'opus-4.1', label: 'Claude 4.1 Opus' },
{ value: 'grok', label: 'Grok' }
],
DEFAULT: 'gpt-5'
};
/**
* Codex (OpenAI) Models
*/
export const CODEX_MODELS = {
OPTIONS: [
{ value: 'gpt-5.2', label: 'GPT-5.2' },
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
{ value: 'o3', label: 'O3' },
{ value: 'o4-mini', label: 'O4-mini' }
],
DEFAULT: 'gpt-5.2'
};

View File

@@ -35,6 +35,7 @@ import { MicButton } from './MicButton.jsx';
import { api, authenticatedFetch } from '../utils/api';
import Fuse from 'fuse.js';
import CommandMenu from './CommandMenu';
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants';
// Helper function to decode HTML entities in text
@@ -1723,13 +1724,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
return localStorage.getItem('selected-provider') || 'claude';
});
const [cursorModel, setCursorModel] = useState(() => {
return localStorage.getItem('cursor-model') || 'gpt-5';
return localStorage.getItem('cursor-model') || CURSOR_MODELS.DEFAULT;
});
const [claudeModel, setClaudeModel] = useState(() => {
return localStorage.getItem('claude-model') || 'sonnet';
return localStorage.getItem('claude-model') || CLAUDE_MODELS.DEFAULT;
});
const [codexModel, setCodexModel] = useState(() => {
return localStorage.getItem('codex-model') || 'gpt-5.2';
return localStorage.getItem('codex-model') || CODEX_MODELS.DEFAULT;
});
// Load permission mode for the current session
useEffect(() => {
@@ -1758,17 +1759,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
.then(res => res.json())
.then(data => {
if (data.success && data.config?.model?.modelId) {
// Map Cursor model IDs to our simplified names
const modelMap = {
'gpt-5': 'gpt-5',
'claude-4-sonnet': 'sonnet-4',
'sonnet-4': 'sonnet-4',
'claude-4-opus': 'opus-4.1',
'opus-4.1': 'opus-4.1'
};
const mappedModel = modelMap[data.config.model.modelId] || data.config.model.modelId;
// Use the model from config directly
const modelId = data.config.model.modelId;
if (!localStorage.getItem('cursor-model')) {
setCursorModel(mappedModel);
setCursorModel(modelId);
}
}
})
@@ -4547,11 +4541,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}}
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 min-w-[140px]"
>
<option value="sonnet">Sonnet</option>
<option value="opus">Opus</option>
<option value="haiku">Haiku</option>
<option value="opusplan">Opus Plan</option>
<option value="sonnet[1m]">Sonnet [1M]</option>
{CLAUDE_MODELS.OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
) : provider === 'codex' ? (
<select
@@ -4563,10 +4555,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}}
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500 min-w-[140px]"
>
<option value="gpt-5.2">GPT-5.2</option>
<option value="gpt-5.1-codex-max">GPT-5.1 Codex Max</option>
<option value="o3">O3</option>
<option value="o4-mini">O4-mini</option>
{CODEX_MODELS.OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
) : (
<select
@@ -4579,23 +4570,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 min-w-[140px]"
disabled={provider !== 'cursor'}
>
<option value="gpt-5.2-high">GPT-5.2 High</option>
<option value="gemini-3-pro">Gemini 3 Pro</option>
<option value="opus-4.5-thinking">Claude 4.5 Opus (Thinking)</option>
<option value="gpt-5.2">GPT-5.2</option>
<option value="gpt-5.1">GPT-5.1</option>
<option value="gpt-5.1-high">GPT-5.1 High</option>
<option value="composer-1">Composer 1</option>
<option value="auto">Auto</option>
<option value="sonnet-4.5">Claude 4.5 Sonnet</option>
<option value="sonnet-4.5-thinking">Claude 4.5 Sonnet (Thinking)</option>
<option value="opus-4.5">Claude 4.5 Opus</option>
<option value="gpt-5.1-codex">GPT-5.1 Codex</option>
<option value="gpt-5.1-codex-high">GPT-5.1 Codex High</option>
<option value="gpt-5.1-codex-max">GPT-5.1 Codex Max</option>
<option value="gpt-5.1-codex-max-high">GPT-5.1 Codex Max High</option>
<option value="opus-4.1">Claude 4.1 Opus</option>
<option value="grok">Grok</option>
{CURSOR_MODELS.OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
)}
</div>