feat: resizable sidebar with drag handle and persisted width

This commit is contained in:
Nicolas Varrot
2026-02-12 15:46:39 +00:00
parent 15f8060beb
commit fa9b10ac97
2 changed files with 148 additions and 1 deletions

View File

@@ -253,3 +253,79 @@
- The cron should NOT create tags or releases — it should commit to main with conventional commits, and Nicolas (or a manual trigger) decides when to cut a release
- Consider using `release-please` or a simple manual workflow_dispatch with version input
- Current Docker workflow (if any) should be reviewed and replaced by this proper one
## Item #29
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** in-progress
- **Description:** Sidebar resizable par drag & drop
- **Details:**
- Les noms de sessions sont coupés dans la sidebar
- Permettre à l'utilisateur de redimensionner la sidebar en glissant le bord droit
- Persister la largeur choisie (localStorage)
## Item #30
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** pending
- **Description:** Supprimer une session depuis la sidebar
- **Details:**
- Ajouter un bouton/action pour supprimer une session (clic droit ou icône)
- Confirmation avant suppression
- Vérifier si le gateway expose un endpoint de suppression de session
## Item #31
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** pending
- **Description:** Contenu des inputs scopé par session
- **Details:**
- Quand on commence à taper un message dans une session puis qu'on switch vers une autre, le brouillon doit être conservé
- Stocker les drafts par sessionKey (en mémoire ou localStorage)
- Restaurer le brouillon quand on revient sur la session
## Item #32
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** pending
- **Description:** Meilleur titre de session en haut de page
- **Details:**
- Actuellement on voit un UUID moche comme titre
- Afficher un nom plus lisible : le displayName de la session, ou le channel + contexte
- Pour les sessions main : afficher "Main" ou le nom de l'agent
- Pour les sub-agents : afficher le label s'il existe
- Fallback : session key nettoyée (sans le préfixe agent:xxx:)
## Item #33
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** pending
- **Description:** Afficher le nom de l'agent dans l'UI
- **Details:**
- Montrer clairement à quel agent on parle (pas juste l'agentId technique)
- Utile pour le multi-agent : savoir si on parle à "Marlbot" ou à un autre agent
- Placement : header ou en haut du chat
## Item #28
- **Date:** 2026-02-12
- **Priority:** high
- **Status:** done
- **Completed:** 2026-02-12 — tagged `v1.4.0`
- **Description:** Cron should auto-tag releases with semver based on commit type
- **Details:**
- The cron knows what changes it made (feat, fix, docs, refactor, etc.)
- It should auto-determine the version bump based on conventional commits:
- `feat:` → minor bump
- `fix:` / `refactor:` / `style:` / `perf:` → patch bump
- `docs:` / `ci:` / `chore:` → no release (skip tagging)
- Breaking changes → major bump
- After each meaningful commit (feat or fix), the cron should:
1. Read current version from package.json
2. Determine bump type from commit prefix
3. Bump version in package.json
4. Update CHANGELOG.md (move unreleased to new version section)
5. Commit the version bump
6. Create and push the git tag (vX.Y.Z)
7. The release.yml workflow handles the rest (Docker, GitHub Release)
- Accumulate doc/ci changes — only tag when there's a feat or fix to release
- This replaces the previous "Nicolas tags manually" approach

View File

@@ -5,6 +5,21 @@ import { useT } from '../hooks/useLocale';
import { SessionIcon } from './SessionIcon';
const PINNED_KEY = 'pinchchat-pinned-sessions';
const WIDTH_KEY = 'pinchchat-sidebar-width';
const MIN_WIDTH = 220;
const MAX_WIDTH = 480;
const DEFAULT_WIDTH = 288; // w-72
function getSavedWidth(): number {
try {
const v = localStorage.getItem(WIDTH_KEY);
if (v) {
const n = Number(v);
if (n >= MIN_WIDTH && n <= MAX_WIDTH) return n;
}
} catch { /* noop */ }
return DEFAULT_WIDTH;
}
function getPinnedSessions(): Set<string> {
try {
@@ -33,8 +48,48 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
const [filter, setFilter] = useState('');
const [focusIdx, setFocusIdx] = useState(-1);
const [pinned, setPinned] = useState(getPinnedSessions);
const [width, setWidth] = useState(getSavedWidth);
const [dragging, setDragging] = useState(false);
const searchRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const dragRef = useRef({ startX: 0, startW: 0 });
// Drag-to-resize logic
useEffect(() => {
if (!dragging) return;
const onMove = (e: MouseEvent | TouchEvent) => {
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
const newW = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, dragRef.current.startW + (clientX - dragRef.current.startX)));
setWidth(newW);
};
const onUp = () => {
setDragging(false);
// persist on release
localStorage.setItem(WIDTH_KEY, String(width));
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
document.addEventListener('touchmove', onMove);
document.addEventListener('touchend', onUp);
return () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onUp);
};
}, [dragging, width]);
// Save width when it changes (debounced via drag end above, but also on unmount)
useEffect(() => {
return () => { try { localStorage.setItem(WIDTH_KEY, String(width)); } catch { /* noop */ } };
}, [width]);
const startDrag = useCallback((e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
dragRef.current = { startX: clientX, startW: width };
setDragging(true);
}, [width]);
const togglePin = useCallback((key: string, e: React.MouseEvent) => {
e.stopPropagation();
@@ -79,7 +134,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
return (
<>
{open && <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden" onClick={onClose} />}
<aside role="navigation" aria-label="Sessions" className={`fixed lg:relative top-0 left-0 h-full w-72 bg-[#1e1e24]/95 border-r border-white/8 z-50 transform transition-transform lg:translate-x-0 ${open ? 'translate-x-0' : '-translate-x-full'} flex flex-col backdrop-blur-xl`}>
<aside role="navigation" aria-label="Sessions" className={`fixed lg:relative top-0 left-0 h-full bg-[#1e1e24]/95 border-r border-white/8 z-50 transform ${dragging ? '' : 'transition-transform'} lg:translate-x-0 ${open ? 'translate-x-0' : '-translate-x-full'} flex flex-col backdrop-blur-xl`} style={{ width: `${width}px` }}>
<div className="h-14 flex items-center justify-between px-4 border-b border-white/8">
<div className="flex items-center gap-2">
<div className="relative">
@@ -238,7 +293,23 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
<span className="h-1.5 w-1.5 rounded-full bg-indigo-300/50 shadow-[0_0_10px_rgba(99,102,241,0.4)]" />
<span className="ml-1 text-[9px] text-zinc-600 select-all" title={`PinchChat v${__APP_VERSION__}`}>v{__APP_VERSION__}</span>
</div>
{/* Resize drag handle */}
<div
onMouseDown={startDrag}
onTouchStart={startDrag}
className={`hidden lg:block absolute top-0 right-0 w-1.5 h-full cursor-col-resize group/resize z-10 ${dragging ? 'bg-cyan-400/20' : 'hover:bg-cyan-400/15'} transition-colors`}
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
aria-valuenow={width}
aria-valuemin={MIN_WIDTH}
aria-valuemax={MAX_WIDTH}
>
<div className={`absolute top-1/2 -translate-y-1/2 right-0 w-0.5 h-8 rounded-full ${dragging ? 'bg-cyan-400/50' : 'bg-white/0 group-hover/resize:bg-cyan-400/30'} transition-colors`} />
</div>
</aside>
{/* Prevent text selection while dragging */}
{dragging && <div className="fixed inset-0 z-[60] cursor-col-resize" />}
</>
);
}