diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
index baefdc5c68..65a58cea29 100644
--- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
+++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
@@ -25,13 +25,14 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/colla
import { Gear } from '../icons';
import { View, ViewOptions } from '../../utils/navigationUtils';
import { DEFAULT_CHAT_TITLE, useChatContext } from '../../contexts/ChatContext';
-import { listSessions, Session } from '../../api';
+import { listSessions, Session, updateSessionName } from '../../api';
import { resumeSession, startNewSession, shouldShowNewChatTitle } from '../../sessions';
import { useNavigation } from '../../hooks/useNavigation';
import { SessionIndicators } from '../SessionIndicators';
import { useSidebarSessionStatus } from '../../hooks/useSidebarSessionStatus';
import { getInitialWorkingDir } from '../../utils/workingDir';
import { useConfig } from '../ConfigContext';
+import { InlineEditText } from '../common/InlineEditText';
interface SidebarProps {
onSelectSession: (sessionId: string) => void;
@@ -124,6 +125,21 @@ const SessionList = React.memo<{
});
}, [sessions]);
+ const handleRenameSession = async (sessionId: string, newName: string) => {
+ await updateSessionName({
+ path: { session_id: sessionId },
+ body: { name: newName },
+ throwOnError: true,
+ });
+
+ // Dispatch event to update all components
+ window.dispatchEvent(
+ new CustomEvent(AppEvents.SESSION_RENAMED, {
+ detail: { sessionId, newName },
+ })
+ );
+ };
+
return (
{sortedSessions.map((session, index) => {
@@ -133,6 +149,7 @@ const SessionList = React.memo<{
const hasUnread = status?.hasUnreadActivity ?? false;
const displayName = getSessionDisplayName(session);
const isLast = index === sortedSessions.length - 1;
+ const canRename = !session.recipe?.title;
return (
@@ -154,7 +171,19 @@ const SessionList = React.memo<{
title={displayName}
>
{session.recipe &&
}
-
{displayName}
+
+ {canRename ? (
+ handleRenameSession(session.id, newName)}
+ className="text-sm -mx-2 -my-1"
+ editClassName="text-sm"
+ singleClickEdit={false}
+ />
+ ) : (
+ {displayName}
+ )}
+
Promise;
+ maxLength?: number;
+ placeholder?: string;
+ disabled?: boolean;
+ className?: string;
+ editClassName?: string;
+ onEditStart?: () => void;
+ onEditEnd?: () => void;
+ allowEmpty?: boolean;
+ singleClickEdit?: boolean;
+}
+
+export const InlineEditText: React.FC = ({
+ value,
+ onSave,
+ maxLength = 200,
+ placeholder = 'Enter text',
+ disabled = false,
+ className = '',
+ editClassName = '',
+ onEditStart,
+ onEditEnd,
+ allowEmpty = false,
+ singleClickEdit = true,
+}) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editValue, setEditValue] = useState(value);
+ const [isSaving, setIsSaving] = useState(false);
+ const inputRef = useRef(null);
+ const originalValue = useRef(value);
+
+ useEffect(() => {
+ if (!isEditing) {
+ setEditValue(value);
+ originalValue.current = value;
+ }
+ }, [value, isEditing]);
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ const handleStartEdit = useCallback(() => {
+ if (disabled || isSaving) return;
+ setIsEditing(true);
+ setEditValue(value);
+ onEditStart?.();
+ }, [disabled, isSaving, value, onEditStart]);
+
+ const handleCancel = useCallback(() => {
+ setIsEditing(false);
+ setEditValue(originalValue.current);
+ onEditEnd?.();
+ }, [onEditEnd]);
+
+ const handleSave = useCallback(async () => {
+ if (isSaving) return;
+
+ const trimmedValue = editValue.trim();
+
+ // Check if value unchanged
+ if (trimmedValue === originalValue.current) {
+ handleCancel();
+ return;
+ }
+
+ // Check if empty when not allowed
+ if (!allowEmpty && !trimmedValue) {
+ handleCancel();
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ await onSave(trimmedValue);
+ originalValue.current = trimmedValue;
+ setIsEditing(false);
+ onEditEnd?.();
+ } catch (error) {
+ const errMsg = errorMessage(error, 'Failed to save');
+ console.error('InlineEditText save error:', errMsg);
+ toast.error(errMsg);
+ setEditValue(originalValue.current);
+ handleCancel();
+ } finally {
+ setIsSaving(false);
+ }
+ }, [editValue, isSaving, allowEmpty, onSave, handleCancel, onEditEnd]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !isSaving) {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === 'Escape' && !isSaving) {
+ e.preventDefault();
+ handleCancel();
+ }
+ },
+ [handleSave, handleCancel, isSaving]
+ );
+
+ const handleBlur = useCallback(() => {
+ if (!isSaving) {
+ handleSave();
+ }
+ }, [handleSave, isSaving]);
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ setEditValue(e.target.value);
+ },
+ []
+ );
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (singleClickEdit) {
+ e.stopPropagation();
+ handleStartEdit();
+ }
+ },
+ [singleClickEdit, handleStartEdit]
+ );
+
+ const handleDoubleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (!singleClickEdit) {
+ e.stopPropagation();
+ handleStartEdit();
+ }
+ },
+ [singleClickEdit, handleStartEdit]
+ );
+
+ if (isEditing) {
+ return (
+ e.stopPropagation()}
+ />
+ );
+ }
+
+ return (
+
+ {value || {placeholder}}
+
+ );
+};