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",
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 6c0bdae..82bb91a 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');
@@ -585,7 +585,7 @@ function AppContent() {
onProjectDelete={handleProjectDelete}
isLoading={isLoadingProjects}
onRefresh={handleSidebarRefresh}
- onShowSettings={() => setShowToolsSettings(true)}
+ onShowSettings={() => setShowSettings(true)}
updateAvailable={updateAvailable}
latestVersion={latestVersion}
currentVersion={currentVersion}
@@ -615,9 +615,10 @@ function AppContent() {
}}
/>
e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
>
@@ -632,7 +633,7 @@ function AppContent() {
onProjectDelete={handleProjectDelete}
isLoading={isLoadingProjects}
onRefresh={handleSidebarRefresh}
- onShowSettings={() => setShowToolsSettings(true)}
+ onShowSettings={() => setShowSettings(true)}
updateAvailable={updateAvailable}
latestVersion={latestVersion}
currentVersion={currentVersion}
@@ -663,7 +664,7 @@ function AppContent() {
onSessionInactive={markSessionAsInactive}
onReplaceTemporarySession={replaceTemporarySession}
onNavigateToSession={(sessionId) => navigate(`/session/${sessionId}`)}
- onShowSettings={() => setShowToolsSettings(true)}
+ onShowSettings={() => setShowSettings(true)}
autoExpandTools={autoExpandTools}
showRawParameters={showRawParameters}
autoScrollToBottom={autoScrollToBottom}
@@ -708,10 +709,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 b6bd04a..645f4b4 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';
@@ -436,10 +436,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 4a118ba..ee6ae29 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -147,7 +147,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');
@@ -162,7 +162,7 @@ function Sidebar({
// Listen for storage changes
const handleStorageChange = (e) => {
- if (e.key === 'claude-tools-settings') {
+ if (e.key === 'claude-settings') {
loadSortOrder();
}
};
@@ -774,8 +774,8 @@ function Sidebar({
{/* Mobile Form - Simple Overlay */}
-
-
+
+
@@ -1655,7 +1655,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