feat: rename sessions inline via double-click (#1320)

* feat: rename sessions inline via double-click

Double-clicking a session name in the sidebar or the mobile session
status bar now switches it into an inline editable input. Enter saves,
Esc cancels, and clicking elsewhere blurs the input to save. This
mirrors the VSCode/Finder rename pattern and removes the need to open
the session menu for what is a very common action.

* fix(rename): close sidebar input on empty title; drop duplicate mobile editor

Two issues from PR review:

1. handleSaveEdit (sidebar) only closed the input when editTitle.trim()
   was non-empty. Clearing the title and pressing Enter or blurring left
   the input open with no exit path other than Escape. The save handler
   now always closes the editor; an empty title is treated as a silent
   cancel (no update call).

2. ExpandedView (mobile) renders the current session twice — once in the
   sticky header, once in the session list — so a single editingSessionId
   produced two simultaneous inputs for the current session. The list row
   for the current session now suppresses its rename input; the header
   remains the single editor in that case.

* fix: refine inline session rename editing

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Roberto Bertó
2026-05-23 21:15:15 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 49bf1dbff3
commit 0c60fd6222
3 changed files with 293 additions and 47 deletions
@@ -302,27 +302,60 @@ function SessionItem({
getSessionTitle, getSessionTitle,
onClick, onClick,
onDoubleClick, onDoubleClick,
needsAttention needsAttention,
isEditing = false,
editingTitle = '',
onEditingTitleChange,
onEditSave,
onEditCancel,
}: { }: {
session: SessionWithStatus; session: SessionWithStatus;
isCurrent: boolean; isCurrent: boolean;
getSessionAgentName: (s: Session) => string; getSessionAgentName: (s: Session) => string;
getSessionTitle: (s: Session) => string; getSessionTitle: (s: Session) => string;
onClick: () => void; onClick: () => void;
onDoubleClick?: () => void; onDoubleClick?: (sessionId: string, sessionTitle: string) => void;
needsAttention: (sessionId: string) => boolean; needsAttention: (sessionId: string) => boolean;
isEditing?: boolean;
editingTitle?: string;
onEditingTitleChange?: (value: string) => void;
onEditSave?: () => void;
onEditCancel?: () => void;
}) { }) {
const agentName = getSessionAgentName(session); const agentName = getSessionAgentName(session);
const agentColor = getAgentColor(agentName); const agentColor = getAgentColor(agentName);
const extraCount = (session._runningChildrenCount || 0) + (session._statusType !== 'idle' ? 1 : 0) - 1 - (session._childIndicators?.length || 0); const extraCount = (session._runningChildrenCount || 0) + (session._statusType !== 'idle' ? 1 : 0) - 1 - (session._childIndicators?.length || 0);
const sessionTitle = getSessionTitle(session);
const editInputRef = React.useRef<HTMLInputElement>(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 ( return (
<button <div
type="button" role={isEditing ? undefined : 'button'}
onClick={onClick} tabIndex={isEditing ? undefined : 0}
onClick={isEditing ? undefined : onClick}
onDoubleClick={(e) => { onDoubleClick={(e) => {
if (isEditing) return;
e.stopPropagation(); e.stopPropagation();
onDoubleClick?.(); onDoubleClick?.(session.id, sessionTitle);
}}
onKeyDown={(e) => {
if (isEditing) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}} }}
className={cn( className={cn(
"flex items-center gap-0.5 px-1.5 py-px text-left transition-colors", "flex items-center gap-0.5 px-1.5 py-px text-left transition-colors",
@@ -342,12 +375,48 @@ function SessionItem({
style={{ backgroundColor: `var(${agentColor.var})` }} style={{ backgroundColor: `var(${agentColor.var})` }}
/> />
<span className={cn( {isEditing ? (
"text-[13px] truncate leading-tight", <input
isCurrent ? "text-[var(--interactive-selection-foreground)] font-medium" : "text-[var(--surface-foreground)]" ref={editInputRef}
)}> type="text"
{getSessionTitle(session)} value={editingTitle}
</span> onChange={(e) => onEditingTitleChange?.(e.target.value)}
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
onEditSave?.();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
editCancelledRef.current = true;
onEditCancel?.();
}
}}
onBlur={() => {
if (editCancelledRef.current) {
editCancelledRef.current = false;
return;
}
onEditSave?.();
}}
className={cn(
"flex-1 min-w-0 text-[13px] leading-tight px-1 py-px rounded",
"bg-background border border-[var(--interactive-border)]",
"text-[var(--surface-foreground)] outline-none",
"focus:border-[var(--primary-base)]"
)}
/>
) : (
<span className={cn(
"text-[13px] truncate leading-tight",
isCurrent ? "text-[var(--interactive-selection-foreground)] font-medium" : "text-[var(--surface-foreground)]"
)}>
{sessionTitle}
</span>
)}
{(session._childIndicators?.length || 0) > 0 && ( {(session._childIndicators?.length || 0) > 0 && (
<div className="flex items-center gap-0.5 text-[var(--surface-mutedForeground)]"> <div className="flex items-center gap-0.5 text-[var(--surface-mutedForeground)]">
@@ -376,7 +445,7 @@ function SessionItem({
<span className="text-[10px]">]</span> <span className="text-[10px]">]</span>
</div> </div>
)} )}
</button> </div>
); );
} }
@@ -396,6 +465,7 @@ function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsa
} }
interface SessionStatusHeaderProps { interface SessionStatusHeaderProps {
currentSessionId?: string | null;
currentSessionTitle: string; currentSessionTitle: string;
currentProjectLabel?: string; currentProjectLabel?: string;
currentProjectIcon?: string | null; currentProjectIcon?: string | null;
@@ -405,9 +475,16 @@ interface SessionStatusHeaderProps {
onToggle: () => void; onToggle: () => void;
isExpanded?: boolean; isExpanded?: boolean;
childIndicators?: Array<{ session: Session; isRunning: 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({ function SessionStatusHeader({
currentSessionId,
currentSessionTitle, currentSessionTitle,
currentProjectLabel, currentProjectLabel,
currentProjectIcon, currentProjectIcon,
@@ -416,22 +493,49 @@ function SessionStatusHeader({
currentProjectColor, currentProjectColor,
onToggle, onToggle,
isExpanded = false, isExpanded = false,
childIndicators = [] childIndicators = [],
isEditing = false,
editingTitle = '',
onTitleDoubleClick,
onEditingTitleChange,
onEditSave,
onEditCancel,
}: SessionStatusHeaderProps) { }: SessionStatusHeaderProps) {
const [imageFailed, setImageFailed] = React.useState(false); const [imageFailed, setImageFailed] = React.useState(false);
const projectIconName = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null; const projectIconName = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null;
const imageUrl = !imageFailed ? currentProjectIconImageUrl : null; const imageUrl = !imageFailed ? currentProjectIconImageUrl : null;
const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null; const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null;
const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0; const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0;
const editInputRef = React.useRef<HTMLInputElement>(null);
const editCancelledRef = React.useRef(false);
React.useEffect(() => { React.useEffect(() => {
setImageFailed(false); setImageFailed(false);
}, [currentProjectIconImageUrl]); }, [currentProjectIconImageUrl]);
React.useEffect(() => {
if (isEditing) {
editCancelledRef.current = false;
const node = editInputRef.current;
if (node) {
node.focus();
node.select();
}
}
}, [isEditing]);
return ( return (
<button <div
type="button" role={isEditing ? undefined : 'button'}
onClick={onToggle} tabIndex={isEditing ? undefined : 0}
onClick={isEditing ? undefined : onToggle}
onKeyDown={(e) => {
if (isEditing) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onToggle();
}
}}
className="w-full flex flex-col px-2 py-0.5 text-left transition-colors hover:bg-[var(--interactive-hover)]" className="w-full flex flex-col px-2 py-0.5 text-left transition-colors hover:bg-[var(--interactive-hover)]"
> >
{!isExpanded && currentProjectLabel && ( {!isExpanded && currentProjectLabel && (
@@ -467,9 +571,53 @@ function SessionStatusHeader({
</div> </div>
)} )}
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<span className="flex-1 min-w-0 text-[13px] text-[var(--surface-foreground)] truncate leading-none"> {isEditing ? (
{currentSessionTitle} <input
</span> ref={editInputRef}
type="text"
value={editingTitle}
onChange={(e) => onEditingTitleChange?.(e.target.value)}
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
onEditSave?.();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
editCancelledRef.current = true;
onEditCancel?.();
}
}}
onBlur={() => {
if (editCancelledRef.current) {
editCancelledRef.current = false;
return;
}
onEditSave?.();
}}
className={cn(
"flex-1 min-w-0 text-[13px] leading-none px-1 py-0.5 rounded",
"bg-background border border-[var(--interactive-border)]",
"text-[var(--surface-foreground)] outline-none",
"focus:border-[var(--primary-base)]"
)}
/>
) : (
<span
className="flex-1 min-w-0 text-[13px] text-[var(--surface-foreground)] truncate leading-none"
onDoubleClick={(e) => {
if (!currentSessionId || !onTitleDoubleClick) return;
e.preventDefault();
e.stopPropagation();
onTitleDoubleClick(currentSessionId, currentSessionTitle);
}}
>
{currentSessionTitle}
</span>
)}
{childIndicators.length > 0 && ( {childIndicators.length > 0 && (
<div className="flex items-center gap-0.5 text-[var(--surface-mutedForeground)]"> <div className="flex items-center gap-0.5 text-[var(--surface-mutedForeground)]">
<span className="text-[10px]">[</span> <span className="text-[10px]">[</span>
@@ -499,7 +647,7 @@ function SessionStatusHeader({
</div> </div>
)} )}
</div> </div>
</button> </div>
); );
} }
@@ -1142,6 +1290,7 @@ function ProjectBar({
function CollapsedView({ function CollapsedView({
runningCount, runningCount,
unreadCount, unreadCount,
currentSessionId,
currentSessionTitle, currentSessionTitle,
currentProjectLabel, currentProjectLabel,
currentProjectIcon, currentProjectIcon,
@@ -1152,9 +1301,16 @@ function CollapsedView({
onNewSession, onNewSession,
contextUsage, contextUsage,
childIndicators = [], childIndicators = [],
editingSessionId = null,
editingTitle = '',
onTitleDoubleClick,
onEditingTitleChange,
onEditSave,
onEditCancel,
}: { }: {
runningCount: number; runningCount: number;
unreadCount: number; unreadCount: number;
currentSessionId?: string | null;
currentSessionTitle: string; currentSessionTitle: string;
currentProjectLabel?: string; currentProjectLabel?: string;
currentProjectIcon?: string | null; currentProjectIcon?: string | null;
@@ -1165,6 +1321,12 @@ function CollapsedView({
onNewSession: () => void; onNewSession: () => void;
contextUsage: SessionContextUsage | null; contextUsage: SessionContextUsage | null;
childIndicators?: Array<{ session: Session; isRunning: boolean }>; 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 { t } = useI18n();
const { handleTouchStart, handleTouchMove, handleTouchEnd } = useDrawerSwipe(); const { handleTouchStart, handleTouchMove, handleTouchEnd } = useDrawerSwipe();
@@ -1182,6 +1344,7 @@ function CollapsedView({
> >
<div className="flex-1 min-w-0 mr-1"> <div className="flex-1 min-w-0 mr-1">
<SessionStatusHeader <SessionStatusHeader
currentSessionId={currentSessionId}
currentSessionTitle={currentSessionTitle} currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel} currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon} currentProjectIcon={currentProjectIcon}
@@ -1190,6 +1353,12 @@ function CollapsedView({
currentProjectColor={currentProjectColor} currentProjectColor={currentProjectColor}
onToggle={onToggle} onToggle={onToggle}
childIndicators={childIndicators} childIndicators={childIndicators}
isEditing={Boolean(currentSessionId) && editingSessionId === currentSessionId}
editingTitle={editingTitle}
onTitleDoubleClick={onTitleDoubleClick}
onEditingTitleChange={onEditingTitleChange}
onEditSave={onEditSave}
onEditCancel={onEditCancel}
/> />
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
@@ -1245,6 +1414,11 @@ function ExpandedView({
getProjectStatus, getProjectStatus,
homeDirectory, homeDirectory,
childIndicators = [], childIndicators = [],
editingSessionId = null,
editingTitle = '',
onEditingTitleChange,
onEditSave,
onEditCancel,
}: { }: {
sessions: SessionWithStatus[]; sessions: SessionWithStatus[];
currentSessionId: string; currentSessionId: string;
@@ -1260,7 +1434,7 @@ function ExpandedView({
onToggleCollapse: () => void; onToggleCollapse: () => void;
onNewSession: () => void; onNewSession: () => void;
onSessionClick: (id: string) => void; onSessionClick: (id: string) => void;
onSessionDoubleClick?: () => void; onSessionDoubleClick?: (sessionId: string, sessionTitle: string) => void;
onProjectSwitch: (projectId: string) => void; onProjectSwitch: (projectId: string) => void;
onAddProject: () => void; onAddProject: () => void;
onRemoveProject?: (projectId: string) => void; onRemoveProject?: (projectId: string) => void;
@@ -1273,6 +1447,11 @@ function ExpandedView({
getProjectStatus: (path: string) => { hasRunning: boolean; hasUnread: boolean }; getProjectStatus: (path: string) => { hasRunning: boolean; hasUnread: boolean };
homeDirectory: string | null; homeDirectory: string | null;
childIndicators?: Array<{ session: Session; isRunning: boolean }>; childIndicators?: Array<{ session: Session; isRunning: boolean }>;
editingSessionId?: string | null;
editingTitle?: string;
onEditingTitleChange?: (value: string) => void;
onEditSave?: () => void;
onEditCancel?: () => void;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
@@ -1334,6 +1513,7 @@ function ExpandedView({
<div className="flex items-center justify-between px-2 py-1.5 border-b border-[var(--interactive-border)]"> <div className="flex items-center justify-between px-2 py-1.5 border-b border-[var(--interactive-border)]">
<div className="flex-1 min-w-0 mr-1"> <div className="flex-1 min-w-0 mr-1">
<SessionStatusHeader <SessionStatusHeader
currentSessionId={currentSessionId}
currentSessionTitle={currentSessionTitle} currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel} currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon} currentProjectIcon={currentProjectIcon}
@@ -1343,6 +1523,12 @@ function ExpandedView({
onToggle={onToggleCollapse} onToggle={onToggleCollapse}
isExpanded={true} isExpanded={true}
childIndicators={childIndicators} childIndicators={childIndicators}
isEditing={editingSessionId === currentSessionId}
editingTitle={editingTitle}
onTitleDoubleClick={onSessionDoubleClick}
onEditingTitleChange={onEditingTitleChange}
onEditSave={onEditSave}
onEditCancel={onEditCancel}
/> />
</div> </div>
<div <div
@@ -1395,18 +1581,30 @@ function ExpandedView({
<span>{t('chat.mobileStatus.noSessionsInProject')}</span> <span>{t('chat.mobileStatus.noSessionsInProject')}</span>
</div> </div>
) : ( ) : (
displaySessions.map((session) => ( displaySessions.map((session) => {
<SessionItem // When the current session is being edited, the sticky header
key={session.id} // already renders the rename input; suppress the duplicate
session={session} // input on this row to avoid two simultaneous editors.
isCurrent={session.id === currentSessionId} const isCurrent = session.id === currentSessionId;
getSessionAgentName={getSessionAgentName} const isEditingHere = editingSessionId === session.id && !isCurrent;
getSessionTitle={getSessionTitle} return (
onClick={() => onSessionClick(session.id)} <SessionItem
onDoubleClick={onSessionDoubleClick} key={session.id}
needsAttention={needsAttention} session={session}
/> isCurrent={isCurrent}
)) getSessionAgentName={getSessionAgentName}
getSessionTitle={getSessionTitle}
onClick={() => onSessionClick(session.id)}
onDoubleClick={onSessionDoubleClick}
needsAttention={needsAttention}
isEditing={isEditingHere}
editingTitle={editingTitle}
onEditingTitleChange={onEditingTitleChange}
onEditSave={onEditSave}
onEditCancel={onEditCancel}
/>
);
})
)} )}
</div> </div>
</div> </div>
@@ -1424,13 +1622,13 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
const getContextUsage = useSessionUIStore((state) => state.getContextUsage); const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
const updateSessionTitle = useSessionUIStore((state) => state.updateSessionTitle);
const agents = useConfigStore((state) => state.agents); const agents = useConfigStore((state) => state.agents);
const getCurrentModel = useConfigStore((state) => state.getCurrentModel); const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
const isMobile = useUIStore((state) => state.isMobile); const isMobile = useUIStore((state) => state.isMobile);
const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar); const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar);
const isMobileSessionStatusBarCollapsed = useUIStore((state) => state.isMobileSessionStatusBarCollapsed); const isMobileSessionStatusBarCollapsed = useUIStore((state) => state.isMobileSessionStatusBarCollapsed);
const setIsMobileSessionStatusBarCollapsed = useUIStore((state) => state.setIsMobileSessionStatusBarCollapsed); const setIsMobileSessionStatusBarCollapsed = useUIStore((state) => state.setIsMobileSessionStatusBarCollapsed);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
// Project store // Project store
const projects = useProjectsStore((state) => state.projects); const projects = useProjectsStore((state) => state.projects);
@@ -1478,20 +1676,44 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
const contextUsage = getContextUsage(contextLimit, outputLimit); const contextUsage = getContextUsage(contextLimit, outputLimit);
const [isExpanded, setIsExpanded] = React.useState(false); const [isExpanded, setIsExpanded] = React.useState(false);
const [editingSessionId, setEditingSessionId] = React.useState<string | null>(null);
const [editingTitle, setEditingTitle] = React.useState('');
if (!isMobile || !showMobileSessionStatusBar || totalCount === 0) { if (!isMobile || !showMobileSessionStatusBar || totalCount === 0) {
return null; return null;
} }
const handleSessionClick = (sessionId: string) => { const handleSessionClick = (sessionId: string) => {
if (editingSessionId) return;
setCurrentSession(sessionId); setCurrentSession(sessionId);
onSessionSwitch?.(sessionId); onSessionSwitch?.(sessionId);
setIsExpanded(false); setIsExpanded(false);
}; };
const handleSessionDoubleClick = () => { const handleSessionDoubleClick = (sessionId: string, sessionTitle: string) => {
// On double-tap, switch to the Chat tab setEditingSessionId(sessionId);
setActiveMainTab('chat'); 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 = () => { const handleCreateSession = () => {
@@ -1533,6 +1755,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
<CollapsedView <CollapsedView
runningCount={totalRunning} runningCount={totalRunning}
unreadCount={totalUnread} unreadCount={totalUnread}
currentSessionId={currentSessionId}
currentSessionTitle={currentSessionTitle} currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel} currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon} currentProjectIcon={currentProjectIcon}
@@ -1543,6 +1766,12 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
onNewSession={handleCreateSession} onNewSession={handleCreateSession}
contextUsage={contextUsage} contextUsage={contextUsage}
childIndicators={currentSessionChildIndicators} childIndicators={currentSessionChildIndicators}
editingSessionId={editingSessionId}
editingTitle={editingTitle}
onTitleDoubleClick={handleSessionDoubleClick}
onEditingTitleChange={handleEditingTitleChange}
onEditSave={handleEditSave}
onEditCancel={handleEditCancel}
/> />
); );
} }
@@ -1579,6 +1808,11 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
getProjectStatus={getProjectStatus} getProjectStatus={getProjectStatus}
homeDirectory={homeDirectory} homeDirectory={homeDirectory}
childIndicators={currentSessionChildIndicators} childIndicators={currentSessionChildIndicators}
editingSessionId={editingSessionId}
editingTitle={editingTitle}
onEditingTitleChange={handleEditingTitleChange}
onEditSave={handleEditSave}
onEditCancel={handleEditCancel}
/> />
); );
}; };
@@ -62,7 +62,7 @@ type Props = {
handleCancelEdit: () => void; handleCancelEdit: () => void;
toggleParent: (expansionKey: string) => void; toggleParent: (expansionKey: string) => void;
handleSessionSelect: (sessionId: string, sessionDirectory: string | null, isMissingDirectory: boolean, projectId?: string | null) => 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; togglePinnedSession: (sessionId: string) => void;
handleShareSession: (session: Session) => void; handleShareSession: (session: Session) => void;
copiedSessionId: string | null; 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')); : (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 alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7';
const suppressNextSelectRef = React.useRef(false); const suppressNextSelectRef = React.useRef(false);
const editCancelledRef = React.useRef(false);
const [isTouchPressed, setIsTouchPressed] = React.useState(false); const [isTouchPressed, setIsTouchPressed] = React.useState(false);
const session = node.session; const session = node.session;
@@ -470,6 +471,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
event.stopPropagation(); event.stopPropagation();
editCancelledRef.current = true;
handleCancelEdit(); handleCancelEdit();
return; return;
} }
@@ -477,6 +479,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
event.stopPropagation(); event.stopPropagation();
} }
}} }}
onBlur={() => {
if (editCancelledRef.current) {
editCancelledRef.current = false;
return;
}
handleSaveEdit();
}}
/> />
<button <button
type="submit" type="submit"
@@ -824,7 +833,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={(event) => handleRowSelect(event)} onClick={(event) => handleRowSelect(event)}
onDoubleClick={(e) => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleSessionDoubleClick(); handleSessionDoubleClick(session.id, sessionTitle);
}} }}
className={cn( className={cn(
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]', 'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
@@ -888,7 +897,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={(event) => handleRowSelect(event)} onClick={(event) => handleRowSelect(event)}
onDoubleClick={(e) => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleSessionDoubleClick(); handleSessionDoubleClick(session.id, sessionTitle);
}} }}
className={cn( className={cn(
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]', 'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
@@ -98,16 +98,19 @@ export const useSessionActions = (args: Args) => {
[args], [args],
); );
const handleSessionDoubleClick = React.useCallback(() => { const handleSessionDoubleClick = React.useCallback((sessionId: string, sessionTitle: string) => {
args.setActiveMainTab('chat'); args.setEditingId(sessionId);
args.setEditTitle(sessionTitle);
}, [args]); }, [args]);
const handleSaveEdit = React.useCallback(async () => { const handleSaveEdit = React.useCallback(async () => {
if (args.editingId && args.editTitle.trim()) { if (!args.editingId) return;
await args.updateSessionTitle(args.editingId, args.editTitle.trim()); const trimmed = args.editTitle.trim();
args.setEditingId(null); if (trimmed) {
args.setEditTitle(''); await args.updateSessionTitle(args.editingId, trimmed);
} }
args.setEditingId(null);
args.setEditTitle('');
}, [args]); }, [args]);
const handleCancelEdit = React.useCallback(() => { const handleCancelEdit = React.useCallback(() => {