From b3498932e1375af23cd04e54e94ca9ae7726046b Mon Sep 17 00:00:00 2001 From: simos Date: Mon, 15 Sep 2025 15:37:59 +0000 Subject: [PATCH 1/2] Feat: Add login to claude code and cursor CLI through the settings Feat: Group sessions based on first uuid --- server/projects.js | 87 ++++++----- src/App.jsx | 21 +-- src/components/ChatInterface.jsx | 4 +- src/components/MainContent.jsx | 9 +- .../{ToolsSettings.jsx => Settings.jsx} | 141 ++++++++++++++++-- src/components/Sidebar.jsx | 10 +- src/components/StandaloneShell.jsx | 106 +++++++++++++ 7 files changed, 311 insertions(+), 67 deletions(-) rename src/components/{ToolsSettings.jsx => Settings.jsx} (93%) create mode 100644 src/components/StandaloneShell.jsx diff --git a/server/projects.js b/server/projects.js index 99e659c..3f0b2b5 100755 --- a/server/projects.js +++ b/server/projects.js @@ -572,48 +572,61 @@ async function getSessions(projectName, limit = 5, offset = 0) { } }); - // Detect session continuations using leafUuid - const sessionContinuations = new Map(); - let pendingContinuationInfo = null; - + // Group sessions by first user message ID + const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] } + const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId + + // Find the first user message for each session allEntries.forEach(entry => { - // Summary entries without sessionId indicate a session continuation - if (entry.type === 'summary' && !entry.sessionId && (entry.leafUuid || entry.leafUUID)) { - pendingContinuationInfo = { - leafUuid: entry.leafUuid || entry.leafUUID, - summary: entry.summary || 'Continued Session' - }; - return; - } - - if (entry.sessionId) { - const session = allSessions.get(entry.sessionId); - - // Apply pending continuation info - if (session && pendingContinuationInfo) { - const previousSession = uuidToSessionMap.get(pendingContinuationInfo.leafUuid); - if (previousSession) { - session.summary = pendingContinuationInfo.summary; - sessionContinuations.set(entry.sessionId, previousSession); - } - pendingContinuationInfo = null; - } - - // Handle summary entries with sessionId that have leafUuid - if (entry.type === 'summary' && (entry.leafUuid || entry.leafUUID)) { - const leafUuid = entry.leafUuid || entry.leafUUID; - const previousSession = uuidToSessionMap.get(leafUuid); - if (previousSession && session) { - sessionContinuations.set(entry.sessionId, previousSession); + if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) { + // This is a first user message in a session (parentUuid is null) + const firstUserMsgId = entry.uuid; + + if (!sessionToFirstUserMsgId.has(entry.sessionId)) { + sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId); + + const session = allSessions.get(entry.sessionId); + if (session) { + if (!sessionGroups.has(firstUserMsgId)) { + sessionGroups.set(firstUserMsgId, { + latestSession: session, + allSessions: [session] + }); + } else { + const group = sessionGroups.get(firstUserMsgId); + group.allSessions.push(session); + + // Update latest session if this one is more recent + if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) { + group.latestSession = session; + } + } } } } }); - - // Filter out continued sessions - only show the latest in each timeline - const continuedSessions = new Set(sessionContinuations.values()); - const visibleSessions = Array.from(allSessions.values()) - .filter(session => !continuedSessions.has(session.id)) + + // Collect all sessions that don't belong to any group (standalone sessions) + const groupedSessionIds = new Set(); + sessionGroups.forEach(group => { + group.allSessions.forEach(session => groupedSessionIds.add(session.id)); + }); + + const standaloneSessionsArray = Array.from(allSessions.values()) + .filter(session => !groupedSessionIds.has(session.id)); + + // Combine grouped sessions (only show latest from each group) + standalone sessions + const latestFromGroups = Array.from(sessionGroups.values()).map(group => { + const session = { ...group.latestSession }; + // Add metadata about grouping + if (group.allSessions.length > 1) { + session.isGrouped = true; + session.groupSize = group.allSessions.length; + session.groupSessions = group.allSessions.map(s => s.id); + } + return session; + }); + const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray] .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity)); const total = visibleSessions.length; diff --git a/src/App.jsx b/src/App.jsx index ec0278d..ccc99ef 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,7 +23,7 @@ import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from ' import Sidebar from './components/Sidebar'; import MainContent from './components/MainContent'; import MobileNav from './components/MobileNav'; -import ToolsSettings from './components/ToolsSettings'; +import Settings from './components/Settings'; import QuickSettingsPanel from './components/QuickSettingsPanel'; import { ThemeProvider } from './contexts/ThemeContext'; @@ -52,7 +52,7 @@ function AppContent() { const [sidebarOpen, setSidebarOpen] = useState(false); const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isInputFocused, setIsInputFocused] = useState(false); - const [showToolsSettings, setShowToolsSettings] = useState(false); + const [showSettings, setShowSettings] = useState(false); const [showQuickSettings, setShowQuickSettings] = useState(false); const [autoExpandTools, setAutoExpandTools] = useState(() => { const saved = localStorage.getItem('autoExpandTools'); @@ -556,7 +556,7 @@ function AppContent() { onProjectDelete={handleProjectDelete} isLoading={isLoadingProjects} onRefresh={handleSidebarRefresh} - onShowSettings={() => setShowToolsSettings(true)} + onShowSettings={() => setShowSettings(true)} updateAvailable={updateAvailable} latestVersion={latestVersion} currentVersion={currentVersion} @@ -584,9 +584,10 @@ function AppContent() { }} />
e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} > @@ -601,7 +602,7 @@ function AppContent() { onProjectDelete={handleProjectDelete} isLoading={isLoadingProjects} onRefresh={handleSidebarRefresh} - onShowSettings={() => setShowToolsSettings(true)} + onShowSettings={() => setShowSettings(true)} updateAvailable={updateAvailable} latestVersion={latestVersion} currentVersion={currentVersion} @@ -629,7 +630,7 @@ function AppContent() { onSessionInactive={markSessionAsInactive} onReplaceTemporarySession={replaceTemporarySession} onNavigateToSession={(sessionId) => navigate(`/session/${sessionId}`)} - onShowSettings={() => setShowToolsSettings(true)} + onShowSettings={() => setShowSettings(true)} autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} autoScrollToBottom={autoScrollToBottom} @@ -674,10 +675,10 @@ function AppContent() { /> )} - {/* Tools Settings Modal */} - setShowToolsSettings(false)} + {/* Settings Modal */} + setShowSettings(false)} projects={projects} /> diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index e38efcd..f7a15ba 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -2017,6 +2017,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // When resuming a session, Claude CLI creates a new session instead of resuming. // We detect this by checking for system/init messages with session_id that differs // from our current session. When found, we need to switch the user to the new session. + // This works exactly like new session detection - preserve messages during navigation. if (latestMessage.data.type === 'system' && latestMessage.data.subtype === 'init' && latestMessage.data.session_id && @@ -2029,6 +2030,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }); // Mark this as a system-initiated session change to preserve messages + // This works exactly like new session init - messages stay visible during navigation setIsSystemSessionChange(true); // Switch to the new session using React Router navigation @@ -2743,7 +2745,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Get tools settings from localStorage based on provider const getToolsSettings = () => { try { - const settingsKey = provider === 'cursor' ? 'cursor-tools-settings' : 'claude-tools-settings'; + const settingsKey = provider === 'cursor' ? 'cursor-tools-settings' : 'claude-settings'; const savedSettings = safeLocalStorage.getItem(settingsKey); if (savedSettings) { return JSON.parse(savedSettings); diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx index 5bbbc7a..b915198 100644 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -15,7 +15,7 @@ import React, { useState, useEffect } from 'react'; import ChatInterface from './ChatInterface'; import FileTree from './FileTree'; import CodeEditor from './CodeEditor'; -import Shell from './Shell'; +import StandaloneShell from './StandaloneShell'; import GitPanel from './GitPanel'; import ErrorBoundary from './ErrorBoundary'; import ClaudeLogo from './ClaudeLogo'; @@ -426,10 +426,11 @@ function MainContent({
-
diff --git a/src/components/ToolsSettings.jsx b/src/components/Settings.jsx similarity index 93% rename from src/components/ToolsSettings.jsx rename to src/components/Settings.jsx index 2103abb..15884fe 100644 --- a/src/components/ToolsSettings.jsx +++ b/src/components/Settings.jsx @@ -2,11 +2,14 @@ import { useState, useEffect } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Badge } from './ui/badge'; -import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen } from 'lucide-react'; +import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn } from 'lucide-react'; import { useTheme } from '../contexts/ThemeContext'; import { useTasksSettings } from '../contexts/TasksSettingsContext'; +import StandaloneShell from './StandaloneShell'; +import ClaudeLogo from './ClaudeLogo'; +import CursorLogo from './CursorLogo'; -function ToolsSettings({ isOpen, onClose, projects = [] }) { +function Settings({ isOpen, onClose, projects = [] }) { const { isDarkMode, toggleDarkMode } = useTheme(); const { tasksEnabled, @@ -59,6 +62,11 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { const [newCursorCommand, setNewCursorCommand] = useState(''); const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState(''); const [cursorMcpServers, setCursorMcpServers] = useState([]); + + // Login modal states + const [showLoginModal, setShowLoginModal] = useState(false); + const [loginProvider, setLoginProvider] = useState(''); // 'claude' or 'cursor' + const [selectedProject, setSelectedProject] = useState(null); // Common tool patterns for Claude const commonTools = [ 'Bash(git log:*)', @@ -325,7 +333,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { try { // Load Claude settings from localStorage - const savedSettings = localStorage.getItem('claude-tools-settings'); + const savedSettings = localStorage.getItem('claude-settings'); if (savedSettings) { const settings = JSON.parse(savedSettings); @@ -371,6 +379,26 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { } }; + // Login handlers + const handleClaudeLogin = () => { + setLoginProvider('claude'); + setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() }); + setShowLoginModal(true); + }; + + const handleCursorLogin = () => { + setLoginProvider('cursor'); + setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() }); + setShowLoginModal(true); + }; + + const handleLoginComplete = (exitCode) => { + if (exitCode === 0) { + // Login successful - could show a success message here + } + setShowLoginModal(false); + }; + const saveSettings = () => { setIsSaving(true); setSaveStatus(null); @@ -394,7 +422,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { }; // Save to localStorage - localStorage.setItem('claude-tools-settings', JSON.stringify(claudeSettings)); + localStorage.setItem('claude-settings', JSON.stringify(claudeSettings)); localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings)); setSaveStatus('success'); @@ -600,7 +628,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
- +

Settings

@@ -739,7 +767,10 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { : 'border-transparent text-muted-foreground hover:text-foreground' }`} > - Claude Tools +
+ + Claude +
- {/* Claude Tools Content */} + {/* Claude Content */} {toolsProvider === 'claude' && (
@@ -786,6 +820,36 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
+ {/* Claude Login */} +
+
+ +

+ Authentication +

+
+
+
+
+
+ Claude CLI Login +
+
+ Sign in to your Claude account to enable AI features +
+
+ +
+
+
+ {/* Allowed Tools */}
@@ -1484,7 +1548,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
)} - {/* Cursor Tools Content */} + {/* Cursor Content */} {toolsProvider === 'cursor' && (
@@ -1516,6 +1580,36 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
+ {/* Cursor Login */} +
+
+ +

+ Authentication +

+
+
+
+
+
+ Cursor CLI Login +
+
+ Sign in to your Cursor account to enable AI features +
+
+ +
+
+
+ {/* Allowed Shell Commands */}
@@ -1895,8 +1989,35 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
+ + {/* Login Modal */} + {showLoginModal && ( +
+
+
+

+ {loginProvider === 'claude' ? 'Claude CLI Login' : 'Cursor CLI Login'} +

+ +
+
+ +
+
+
+ )} ); } -export default ToolsSettings; +export default Settings; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 71a6288..3c23e93 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -145,7 +145,7 @@ function Sidebar({ useEffect(() => { const loadSortOrder = () => { try { - const savedSettings = localStorage.getItem('claude-tools-settings'); + const savedSettings = localStorage.getItem('claude-settings'); if (savedSettings) { const settings = JSON.parse(savedSettings); setProjectSortOrder(settings.projectSortOrder || 'name'); @@ -160,7 +160,7 @@ function Sidebar({ // Listen for storage changes const handleStorageChange = (e) => { - if (e.key === 'claude-tools-settings') { + if (e.key === 'claude-settings') { loadSortOrder(); } }; @@ -766,8 +766,8 @@ function Sidebar({ {/* Mobile Form - Simple Overlay */} -
-
+
+
@@ -1647,7 +1647,7 @@ function Sidebar({ onClick={onShowSettings} > - Tools Settings + Settings
diff --git a/src/components/StandaloneShell.jsx b/src/components/StandaloneShell.jsx new file mode 100644 index 0000000..6ddcbe7 --- /dev/null +++ b/src/components/StandaloneShell.jsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react'; +import Shell from './Shell.jsx'; + +/** + * Generic Shell wrapper that can be used in tabs, modals, and other contexts. + * Provides a flexible API for both standalone and session-based usage. + * + * @param {Object} project - Project object with name, fullPath/path, displayName + * @param {Object} session - Session object (optional, for tab usage) + * @param {string} command - Initial command to run (optional) + * @param {boolean} isActive - Whether the shell is active (for tab usage, default: true) + * @param {boolean} isPlainShell - Use plain shell mode vs Claude CLI (default: auto-detect) + * @param {boolean} autoConnect - Whether to auto-connect when mounted (default: true) + * @param {function} onComplete - Callback when process completes (receives exitCode) + * @param {function} onClose - Callback for close button (optional) + * @param {string} title - Custom header title (optional) + * @param {string} className - Additional CSS classes + * @param {boolean} showHeader - Whether to show custom header (default: true) + * @param {boolean} compact - Use compact layout (default: false) + */ +function StandaloneShell({ + project, + session = null, + command = null, + isActive = true, + isPlainShell = null, // Auto-detect: true if command provided, false if session provided + autoConnect = true, + onComplete = null, + onClose = null, + title = null, + className = "", + showHeader = true, + compact = false +}) { + const [isCompleted, setIsCompleted] = useState(false); + + // Auto-detect isPlainShell based on props + const shouldUsePlainShell = isPlainShell !== null ? isPlainShell : (command !== null); + + // Handle process completion + const handleProcessComplete = (exitCode) => { + setIsCompleted(true); + if (onComplete) { + onComplete(exitCode); + } + }; + + if (!project) { + return ( +
+
+
+ + + +
+

No Project Selected

+

A project is required to open a shell

+
+
+ ); + } + + return ( +
+ {/* Optional custom header */} + {showHeader && title && ( +
+
+
+

{title}

+ {isCompleted && ( + (Completed) + )} +
+ {onClose && ( + + )} +
+
+ )} + + {/* Shell component wrapper */} +
+ +
+
+ ); +} + +export default StandaloneShell; \ No newline at end of file From 34583a7c7b1f005e1730353f00107524c2b4e4cc Mon Sep 17 00:00:00 2001 From: simos Date: Mon, 15 Sep 2025 15:38:42 +0000 Subject: [PATCH 2/2] Bump package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f88599a..53d74b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-ui", - "version": "1.8.0", + "version": "1.8.1", "description": "A web-based UI for Claude Code CLI", "type": "module", "main": "server/index.js",