Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 33 additions & 42 deletions server/modules/websocket/services/shell-websocket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,46 +191,33 @@ export function handleShellConnection(
isPlainShell && initialCommand
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
: '';
ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
const sessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
ptySessionKey = sessionKey;

if (isLoginCommand) {
const oldSession = ptySessionsMap.get(ptySessionKey);
const oldSession = ptySessionsMap.get(sessionKey);
if (oldSession) {
if (oldSession.timeoutId) {
clearTimeout(oldSession.timeoutId);
}
oldSession.pty.kill();
ptySessionsMap.delete(ptySessionKey);
ptySessionsMap.delete(sessionKey);
}
}

const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
const existingSession = isLoginCommand ? null : ptySessionsMap.get(sessionKey);
if (existingSession) {
shellProcess = existingSession.pty;
// Kill the old PTY and start fresh so the shell shows current state.
// claude --resume <sessionId> restores conversation from the JSONL file.
if (existingSession.timeoutId) {
clearTimeout(existingSession.timeoutId);
}

ws.send(
JSON.stringify({
type: 'output',
data: '\x1b[36m[Reconnected to existing session]\x1b[0m\r\n',
})
);

if (existingSession.buffer.length > 0) {
existingSession.buffer.forEach((bufferedData) => {
ws.send(
JSON.stringify({
type: 'output',
data: bufferedData,
})
);
});
try {
existingSession.pty.kill();
} catch {
// PTY may already be dead
}

existingSession.ws = ws;
return;
ptySessionsMap.delete(sessionKey);
}

const resolvedProjectPath = path.resolve(projectPath);
Expand All @@ -257,7 +244,7 @@ export function handleShellConnection(
const termCols = readNumber(data.cols, 80);
const termRows = readNumber(data.rows, 24);

shellProcess = pty.spawn(shell, shellArgs, {
const spawnedPty = pty.spawn(shell, shellArgs, {
name: 'xterm-256color',
cols: termCols,
rows: termRows,
Expand All @@ -270,22 +257,22 @@ export function handleShellConnection(
},
});

ptySessionsMap.set(ptySessionKey, {
pty: shellProcess,
shellProcess = spawnedPty;

ptySessionsMap.set(sessionKey, {
pty: spawnedPty,
ws,
buffer: [],
timeoutId: null,
projectPath,
sessionId,
});

shellProcess.onData((chunk) => {
if (!ptySessionKey) {
return;
}

const session = ptySessionsMap.get(ptySessionKey);
if (!session) {
// Capture immutable locals so callback closures never reference a
// replacement PTY or session key from a later init on the same socket.
spawnedPty.onData((chunk) => {
const session = ptySessionsMap.get(sessionKey);
if (!session || session.pty !== spawnedPty) {
return;
}

Expand Down Expand Up @@ -355,13 +342,15 @@ export function handleShellConnection(
}
});

shellProcess.onExit((exitCode) => {
if (!ptySessionKey) {
spawnedPty.onExit((exitCode) => {
const session = ptySessionsMap.get(sessionKey);
// Only act if this PTY is still the active one — a replacement PTY
// may have been spawned while this one was being killed.
if (!session || session.pty !== spawnedPty) {
return;
}

const session = ptySessionsMap.get(ptySessionKey);
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
session.ws.send(
JSON.stringify({
type: 'output',
Expand All @@ -372,12 +361,14 @@ export function handleShellConnection(
);
}

if (session?.timeoutId) {
if (session.timeoutId) {
clearTimeout(session.timeoutId);
}

ptySessionsMap.delete(ptySessionKey);
shellProcess = null;
ptySessionsMap.delete(sessionKey);
if (shellProcess === spawnedPty) {
shellProcess = null;
}
});

let welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
Expand Down
2 changes: 2 additions & 0 deletions shared/modelConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const CLAUDE_MODELS = {
{ value: "opusplan", label: "Opus Plan" },
{ value: "sonnet[1m]", label: "Sonnet [1M]" },
{ value: "opus[1m]", label: "Opus [1M]" },
{ value: "deepseek-v4-flash", label: "DeepSeek V4 Flash" },
{ value: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
Comment on lines +23 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how CLAUDE_MODELS / PROVIDERS are consumed downstream to determine
# if DeepSeek entries being under the "claude" provider causes routing failures.

# Find all files that import or reference CLAUDE_MODELS or PROVIDERS
rg -n --type=ts --type=js -C3 'CLAUDE_MODELS|PROVIDERS' \
  -g '!shared/modelConstants.js'

Repository: siteboon/claudecodeui

Length of output: 11976


🏁 Script executed:

#!/bin/bash
# Search for API endpoint/base URL configuration and how it's determined by provider
rg -n --type=js --type=ts 'endpoint|baseUrl|BASE_URL|api\.anthropic|api\.deepseek' \
  -A2 -B2 | head -100

Repository: siteboon/claudecodeui

Length of output: 5764


🏁 Script executed:

#!/bin/bash
# Search for any DeepSeek-specific handling in backend
rg -n --type=js --type=ts -i 'deepseek' \
  server/ src/

Repository: siteboon/claudecodeui

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Check claude-sdk.js to see how it's initialized and whether it uses provider-specific config
head -80 server/claude-sdk.js

Repository: siteboon/claudecodeui

Length of output: 2539


🏁 Script executed:

#!/bin/bash
# Search for where backend routes based on provider to different SDK handlers
rg -n --type=js 'provider.*===.*claude|if.*provider' server/routes/ -A3 -B1

Repository: siteboon/claudecodeui

Length of output: 3477


🏁 Script executed:

#!/bin/bash
# Look for where queryClaude, queryGemini, queryCodex functions are called
rg -n --type=js 'queryClaude|queryGemini|queryCodex|spawnGemini' server/ -B5 -A2 | head -150

Repository: siteboon/claudecodeui

Length of output: 7769


🏁 Script executed:

#!/bin/bash
# Check agent.js to see the main dispatch logic
cat -n server/routes/agent.js | head -100

Repository: siteboon/claudecodeui

Length of output: 3921


🏁 Script executed:

#!/bin/bash
# Check queryClaudeSDK to see if there's any endpoint override based on model name
sed -n '200,250p' server/claude-sdk.js

Repository: siteboon/claudecodeui

Length of output: 1618


🏁 Script executed:

#!/bin/bash
# Search for any endpoint/baseUrl configuration in claude-sdk.js
rg -n 'baseUrl|BASE_URL|endpoint|api\.anthropic' server/claude-sdk.js -B2 -A2

Repository: siteboon/claudecodeui

Length of output: 309


DeepSeek models misclassified under CLAUDE_MODELS — will cause runtime failures

CLAUDE_MODELS is explicitly the "Claude (Anthropic) Models" section, and the backend's provider dispatch logic (server/routes/agent.js:942) uses hardcoded if/else on provider ID. When a user selects a DeepSeek model from the frontend, the provider remains "claude", triggering queryClaudeSDK() at runtime.

The queryClaudeSDK() function (server/claude-sdk.js:207) passes the model value directly to the Anthropic SDK without any endpoint override or model-based routing. This means a request with model="deepseek-v4-flash" will be sent to the Anthropic API, which will reject it as an invalid model, causing the request to fail.

These models require either a dedicated DEEPSEEK_MODELS constant with a corresponding PROVIDERS entry and backend handler, or routing logic that intercepts DeepSeek model names and redirects them to the correct endpoint and API client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/modelConstants.js` around lines 23 - 24, The DeepSeek entries were
incorrectly placed in CLAUDE_MODELS causing provider selection to stay "claude"
and triggering queryClaudeSDK with invalid models; fix by removing the DeepSeek
entries from CLAUDE_MODELS and adding a new DEEPSEEK_MODELS constant (with the
deepseek-v4-flash and deepseek-v4-pro values), add a corresponding PROVIDERS
entry for provider id "deepseek" and implement/route to a queryDeepSeekSDK
handler (or extend the provider dispatch logic) so the agent route handler
selects the "deepseek" provider instead of calling queryClaudeSDK for DeepSeek
models.

],

DEFAULT: "opus",
Expand Down
17 changes: 10 additions & 7 deletions src/components/code-editor/hooks/useCodeEditorDocument.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../../../utils/api';
import type { CodeEditorFile } from '../types/types';
import { isBinaryFile } from '../utils/binaryFile';
import { getFileCategory, type FileCategory } from '../utils/binaryFile';

type UseCodeEditorDocumentParams = {
file: CodeEditorFile;
Expand All @@ -22,7 +22,7 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [isBinary, setIsBinary] = useState(false);
const [fileCategory, setFileCategory] = useState<FileCategory>('text');
// `fileProjectId` is the DB primary key passed down from the editor sidebar;
// the fallback to `projectPath` preserves older callers that didn't yet
// propagate the identifier.
Expand All @@ -36,11 +36,13 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
const loadFileContent = async () => {
try {
setLoading(true);
setIsBinary(false);
setFileCategory('text');

// Check if file is binary by extension
if (isBinaryFile(file.name)) {
setIsBinary(true);
// Check file category by extension — previewable types (image/pdf)
// are handled by dedicated viewer components in CodeEditor.
const category = getFileCategory(file.name);
if (category !== 'text') {
setFileCategory(category);
setLoading(false);
return;
}
Expand Down Expand Up @@ -133,7 +135,8 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
saving,
saveSuccess,
saveError,
isBinary,
fileCategory,
isBinary: fileCategory === 'binary',
handleSave,
handleDownload,
};
Expand Down
38 changes: 30 additions & 8 deletions src/components/code-editor/utils/binaryFile.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
// Binary file extensions (images are handled by ImageViewer, not here)
const IMAGE_EXTENSIONS = [
'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp',
];

const PDF_EXTENSIONS = ['pdf'];

// Truly unviewable binary formats (no in-browser preview possible)
const BINARY_EXTENSIONS = [
// Archives
'zip', 'tar', 'gz', 'rar', '7z', 'bz2', 'xz',
// Executables
'exe', 'dll', 'so', 'dylib', 'app', 'dmg', 'msi',
// Media
// Media (audio/video — could be previewable, but not in scope yet)
'mp3', 'mp4', 'wav', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'm4a', 'ogg',
// Documents
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp',
// Documents (non-PDF)
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp',
// Fonts
'ttf', 'otf', 'woff', 'woff2', 'eot',
// Database
'db', 'sqlite', 'sqlite3',
// Other binary
'bin', 'dat', 'iso', 'img', 'class', 'jar', 'war', 'pyc', 'pyo'
'bin', 'dat', 'iso', 'img', 'class', 'jar', 'war', 'pyc', 'pyo',
];

export const isBinaryFile = (filename: string): boolean => {
const ext = filename.split('.').pop()?.toLowerCase();
return BINARY_EXTENSIONS.includes(ext ?? '');
const getExt = (filename: string): string =>
(filename.split('.').pop() ?? '').toLowerCase();

export const isImageFile = (filename: string): boolean =>
IMAGE_EXTENSIONS.includes(getExt(filename));

export const isPdfFile = (filename: string): boolean =>
PDF_EXTENSIONS.includes(getExt(filename));

export const isBinaryFile = (filename: string): boolean =>
BINARY_EXTENSIONS.includes(getExt(filename));

export type FileCategory = 'text' | 'image' | 'pdf' | 'binary';

export const getFileCategory = (filename: string): FileCategory => {
if (isImageFile(filename)) return 'image';
if (isPdfFile(filename)) return 'pdf';
if (isBinaryFile(filename)) return 'binary';
return 'text';
};
35 changes: 35 additions & 0 deletions src/components/code-editor/view/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import CodeEditorHeader from './subcomponents/CodeEditorHeader';
import CodeEditorLoadingState from './subcomponents/CodeEditorLoadingState';
import CodeEditorSurface from './subcomponents/CodeEditorSurface';
import CodeEditorBinaryFile from './subcomponents/CodeEditorBinaryFile';
import CodeEditorImagePreview from './subcomponents/CodeEditorImagePreview';
import CodeEditorPdfPreview from './subcomponents/CodeEditorPdfPreview';

type CodeEditorProps = {
file: CodeEditorFile;
Expand Down Expand Up @@ -57,6 +59,7 @@ export default function CodeEditor({
saving,
saveSuccess,
saveError,
fileCategory,
isBinary,
handleSave,
handleDownload,
Expand Down Expand Up @@ -152,6 +155,12 @@ export default function CodeEditor({
dependency: content,
});

// Ensure projectId is populated for preview API requests.
const previewFile = useMemo<CodeEditorFile>(
() => ({ ...file, projectId: file.projectId ?? projectPath ?? '' }),
[file, projectPath],
);

if (loading) {
return (
<CodeEditorLoadingState
Expand All @@ -162,6 +171,32 @@ export default function CodeEditor({
);
}

// Image preview
if (fileCategory === 'image') {
return (
<CodeEditorImagePreview
file={previewFile}
isSidebar={isSidebar}
isFullscreen={isFullscreen}
onClose={onClose}
onToggleFullscreen={() => setIsFullscreen((prev) => !prev)}
/>
);
}

// PDF preview
if (fileCategory === 'pdf') {
return (
<CodeEditorPdfPreview
file={previewFile}
isSidebar={isSidebar}
isFullscreen={isFullscreen}
onClose={onClose}
onToggleFullscreen={() => setIsFullscreen((prev) => !prev)}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

// Binary file display
if (isBinary) {
return (
Expand Down
Loading