Skip to content

Commit ed653ee

Browse files
authored
Merge branch 'main' into feat/rtl-support
2 parents 2ff37c4 + 7a17307 commit ed653ee

7 files changed

Lines changed: 125 additions & 36 deletions

File tree

server/claude-sdk.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function mapCliOptionsToSDK(options = {}) {
5757

5858
// Add plan mode default tools
5959
if (permissionMode === 'plan') {
60-
const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
60+
const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
6161
for (const tool of planModeTools) {
6262
if (!allowedTools.includes(tool)) {
6363
allowedTools.push(tool);
@@ -76,8 +76,9 @@ function mapCliOptionsToSDK(options = {}) {
7676
}
7777

7878
// Map model (default to sonnet)
79-
// Map model (default to sonnet)
79+
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
8080
sdkOptions.model = options.model || 'sonnet';
81+
console.log(`🤖 Using model: ${sdkOptions.model}`);
8182

8283
// Map system prompt configuration
8384
sdkOptions.systemPrompt = {

server/index.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -827,9 +827,31 @@ function handleShellConnection(ws) {
827827
const initialCommand = data.initialCommand;
828828
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
829829

830-
ptySessionKey = `${projectPath}_${sessionId || 'default'}`;
830+
// Login commands (Claude/Cursor auth) should never reuse cached sessions
831+
const isLoginCommand = initialCommand && (
832+
initialCommand.includes('setup-token') ||
833+
initialCommand.includes('cursor-agent login') ||
834+
initialCommand.includes('auth login')
835+
);
836+
837+
// Include command hash in session key so different commands get separate sessions
838+
const commandSuffix = isPlainShell && initialCommand
839+
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
840+
: '';
841+
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
842+
843+
// Kill any existing login session before starting fresh
844+
if (isLoginCommand) {
845+
const oldSession = ptySessionsMap.get(ptySessionKey);
846+
if (oldSession) {
847+
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
848+
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
849+
if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
850+
ptySessionsMap.delete(ptySessionKey);
851+
}
852+
}
831853

832-
const existingSession = ptySessionsMap.get(ptySessionKey);
854+
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
833855
if (existingSession) {
834856
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
835857
shellProcess = existingSession.pty;

server/routes/agent.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,43 @@ import path from 'path';
44
import os from 'os';
55
import { promises as fs } from 'fs';
66
import crypto from 'crypto';
7-
import { apiKeysDb, githubTokensDb } from '../database/db.js';
7+
import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
88
import { addProjectManually } from '../projects.js';
99
import { queryClaudeSDK } from '../claude-sdk.js';
1010
import { spawnCursor } from '../cursor-cli.js';
1111
import { Octokit } from '@octokit/rest';
1212

1313
const router = express.Router();
1414

15-
// Middleware to validate API key for external requests
15+
/**
16+
* Middleware to authenticate agent API requests.
17+
*
18+
* Supports two authentication modes:
19+
* 1. Platform mode (VITE_IS_PLATFORM=true): For managed/hosted deployments where
20+
* authentication is handled by an external proxy. Requests are trusted and
21+
* the default user context is used.
22+
*
23+
* 2. API key mode (default): For self-hosted deployments where users authenticate
24+
* via API keys created in the UI. Keys are validated against the local database.
25+
*/
1626
const validateExternalApiKey = (req, res, next) => {
27+
// Platform mode: Authentication is handled externally (e.g., by a proxy layer).
28+
// Trust the request and use the default user context.
29+
if (process.env.VITE_IS_PLATFORM === 'true') {
30+
try {
31+
const user = userDb.getFirstUser();
32+
if (!user) {
33+
return res.status(500).json({ error: 'Platform mode: No user found in database' });
34+
}
35+
req.user = user;
36+
return next();
37+
} catch (error) {
38+
console.error('Platform mode error:', error);
39+
return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
40+
}
41+
}
42+
43+
// Self-hosted mode: Validate API key from header or query parameter
1744
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
1845

1946
if (!apiKey) {

src/App.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ function AppContent() {
8989
window.navigator.standalone ||
9090
document.referrer.includes('android-app://');
9191
setIsPWA(isStandalone);
92-
92+
document.addEventListener('touchstart', {});
93+
9394
// Add class to html and body for CSS targeting
9495
if (isStandalone) {
9596
document.documentElement.classList.add('pwa-mode');
@@ -966,4 +967,4 @@ function App() {
966967
);
967968
}
968969

969-
export default App;
970+
export default App;

src/components/ChatInterface.jsx

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,7 +1648,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
16481648
//
16491649
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
16501650
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onSessionProcessing, onSessionNotProcessing, processingSessions, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, onTaskClick, onShowAllTasks }) {
1651-
const { tasksEnabled } = useTasksSettings();
1651+
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
16521652
const [input, setInput] = useState(() => {
16531653
if (typeof window !== 'undefined' && selectedProject) {
16541654
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
@@ -1716,6 +1716,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
17161716
const [cursorModel, setCursorModel] = useState(() => {
17171717
return localStorage.getItem('cursor-model') || 'gpt-5';
17181718
});
1719+
const [claudeModel, setClaudeModel] = useState(() => {
1720+
return localStorage.getItem('claude-model') || 'sonnet';
1721+
});
17191722
// Load permission mode for the current session
17201723
useEffect(() => {
17211724
if (selectedSession?.id) {
@@ -2047,7 +2050,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
20472050
projectName: selectedProject.name,
20482051
sessionId: currentSessionId,
20492052
provider,
2050-
model: provider === 'cursor' ? cursorModel : 'claude-sonnet-4.5',
2053+
model: provider === 'cursor' ? cursorModel : claudeModel,
20512054
tokenUsage: tokenBudget
20522055
};
20532056

@@ -3863,6 +3866,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
38633866
resume: !!currentSessionId,
38643867
toolsSettings: toolsSettings,
38653868
permissionMode: permissionMode,
3869+
model: claudeModel,
38663870
images: uploadedImages // Pass images to backend
38673871
}
38683872
});
@@ -3883,7 +3887,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
38833887
if (selectedProject) {
38843888
safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`);
38853889
}
3886-
}, [input, isLoading, selectedProject, attachedImages, currentSessionId, selectedSession, provider, permissionMode, onSessionActive, cursorModel, sendMessage, setInput, setAttachedImages, setUploadingImages, setImageErrors, setIsTextareaExpanded, textareaRef, setChatMessages, setIsLoading, setCanAbortSession, setClaudeStatus, setIsUserScrolledUp, scrollToBottom]);
3890+
}, [input, isLoading, selectedProject, attachedImages, currentSessionId, selectedSession, provider, permissionMode, onSessionActive, cursorModel, claudeModel, sendMessage, setInput, setAttachedImages, setUploadingImages, setImageErrors, setIsTextareaExpanded, textareaRef, setChatMessages, setIsLoading, setCanAbortSession, setClaudeStatus, setIsUserScrolledUp, scrollToBottom]);
38873891

38883892
// Store handleSubmit in ref so handleCustomCommand can access it
38893893
useEffect(() => {
@@ -4282,40 +4286,72 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
42824286
</button>
42834287
</div>
42844288

4285-
{/* Model Selection for Cursor - Always reserve space to prevent jumping */}
4286-
<div className={`mb-6 transition-opacity duration-200 ${provider === 'cursor' ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
4289+
{/* Model Selection - Always reserve space to prevent jumping */}
4290+
<div className={`mb-6 transition-opacity duration-200 ${provider ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
42874291
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
4288-
{provider === 'cursor' ? 'Select Model' : '\u00A0'}
4292+
Select Model
42894293
</label>
4290-
<select
4291-
value={cursorModel}
4292-
onChange={(e) => {
4293-
const newModel = e.target.value;
4294-
setCursorModel(newModel);
4295-
localStorage.setItem('cursor-model', newModel);
4296-
}}
4297-
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]"
4298-
disabled={provider !== 'cursor'}
4299-
>
4300-
<option value="gpt-5">GPT-5</option>
4301-
<option value="sonnet-4">Sonnet-4</option>
4302-
<option value="opus-4.1">Opus 4.1</option>
4303-
</select>
4294+
{provider === 'claude' ? (
4295+
<select
4296+
value={claudeModel}
4297+
onChange={(e) => {
4298+
const newModel = e.target.value;
4299+
setClaudeModel(newModel);
4300+
localStorage.setItem('claude-model', newModel);
4301+
}}
4302+
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]"
4303+
>
4304+
<option value="sonnet">Sonnet</option>
4305+
<option value="opus">Opus</option>
4306+
<option value="haiku">Haiku</option>
4307+
<option value="opusplan">Opus Plan</option>
4308+
<option value="sonnet[1m]">Sonnet [1M]</option>
4309+
</select>
4310+
) : (
4311+
<select
4312+
value={cursorModel}
4313+
onChange={(e) => {
4314+
const newModel = e.target.value;
4315+
setCursorModel(newModel);
4316+
localStorage.setItem('cursor-model', newModel);
4317+
}}
4318+
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]"
4319+
disabled={provider !== 'cursor'}
4320+
>
4321+
<option value="gpt-5.2-high">GPT-5.2 High</option>
4322+
<option value="gemini-3-pro">Gemini 3 Pro</option>
4323+
<option value="opus-4.5-thinking">Claude 4.5 Opus (Thinking)</option>
4324+
<option value="gpt-5.2">GPT-5.2</option>
4325+
<option value="gpt-5.1">GPT-5.1</option>
4326+
<option value="gpt-5.1-high">GPT-5.1 High</option>
4327+
<option value="composer-1">Composer 1</option>
4328+
<option value="auto">Auto</option>
4329+
<option value="sonnet-4.5">Claude 4.5 Sonnet</option>
4330+
<option value="sonnet-4.5-thinking">Claude 4.5 Sonnet (Thinking)</option>
4331+
<option value="opus-4.5">Claude 4.5 Opus</option>
4332+
<option value="gpt-5.1-codex">GPT-5.1 Codex</option>
4333+
<option value="gpt-5.1-codex-high">GPT-5.1 Codex High</option>
4334+
<option value="gpt-5.1-codex-max">GPT-5.1 Codex Max</option>
4335+
<option value="gpt-5.1-codex-max-high">GPT-5.1 Codex Max High</option>
4336+
<option value="opus-4.1">Claude 4.1 Opus</option>
4337+
<option value="grok">Grok</option>
4338+
</select>
4339+
)}
43044340
</div>
43054341

43064342
<p className="text-sm text-gray-500 dark:text-gray-400">
4307-
{provider === 'claude'
4308-
? 'Ready to use Claude AI. Start typing your message below.'
4343+
{provider === 'claude'
4344+
? `Ready to use Claude with ${claudeModel}. Start typing your message below.`
43094345
: provider === 'cursor'
43104346
? `Ready to use Cursor with ${cursorModel}. Start typing your message below.`
43114347
: 'Select a provider above to begin'
43124348
}
43134349
</p>
43144350

4315-
{/* Show NextTaskBanner when provider is selected and ready */}
4316-
{provider && tasksEnabled && (
4351+
{/* Show NextTaskBanner when provider is selected and ready, only if TaskMaster is installed */}
4352+
{provider && tasksEnabled && isTaskMasterInstalled && (
43174353
<div className="mt-4 px-4 sm:px-0">
4318-
<NextTaskBanner
4354+
<NextTaskBanner
43194355
onStartTask={() => setInput('Start the next task')}
43204356
onShowAllTasks={onShowAllTasks}
43214357
/>
@@ -4330,10 +4366,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
43304366
Ask questions about your code, request changes, or get help with development tasks
43314367
</p>
43324368

4333-
{/* Show NextTaskBanner for existing sessions too */}
4334-
{tasksEnabled && (
4369+
{/* Show NextTaskBanner for existing sessions too, only if TaskMaster is installed */}
4370+
{tasksEnabled && isTaskMasterInstalled && (
43354371
<div className="mt-4 px-4 sm:px-0">
4336-
<NextTaskBanner
4372+
<NextTaskBanner
43374373
onStartTask={() => setInput('Start the next task')}
43384374
onShowAllTasks={onShowAllTasks}
43394375
/>

src/components/Onboarding.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ const Onboarding = ({ onComplete }) => {
575575
{/* Login Modal */}
576576
{showLoginModal && (
577577
<LoginModal
578+
key={loginProvider}
578579
isOpen={showLoginModal}
579580
onClose={() => setShowLoginModal(false)}
580581
provider={loginProvider}

src/components/Settings.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
21492149

21502150
{/* Login Modal */}
21512151
<LoginModal
2152+
key={loginProvider}
21522153
isOpen={showLoginModal}
21532154
onClose={() => setShowLoginModal(false)}
21542155
provider={loginProvider}

0 commit comments

Comments
 (0)