From 7890d345832c13ffe8e1acb0be8f887d1bc0e5b4 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Sun, 15 Feb 2026 14:03:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20session=20rename=20=E2=80=94=20double-c?= =?UTF-8?q?lick=20or=20pencil=20icon=20to=20set=20custom=20session=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Double-click session name in sidebar to rename inline - Pencil icon on hover for discoverability - Custom names persisted in localStorage - Enter to confirm, Escape to cancel, blur to save - Clear name to revert to auto-generated display name - Search filter respects custom names - i18n for all 8 languages --- package-lock.json | 4 +- package.json | 2 +- src/components/Sidebar.tsx | 96 ++++++++++++++++++++++++++++++++++++-- src/lib/i18n.ts | 8 ++++ 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69b4c49..4c232d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pinchchat", - "version": "1.61.0", + "version": "1.62.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pinchchat", - "version": "1.61.0", + "version": "1.62.0", "license": "MIT", "dependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/package.json b/package.json index b0d10c9..6beaa40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinchchat", - "version": "1.61.0", + "version": "1.62.0", "description": "A sleek, dark-themed webchat UI for OpenClaw — monitor sessions, stream responses, and inspect tool calls in real-time.", "type": "module", "repository": { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f742b51..8bf1cbd 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; -import { X, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle, Download } from 'lucide-react'; +import { X, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle, Download, Pencil } from 'lucide-react'; import type { Session } from '../types'; import { useT } from '../hooks/useLocale'; import { SessionIcon } from './SessionIcon'; @@ -64,6 +64,21 @@ const PINNED_KEY = 'pinchchat-pinned-sessions'; const WIDTH_KEY = 'pinchchat-sidebar-width'; const ORDER_KEY = 'pinchchat-session-order'; const FILTER_KEY = 'pinchchat-session-filter'; +const NAMES_KEY = 'pinchchat-session-names'; + +function getCustomNames(): Record { + try { + const raw = localStorage.getItem(NAMES_KEY); + if (raw) return JSON.parse(raw) as Record; + } catch { /* noop */ } + return {}; +} + +function saveCustomNames(names: Record) { + try { + localStorage.setItem(NAMES_KEY, JSON.stringify(names)); + } catch { /* noop */ } +} /** Detect the category of a session for filtering */ function sessionCategory(s: Session): string { @@ -167,6 +182,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, }); const [dragKey, setDragKey] = useState(null); const [dropTarget, setDropTarget] = useState(null); + const [customNames, setCustomNames] = useState>(getCustomNames); + const [renamingKey, setRenamingKey] = useState(null); + const [renameValue, setRenameValue] = useState(''); + const renameInputRef = useRef(null); const searchRef = useRef(null); const listRef = useRef(null); const dragRef = useRef({ startX: 0, startW: 0 }); @@ -219,6 +238,44 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, }); }, []); + const startRename = useCallback((key: string, currentName: string, e: React.MouseEvent) => { + e.stopPropagation(); + setRenamingKey(key); + setRenameValue(currentName); + // Focus the input after render + requestAnimationFrame(() => renameInputRef.current?.focus()); + }, []); + + const commitRename = useCallback(() => { + if (!renamingKey) return; + const trimmed = renameValue.trim(); + setCustomNames(prev => { + const next = { ...prev }; + if (trimmed) { + next[renamingKey] = trimmed; + } else { + delete next[renamingKey]; + } + saveCustomNames(next); + return next; + }); + setRenamingKey(null); + setRenameValue(''); + }, [renamingKey, renameValue]); + + const cancelRename = useCallback(() => { + setRenamingKey(null); + setRenameValue(''); + }, []); + + // Focus rename input when it appears + useEffect(() => { + if (renamingKey) { + renameInputRef.current?.focus(); + renameInputRef.current?.select(); + } + }, [renamingKey]); + // Keyboard shortcut: Ctrl+K or Cmd+K to focus search when sidebar is open useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -259,7 +316,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, } if (filter.trim()) { const q = filter.toLowerCase(); - list = list.filter(s => sessionDisplayName(s).toLowerCase().includes(q)); + list = list.filter(s => (customNames[s.key] || sessionDisplayName(s)).toLowerCase().includes(q)); } // Sort pinned sessions to top (preserving relative order within each group) const pinnedList = list.filter(s => pinned.has(s.key)); @@ -277,7 +334,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, pinnedList.sort(byCustomThenRecent); unpinnedList.sort(byCustomThenRecent); return [...pinnedList, ...unpinnedList]; - }, [sessions, filter, pinned, customOrder, channelFilter]); + }, [sessions, filter, pinned, customOrder, channelFilter, customNames]); return ( <> @@ -473,11 +530,42 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit,
- {sessionDisplayName(s)} + {renamingKey === s.key ? ( + setRenameValue(e.target.value)} + onBlur={commitRename} + onKeyDown={(e) => { + if (e.key === 'Enter') { e.preventDefault(); commitRename(); } + if (e.key === 'Escape') { e.preventDefault(); cancelRename(); } + }} + onClick={(e) => e.stopPropagation()} + className="flex-1 min-w-0 bg-[var(--pc-hover)] text-pc-text-primary text-[13px] rounded px-1 py-0 border border-pc-border outline-none focus:ring-1 focus:ring-[var(--pc-accent-dim)]" + maxLength={60} + /> + ) : ( + startRename(s.key, customNames[s.key] || sessionDisplayName(s), e)} + title={t('sidebar.rename')} + > + {customNames[s.key] || sessionDisplayName(s)} + + )} {(() => { const rel = relativeTime(s.updatedAt); return rel ? {rel} : null; })()} +