feat: multi-tab split view for 2 sessions side by side

- Add split view button (columns icon) in sidebar session actions
- Click to open any session in a secondary pane alongside the primary
- Resizable divider between panes (drag to resize, persisted in localStorage)
- Secondary pane supports full chat: history, streaming, send, abort
- Close split view via X button or clicking the split icon again
- Each pane has independent scroll, search, and tool collapse
- Keyboard shortcut and i18n support (EN/FR)
This commit is contained in:
Nicolas Varrot
2026-02-13 02:44:33 +00:00
parent 00bf6d156f
commit f09482e6cb
5 changed files with 369 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
import { X, Sparkles, Search, Pin, Trash2 } from 'lucide-react';
import { X, Sparkles, Search, Pin, Trash2, Columns2 } from 'lucide-react';
import type { Session } from '../types';
import { useT } from '../hooks/useLocale';
import { SessionIcon } from './SessionIcon';
@@ -57,11 +57,13 @@ interface Props {
activeSession: string;
onSwitch: (key: string) => void;
onDelete: (key: string) => void;
onSplit?: (key: string) => void;
splitSession?: string | null;
open: boolean;
onClose: () => void;
}
export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onClose }: Props) {
export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, splitSession, open, onClose }: Props) {
const t = useT();
const [filter, setFilter] = useState('');
const [focusIdx, setFocusIdx] = useState(-1);
@@ -332,6 +334,20 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
>
<Pin size={12} className={isPinned ? 'fill-current' : ''} />
</button>
{onSplit && (
<button
onClick={(e) => { e.stopPropagation(); onSplit(s.key); }}
className={`shrink-0 p-0.5 rounded-lg transition-all ${
splitSession === s.key
? 'text-pc-accent opacity-80 hover:opacity-100'
: 'text-pc-text-faint opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-pc-text-secondary'
}`}
title={t('sidebar.openSplit')}
aria-label={t('sidebar.openSplit')}
>
<Columns2 size={12} />
</button>
)}
<button
onClick={(e) => { e.stopPropagation(); setConfirmDelete(s.key); }}
className="shrink-0 p-0.5 rounded-lg transition-all text-pc-text-faint opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-red-400"