mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-25 12:16:00 +08:00
Merge branch 'main' into feat/voice
This commit is contained in:
@@ -204,7 +204,7 @@ function mapCliOptionsToSDK(options = {}) {
|
||||
sdkOptions.disallowedTools = settings.disallowedTools || [];
|
||||
|
||||
// Map model (default to sonnet)
|
||||
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
|
||||
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m], fable
|
||||
sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
|
||||
// Model logged at query start below
|
||||
|
||||
|
||||
@@ -455,7 +455,7 @@ async function sandboxCommand(args) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
console.log(`${c.info('▶')} Launching CloudCLI web server...`);
|
||||
sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']);
|
||||
sbx(['exec', opts.name, 'bash', '-c', 'nohup cloudcli start --port 3001 > /tmp/cloudcli-ui.log 2>&1 & disown']);
|
||||
|
||||
console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
|
||||
try {
|
||||
@@ -554,7 +554,7 @@ async function sandboxCommand(args) {
|
||||
|
||||
// Step 3: Start CloudCLI inside the sandbox
|
||||
console.log(`${c.info('▶')} Launching CloudCLI web server...`);
|
||||
sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']);
|
||||
sbx(['exec', opts.name, 'bash', '-c', 'nohup cloudcli start --port 3001 > /tmp/cloudcli-ui.log 2>&1 & disown']);
|
||||
|
||||
// Step 4: Forward port
|
||||
console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
|
||||
|
||||
@@ -85,6 +85,9 @@ const __dirname = getModuleDir(import.meta.url);
|
||||
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
||||
const APP_ROOT = findAppRoot(__dirname);
|
||||
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
||||
const MAX_FILE_UPLOAD_SIZE_MB = 200;
|
||||
const MAX_FILE_UPLOAD_SIZE_BYTES = MAX_FILE_UPLOAD_SIZE_MB * 1024 * 1024;
|
||||
const MAX_FILE_UPLOAD_COUNT = 20;
|
||||
|
||||
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
||||
|
||||
@@ -900,27 +903,27 @@ const uploadFilesHandler = async (req, res) => {
|
||||
}
|
||||
}),
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB limit
|
||||
files: 20 // Max 20 files at once
|
||||
fileSize: MAX_FILE_UPLOAD_SIZE_BYTES,
|
||||
files: MAX_FILE_UPLOAD_COUNT
|
||||
}
|
||||
});
|
||||
|
||||
// Use multer middleware
|
||||
uploadMiddleware.array('files', 20)(req, res, async (err) => {
|
||||
uploadMiddleware.array('files', MAX_FILE_UPLOAD_COUNT)(req, res, async (err) => {
|
||||
if (err) {
|
||||
console.error('Multer error:', err);
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
|
||||
return res.status(400).json({ error: `File too large. Maximum size is ${MAX_FILE_UPLOAD_SIZE_MB}MB.` });
|
||||
}
|
||||
if (err.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({ error: 'Too many files. Maximum is 20 files.' });
|
||||
return res.status(400).json({ error: `Too many files. Maximum is ${MAX_FILE_UPLOAD_COUNT} files.` });
|
||||
}
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { targetPath, relativePaths } = req.body;
|
||||
const { targetPath, relativePaths, requestedFileCount: requestedFileCountRaw } = req.body;
|
||||
|
||||
// Parse relative paths if provided (for folder uploads)
|
||||
let filePaths = [];
|
||||
@@ -944,6 +947,11 @@ const uploadFilesHandler = async (req, res) => {
|
||||
return res.status(400).json({ error: 'No files provided' });
|
||||
}
|
||||
|
||||
const parsedRequestedFileCount = Number.parseInt(requestedFileCountRaw, 10);
|
||||
const requestedFileCount = Number.isFinite(parsedRequestedFileCount) && parsedRequestedFileCount > 0
|
||||
? parsedRequestedFileCount
|
||||
: req.files.length;
|
||||
|
||||
// Resolve the project directory through the DB using the new projectId.
|
||||
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
||||
if (!projectRoot) {
|
||||
@@ -1022,8 +1030,10 @@ const uploadFilesHandler = async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
files: uploadedFiles,
|
||||
uploadedCount: uploadedFiles.length,
|
||||
requestedFileCount,
|
||||
targetPath: resolvedTargetDir,
|
||||
message: `Uploaded ${uploadedFiles.length} file(s) successfully`
|
||||
message: `Uploaded ${uploadedFiles.length} ${uploadedFiles.length === 1 ? 'file' : 'files'} successfully`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error uploading files:', error);
|
||||
|
||||
@@ -10,6 +10,7 @@ type NotificationPreferences = {
|
||||
channels: {
|
||||
inApp: boolean;
|
||||
webPush: boolean;
|
||||
sound: boolean;
|
||||
};
|
||||
events: {
|
||||
actionRequired: boolean;
|
||||
@@ -22,6 +23,7 @@ const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {
|
||||
channels: {
|
||||
inApp: false,
|
||||
webPush: false,
|
||||
sound: true,
|
||||
},
|
||||
events: {
|
||||
actionRequired: true,
|
||||
@@ -37,6 +39,7 @@ function normalizeNotificationPreferences(value: unknown): NotificationPreferenc
|
||||
channels: {
|
||||
inApp: source.channels?.inApp === true,
|
||||
webPush: source.channels?.webPush === true,
|
||||
sound: source.channels?.sound !== false,
|
||||
},
|
||||
events: {
|
||||
actionRequired: source.events?.actionRequired !== false,
|
||||
|
||||
@@ -83,7 +83,7 @@ The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
|
||||
- Update `server/modules/providers/provider.routes.ts`.
|
||||
- Update `server/routes/agent.js` if the provider is launchable from the agent runtime.
|
||||
- Update `server/index.js` if the provider needs runtime boot or shutdown wiring.
|
||||
- Update `public/modelConstants.js` if the provider appears in README or public API docs.
|
||||
- Update the `PROVIDER_ORDER` list in `public/api-docs.html` if the provider should appear in the public API docs.
|
||||
- Update `src/components/chat/hooks/useChatProviderState.ts` and
|
||||
`src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx` if
|
||||
the provider should be selectable in chat.
|
||||
|
||||
@@ -20,6 +20,11 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
|
||||
label: 'Default (recommended)',
|
||||
description: 'Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok',
|
||||
},
|
||||
{
|
||||
value: 'fable',
|
||||
label: 'Fable',
|
||||
description: 'Fable 5 · Most capable for your hardest and longest-running tasks · Uses your limits ~2× faster than Opus',
|
||||
},
|
||||
{
|
||||
value: "sonnet",
|
||||
label: "Sonnet",
|
||||
|
||||
@@ -646,7 +646,7 @@ class ResponseCollector {
|
||||
*
|
||||
* @param {string} model - (Optional) Model identifier for providers.
|
||||
*
|
||||
* Claude models: 'sonnet' (default), 'opus', 'haiku', 'opusplan', 'sonnet[1m]'
|
||||
* Claude models: 'sonnet' (default), 'opus', 'haiku', 'opusplan', 'sonnet[1m]', 'fable'
|
||||
* 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',
|
||||
|
||||
@@ -68,7 +68,7 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & {
|
||||
export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode';
|
||||
|
||||
/**
|
||||
* One selectable model row (matches the documentation `public/modelConstants.js` option shape).
|
||||
* One selectable model row in a provider model catalog.
|
||||
*/
|
||||
export type ProviderModelOption = {
|
||||
value: string;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
const PLUGINS_DIR = path.join(os.homedir(), '.claude-code-ui', 'plugins');
|
||||
const PLUGINS_CONFIG_PATH = path.join(os.homedir(), '.claude-code-ui', 'plugins.json');
|
||||
|
||||
@@ -7,6 +7,41 @@ const runningPlugins = new Map();
|
||||
// Map<pluginName, Promise<port>> — in-flight start operations
|
||||
const startingPlugins = new Map();
|
||||
|
||||
/**
|
||||
* Build the environment handed to a plugin server subprocess.
|
||||
*
|
||||
* Intentionally minimal: only non-secret essentials, never the host's full
|
||||
* environment. On Windows a handful of system variables are required for any
|
||||
* child to bootstrap (Node itself, and any Python or CLI a plugin shells out
|
||||
* to). Without APPDATA a `pip install --user` tool cannot locate its
|
||||
* site-packages and fails to import; SystemRoot, PATHEXT and TEMP are needed to
|
||||
* resolve system DLLs, executable extensions and a temp directory. None of
|
||||
* these carry secrets, so the ones that are set get passed straight through.
|
||||
*/
|
||||
function buildPluginEnv(name) {
|
||||
const env = {
|
||||
PATH: process.env.PATH,
|
||||
HOME: process.env.HOME,
|
||||
NODE_ENV: process.env.NODE_ENV || 'production',
|
||||
PLUGIN_NAME: name,
|
||||
};
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const WINDOWS_ESSENTIALS = [
|
||||
'SystemRoot', 'windir', 'SystemDrive',
|
||||
'USERPROFILE', 'APPDATA', 'LOCALAPPDATA',
|
||||
'TEMP', 'TMP', 'PATHEXT',
|
||||
];
|
||||
for (const key of WINDOWS_ESSENTIALS) {
|
||||
if (process.env[key] !== undefined) {
|
||||
env[key] = process.env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a plugin's server subprocess.
|
||||
* The plugin's server entry must print a JSON line with { ready: true, port: <number> }
|
||||
@@ -26,15 +61,9 @@ export function startPluginServer(name, pluginDir, serverEntry) {
|
||||
|
||||
const serverPath = path.join(pluginDir, serverEntry);
|
||||
|
||||
// Restricted env — only essentials, no host secrets
|
||||
const pluginProcess = spawn('node', [serverPath], {
|
||||
cwd: pluginDir,
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HOME: process.env.HOME,
|
||||
NODE_ENV: process.env.NODE_ENV || 'production',
|
||||
PLUGIN_NAME: name,
|
||||
},
|
||||
env: buildPluginEnv(name),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user