diff --git a/packages/ui/src/components/chat/MobileSessionStatusBar.tsx b/packages/ui/src/components/chat/MobileSessionStatusBar.tsx index a5dea54b..27709da4 100644 --- a/packages/ui/src/components/chat/MobileSessionStatusBar.tsx +++ b/packages/ui/src/components/chat/MobileSessionStatusBar.tsx @@ -302,27 +302,60 @@ function SessionItem({ getSessionTitle, onClick, onDoubleClick, - needsAttention + needsAttention, + isEditing = false, + editingTitle = '', + onEditingTitleChange, + onEditSave, + onEditCancel, }: { session: SessionWithStatus; isCurrent: boolean; getSessionAgentName: (s: Session) => string; getSessionTitle: (s: Session) => string; onClick: () => void; - onDoubleClick?: () => void; + onDoubleClick?: (sessionId: string, sessionTitle: string) => void; needsAttention: (sessionId: string) => boolean; + isEditing?: boolean; + editingTitle?: string; + onEditingTitleChange?: (value: string) => void; + onEditSave?: () => void; + onEditCancel?: () => void; }) { const agentName = getSessionAgentName(session); const agentColor = getAgentColor(agentName); const extraCount = (session._runningChildrenCount || 0) + (session._statusType !== 'idle' ? 1 : 0) - 1 - (session._childIndicators?.length || 0); + const sessionTitle = getSessionTitle(session); + const editInputRef = React.useRef(null); + const editCancelledRef = React.useRef(false); + + React.useEffect(() => { + if (isEditing) { + editCancelledRef.current = false; + const node = editInputRef.current; + if (node) { + node.focus(); + node.select(); + } + } + }, [isEditing]); return ( - + ); } @@ -396,6 +465,7 @@ function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsa } interface SessionStatusHeaderProps { + currentSessionId?: string | null; currentSessionTitle: string; currentProjectLabel?: string; currentProjectIcon?: string | null; @@ -405,9 +475,16 @@ interface SessionStatusHeaderProps { onToggle: () => void; isExpanded?: boolean; childIndicators?: Array<{ session: Session; isRunning: boolean }>; + isEditing?: boolean; + editingTitle?: string; + onTitleDoubleClick?: (sessionId: string, sessionTitle: string) => void; + onEditingTitleChange?: (value: string) => void; + onEditSave?: () => void; + onEditCancel?: () => void; } function SessionStatusHeader({ + currentSessionId, currentSessionTitle, currentProjectLabel, currentProjectIcon, @@ -416,22 +493,49 @@ function SessionStatusHeader({ currentProjectColor, onToggle, isExpanded = false, - childIndicators = [] + childIndicators = [], + isEditing = false, + editingTitle = '', + onTitleDoubleClick, + onEditingTitleChange, + onEditSave, + onEditCancel, }: SessionStatusHeaderProps) { const [imageFailed, setImageFailed] = React.useState(false); const projectIconName = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null; const imageUrl = !imageFailed ? currentProjectIconImageUrl : null; const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null; const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0; + const editInputRef = React.useRef(null); + const editCancelledRef = React.useRef(false); React.useEffect(() => { setImageFailed(false); }, [currentProjectIconImageUrl]); + React.useEffect(() => { + if (isEditing) { + editCancelledRef.current = false; + const node = editInputRef.current; + if (node) { + node.focus(); + node.select(); + } + } + }, [isEditing]); + return ( - + ); } @@ -1142,6 +1290,7 @@ function ProjectBar({ function CollapsedView({ runningCount, unreadCount, + currentSessionId, currentSessionTitle, currentProjectLabel, currentProjectIcon, @@ -1152,9 +1301,16 @@ function CollapsedView({ onNewSession, contextUsage, childIndicators = [], + editingSessionId = null, + editingTitle = '', + onTitleDoubleClick, + onEditingTitleChange, + onEditSave, + onEditCancel, }: { runningCount: number; unreadCount: number; + currentSessionId?: string | null; currentSessionTitle: string; currentProjectLabel?: string; currentProjectIcon?: string | null; @@ -1165,6 +1321,12 @@ function CollapsedView({ onNewSession: () => void; contextUsage: SessionContextUsage | null; childIndicators?: Array<{ session: Session; isRunning: boolean }>; + editingSessionId?: string | null; + editingTitle?: string; + onTitleDoubleClick?: (sessionId: string, sessionTitle: string) => void; + onEditingTitleChange?: (value: string) => void; + onEditSave?: () => void; + onEditCancel?: () => void; }) { const { t } = useI18n(); const { handleTouchStart, handleTouchMove, handleTouchEnd } = useDrawerSwipe(); @@ -1182,6 +1344,7 @@ function CollapsedView({ >
@@ -1245,6 +1414,11 @@ function ExpandedView({ getProjectStatus, homeDirectory, childIndicators = [], + editingSessionId = null, + editingTitle = '', + onEditingTitleChange, + onEditSave, + onEditCancel, }: { sessions: SessionWithStatus[]; currentSessionId: string; @@ -1260,7 +1434,7 @@ function ExpandedView({ onToggleCollapse: () => void; onNewSession: () => void; onSessionClick: (id: string) => void; - onSessionDoubleClick?: () => void; + onSessionDoubleClick?: (sessionId: string, sessionTitle: string) => void; onProjectSwitch: (projectId: string) => void; onAddProject: () => void; onRemoveProject?: (projectId: string) => void; @@ -1273,6 +1447,11 @@ function ExpandedView({ getProjectStatus: (path: string) => { hasRunning: boolean; hasUnread: boolean }; homeDirectory: string | null; childIndicators?: Array<{ session: Session; isRunning: boolean }>; + editingSessionId?: string | null; + editingTitle?: string; + onEditingTitleChange?: (value: string) => void; + onEditSave?: () => void; + onEditCancel?: () => void; }) { const { t } = useI18n(); const containerRef = React.useRef(null); @@ -1334,6 +1513,7 @@ function ExpandedView({
{t('chat.mobileStatus.noSessionsInProject')}
) : ( - displaySessions.map((session) => ( - onSessionClick(session.id)} - onDoubleClick={onSessionDoubleClick} - needsAttention={needsAttention} - /> - )) + displaySessions.map((session) => { + // When the current session is being edited, the sticky header + // already renders the rename input; suppress the duplicate + // input on this row to avoid two simultaneous editors. + const isCurrent = session.id === currentSessionId; + const isEditingHere = editingSessionId === session.id && !isCurrent; + return ( + onSessionClick(session.id)} + onDoubleClick={onSessionDoubleClick} + needsAttention={needsAttention} + isEditing={isEditingHere} + editingTitle={editingTitle} + onEditingTitleChange={onEditingTitleChange} + onEditSave={onEditSave} + onEditCancel={onEditCancel} + /> + ); + }) )}
@@ -1424,13 +1622,13 @@ export const MobileSessionStatusBar: React.FC = ({ const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const getContextUsage = useSessionUIStore((state) => state.getContextUsage); + const updateSessionTitle = useSessionUIStore((state) => state.updateSessionTitle); const agents = useConfigStore((state) => state.agents); const getCurrentModel = useConfigStore((state) => state.getCurrentModel); const isMobile = useUIStore((state) => state.isMobile); const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar); const isMobileSessionStatusBarCollapsed = useUIStore((state) => state.isMobileSessionStatusBarCollapsed); const setIsMobileSessionStatusBarCollapsed = useUIStore((state) => state.setIsMobileSessionStatusBarCollapsed); - const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); // Project store const projects = useProjectsStore((state) => state.projects); @@ -1478,20 +1676,44 @@ export const MobileSessionStatusBar: React.FC = ({ const contextUsage = getContextUsage(contextLimit, outputLimit); const [isExpanded, setIsExpanded] = React.useState(false); + const [editingSessionId, setEditingSessionId] = React.useState(null); + const [editingTitle, setEditingTitle] = React.useState(''); if (!isMobile || !showMobileSessionStatusBar || totalCount === 0) { return null; } const handleSessionClick = (sessionId: string) => { + if (editingSessionId) return; setCurrentSession(sessionId); onSessionSwitch?.(sessionId); setIsExpanded(false); }; - const handleSessionDoubleClick = () => { - // On double-tap, switch to the Chat tab - setActiveMainTab('chat'); + const handleSessionDoubleClick = (sessionId: string, sessionTitle: string) => { + setEditingSessionId(sessionId); + setEditingTitle(sessionTitle); + }; + + const handleEditCancel = () => { + setEditingSessionId(null); + setEditingTitle(''); + }; + + const handleEditSave = () => { + if (!editingSessionId) return; + const trimmed = editingTitle.trim(); + const target = sessions.find((s) => s.id === editingSessionId); + const originalTitle = target ? getSessionTitle(target) : ''; + if (trimmed && trimmed !== originalTitle) { + void updateSessionTitle(editingSessionId, trimmed); + } + setEditingSessionId(null); + setEditingTitle(''); + }; + + const handleEditingTitleChange = (value: string) => { + setEditingTitle(value); }; const handleCreateSession = () => { @@ -1533,6 +1755,7 @@ export const MobileSessionStatusBar: React.FC = ({ = ({ onNewSession={handleCreateSession} contextUsage={contextUsage} childIndicators={currentSessionChildIndicators} + editingSessionId={editingSessionId} + editingTitle={editingTitle} + onTitleDoubleClick={handleSessionDoubleClick} + onEditingTitleChange={handleEditingTitleChange} + onEditSave={handleEditSave} + onEditCancel={handleEditCancel} /> ); } @@ -1579,6 +1808,11 @@ export const MobileSessionStatusBar: React.FC = ({ getProjectStatus={getProjectStatus} homeDirectory={homeDirectory} childIndicators={currentSessionChildIndicators} + editingSessionId={editingSessionId} + editingTitle={editingTitle} + onEditingTitleChange={handleEditingTitleChange} + onEditSave={handleEditSave} + onEditCancel={handleEditCancel} /> ); }; diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index df4de3f5..06431912 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -62,7 +62,7 @@ type Props = { handleCancelEdit: () => void; toggleParent: (expansionKey: string) => void; handleSessionSelect: (sessionId: string, sessionDirectory: string | null, isMissingDirectory: boolean, projectId?: string | null) => void; - handleSessionDoubleClick: () => void; + handleSessionDoubleClick: (sessionId: string, sessionTitle: string) => void; togglePinnedSession: (sessionId: string) => void; handleShareSession: (session: Session) => void; copiedSessionId: string | null; @@ -284,6 +284,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { : (showQuickArchiveAction ? 'group-hover:pr-12 group-focus-within:pr-12' : 'group-hover:pr-5 group-focus-within:pr-5')); const alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7'; const suppressNextSelectRef = React.useRef(false); + const editCancelledRef = React.useRef(false); const [isTouchPressed, setIsTouchPressed] = React.useState(false); const session = node.session; @@ -470,6 +471,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { onKeyDown={(event) => { if (event.key === 'Escape') { event.stopPropagation(); + editCancelledRef.current = true; handleCancelEdit(); return; } @@ -477,6 +479,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { event.stopPropagation(); } }} + onBlur={() => { + if (editCancelledRef.current) { + editCancelledRef.current = false; + return; + } + handleSaveEdit(); + }} />