/** * @local/dsh-chat-split — browser side. * * Provides a split-screen multi-chat environment: * - Arranges chats side-by-side or wraps them vertically in a responsive grid. * - Minimum height of 396px per chat window, configurable in Settings. * - Settings section includes an interactive preview showing how real layout reflows. * - Each chat has its own sidebar inside its window with session selection and quick switching. * - Brief blue glow highlight when selecting an already-open chat while split mode is active. * - Include/exclude chats to/from split view directly from the session menu. * - Top bar on each split window with position rearrangement in grid and window closing. */ window.__ModuleLoader__.load({ id: '@local/dsh-chat-split', factory(require) { const React = require('react'); const h = React.createElement; const { useState, useEffect, useRef, useCallback, useMemo, useSyncExternalStore } = React; const PANEL_ID = 'dsh-chat-split'; const SETTINGS_KEY = 'dsh.chat-split.settings.v1'; const SESSIONS_KEY = 'dsh.chat-split.sessions.v1'; const ACTIVE_KEY = 'dsh.chat-split.active.v1'; // ---- Settings & State Store ------------------------------------------ const defaultSettings = { minHeight: 396, minWidth: 420, gap: 10, }; function loadJson(key, fallback) { try { const raw = localStorage.getItem(key); if (!raw) return fallback; return { ...fallback, ...JSON.parse(raw) }; } catch { return fallback; } } function saveJson(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} } const listeners = new Set(); function notify() { listeners.forEach((fn) => { try { fn(); } catch {} }); } let currentSettings = loadJson(SETTINGS_KEY, defaultSettings); let splitSessionIds = (() => { try { const raw = localStorage.getItem(SESSIONS_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } })(); let isSplitActive = localStorage.getItem(ACTIVE_KEY) === 'true'; let glowingSessionId = null; let glowTimer = null; const store = { getSettings() { return currentSettings; }, updateSettings(patch) { currentSettings = { ...currentSettings, ...patch }; saveJson(SETTINGS_KEY, currentSettings); notify(); }, getSessions() { return splitSessionIds; }, setSessions(ids) { splitSessionIds = ids; saveJson(SESSIONS_KEY, ids); notify(); }, addSession(id) { if (!splitSessionIds.includes(id)) { splitSessionIds = [...splitSessionIds, id]; saveJson(SESSIONS_KEY, splitSessionIds); notify(); } }, removeSession(id) { splitSessionIds = splitSessionIds.filter((x) => x !== id); saveJson(SESSIONS_KEY, splitSessionIds); notify(); }, moveSession(index, direction) { const targetIndex = index + direction; if (targetIndex < 0 || targetIndex >= splitSessionIds.length) return; const copy = [...splitSessionIds]; const [removed] = copy.splice(index, 1); copy.splice(targetIndex, 0, removed); splitSessionIds = copy; saveJson(SESSIONS_KEY, copy); notify(); }, isSplitActive() { return isSplitActive; }, setSplitActive(active) { isSplitActive = !!active; localStorage.setItem(ACTIVE_KEY, isSplitActive ? 'true' : 'false'); notify(); }, getGlowingSessionId() { return glowingSessionId; }, triggerGlow(sessionId) { if (!isSplitActive) return; glowingSessionId = sessionId; if (glowTimer) clearTimeout(glowTimer); notify(); glowTimer = setTimeout(() => { glowingSessionId = null; notify(); }, 1600); }, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } }; // ---- CSS Styles ------------------------------------------------------ function installStyles() { if (document.getElementById('dsh-chat-split-styles')) return; const el = document.createElement('style'); el.id = 'dsh-chat-split-styles'; el.textContent = ` @keyframes dsh-split-blue-glow { 0% { box-shadow: 0 0 0 3px #1f6feb, 0 0 20px 6px rgba(31, 111, 235, 0.65); border-color: #58a6ff; } 60% { box-shadow: 0 0 0 3px #1f6feb, 0 0 16px 4px rgba(31, 111, 235, 0.45); border-color: #58a6ff; } 100% { box-shadow: none; border-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.12)); } } .dsh-split-container { display: flex; flex-direction: column; height: 100%; width: 100%; background: var(--dsw-alias-bg-base, #0d1117); color: var(--dsw-alias-label-primary, #e6edf3); overflow: hidden; font-family: var(--dsw-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif); } .dsh-split-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.1)); background: var(--dsw-alias-bg-l1, #161b22); flex-shrink: 0; gap: 12px; } .dsh-split-header-left { display: flex; align-items: center; gap: 10px; } .dsh-split-header-title { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } .dsh-split-badge { background: rgba(31, 111, 235, 0.2); color: #58a6ff; border: 1px solid rgba(56, 139, 253, 0.3); padding: 2px 7px; border-radius: 12px; font-size: 11px; font-weight: 500; } .dsh-split-grid { display: grid; flex: 1; padding: 10px; overflow: auto; box-sizing: border-box; align-content: start; } .dsh-split-window { display: flex; flex-direction: column; background: var(--dsw-alias-bg-l1, #161b22); border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.12)); border-radius: 8px; overflow: hidden; box-sizing: border-box; position: relative; transition: border-color 0.2s, box-shadow 0.2s; } .dsh-split-window.glow { animation: dsh-split-blue-glow 1.5s ease-out; border-color: #58a6ff !important; z-index: 5; } .dsh-split-win-bar { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: var(--dsw-alias-bg-l2, #21262d); border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.1)); font-size: 12px; font-weight: 500; user-select: none; flex-shrink: 0; gap: 8px; } .dsh-split-win-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; flex: 1; display: flex; align-items: center; gap: 6px; } .dsh-split-win-controls { display: flex; align-items: center; gap: 4px; } .dsh-split-win-btn { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #8b949e); cursor: pointer; padding: 3px 6px; border-radius: 4px; font-size: 12px; display: inline-flex; align-items: center; justify-content: center; line-height: 1; transition: background 0.15s, color 0.15s; } .dsh-split-win-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-l3, rgba(255, 255, 255, 0.1)); color: var(--dsw-alias-label-primary, #ffffff); } .dsh-split-win-btn:disabled { opacity: 0.35; cursor: default; } .dsh-split-win-body { display: flex; flex: 1; min-height: 0; position: relative; overflow: hidden; } .dsh-split-sidebar { width: 170px; flex-shrink: 0; border-right: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.1)); background: var(--dsw-alias-bg-l2, #1c2128); display: flex; flex-direction: column; overflow: hidden; transition: width 0.2s; } .dsh-split-sidebar.collapsed { width: 0; border-right: none; } .dsh-split-sidebar-head { padding: 6px 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #8b949e); border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08)); display: flex; align-items: center; justify-content: space-between; } .dsh-split-sidebar-list { flex: 1; overflow-y: auto; padding: 4px 0; margin: 0; list-style: none; } .dsh-split-sidebar-item { display: flex; align-items: center; padding: 6px 8px; gap: 6px; font-size: 11px; cursor: pointer; color: var(--dsw-alias-label-primary, #e6edf3); border-left: 2px solid transparent; transition: background 0.15s; } .dsh-split-sidebar-item:hover { background: var(--dsw-alias-bg-l3, rgba(255, 255, 255, 0.06)); } .dsh-split-sidebar-item.current { background: rgba(31, 111, 235, 0.15); border-left-color: #58a6ff; font-weight: 600; } .dsh-split-sidebar-item.open-elsewhere { opacity: 0.75; } .dsh-split-sidebar-item-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .dsh-split-conversation-pane { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; overflow: hidden; } .dsh-split-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--dsw-alias-label-secondary, #8b949e); padding: 24px; } .dsh-split-empty h3 { margin: 0 0 8px 0; color: var(--dsw-alias-label-primary, #ffffff); font-size: 16px; } .dsh-split-btn { border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.18)); background: var(--dsw-alias-bg-l2, #21262d); color: var(--dsw-alias-label-primary, #ffffff); padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s; } .dsh-split-btn:hover { background: var(--dsw-alias-bg-l3, #30363d); border-color: var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.3)); } .dsh-split-btn-primary { background: #238636; border-color: rgba(240, 246, 252, 0.1); } .dsh-split-btn-primary:hover { background: #2ea043; } .dsh-split-preview-box { border: 1px dashed var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.25)); border-radius: 8px; padding: 12px; background: var(--dsw-alias-bg-base, #0d1117); position: relative; box-sizing: border-box; margin-top: 10px; } .dsh-split-mock-win { background: var(--dsw-alias-bg-l1, #161b22); border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.15)); border-radius: 6px; display: flex; flex-direction: column; overflow: hidden; font-size: 11px; } .dsh-split-mock-bar { background: var(--dsw-alias-bg-l2, #21262d); padding: 4px 8px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.1)); font-weight: 600; } .dsh-split-mock-body { flex: 1; display: flex; } .dsh-split-mock-side { width: 50px; background: var(--dsw-alias-bg-l2, #1c2128); border-right: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08)); padding: 4px; } .dsh-split-mock-chat { flex: 1; padding: 8px; display: flex; flex-direction: column; justify-content: space-between; background: rgba(0,0,0,0.2); } `; document.head.appendChild(el); } // ---- Child Slot Occupant: Split Window Content ----------------------- function FixedChatConversationView(props) { return h(React.Fragment, null, props.renderSlot('conversation.session', { view: 'chat' })); } function SplitWindowConversationPanel({ sessionId, useSession, useConversation, useSessions, renderFactorySlot }) { const session = useSession ? useSession((v) => v) : null; const conversation = useConversation ? useConversation((v) => v) : null; const summaryBlank = useSessions ? useSessions((s) => s.byId?.[sessionId]?.blank) : false; const shellPhase = conversation && conversation.activeTargets && conversation.activeTargets.size > 0 || (session && !session.blank && !session.awaitingFirstTurn) || (session && session.running) ? 'active' : session && session.promptAttempted ? 'engaging' : 'blank'; const parentAvailabilityPending = session?.subagent?.address?.mode === 'continuable' && session?.subagent?.parentAvailable === void 0; const settling = shellPhase === 'blank' && session?.openState === 'loading' && summaryBlank !== true || parentAvailabilityPending; const hero = shellPhase === 'blank' && (session?.openState === 'open' || summaryBlank === true); if (typeof renderFactorySlot !== 'function') { return h('div', { style: { padding: 16, color: '#888' } }, `Session ${sessionId}`); } return renderFactorySlot('conversation.content', { variant: 'embedded', phase: settling ? 'settling' : hero ? 'hero' : 'active', hero: !!hero, }, { slots: { views: FixedChatConversationView } }); } // ---- Single Split Window Frame --------------------------------------- function SplitWindowFrame({ sessionId, index, total, ctx, settings, onClose, onMove, onSelectSession, isGlowing, renderSlot, SessionProvider, availableSessions, }) { const [sidebarOpen, setSidebarOpen] = useState(true); const [sessionRef, setSessionRef] = useState(null); const [loadError, setLoadError] = useState(null); const title = availableSessions?.byId?.[sessionId]?.displayTitle || availableSessions?.byId?.[sessionId]?.title || sessionId || 'Untitled chat'; useEffect(() => { if (!ctx?.sessions || !sessionId) return; const ctrl = new AbortController(); let ref = null; try { ref = ctx.sessions.retain(sessionId, { source: 'chatSplit', signal: ctrl.signal }); setSessionRef(ref); setLoadError(null); } catch (err) { setLoadError(err.message || String(err)); setSessionRef(null); } return () => { ctrl.abort(); if (ref) { try { ref.release(); } catch {} } }; }, [ctx, sessionId]); const sessionIdsList = useMemo(() => { if (!availableSessions?.ids) return []; return availableSessions.ids.filter((id) => !availableSessions.byId?.[id]?.blank); }, [availableSessions]); return h('div', { className: `dsh-split-window ${isGlowing ? 'glow' : ''}`, style: { minHeight: `${settings.minHeight}px` } }, // Top bar h('div', { className: 'dsh-split-win-bar' }, h('div', { className: 'dsh-split-win-title', title }, h('span', { style: { color: '#58a6ff', fontSize: 13 } }, '💬'), h('span', null, title) ), h('div', { className: 'dsh-split-win-controls' }, // Toggle per-chat sidebar h('button', { className: 'dsh-split-win-btn', onClick: () => setSidebarOpen((v) => !v), title: sidebarOpen ? "Collapse chat's sidebar" : "Open chat's sidebar" }, sidebarOpen ? '◀ Sidebar' : '▶ Sidebar'), // Move left / up in grid h('button', { className: 'dsh-split-win-btn', disabled: index === 0, onClick: () => onMove(index, -1), title: 'Move earlier in grid' }, '◀'), // Move right / down in grid h('button', { className: 'dsh-split-win-btn', disabled: index === total - 1, onClick: () => onMove(index, 1), title: 'Move later in grid' }, '▶'), // Close window h('button', { className: 'dsh-split-win-btn', onClick: () => onClose(sessionId), title: 'Close window' }, '✕') ) ), // Window body: per-chat sidebar + conversation h('div', { className: 'dsh-split-win-body' }, // Per-chat sidebar (session list for this window) h('div', { className: `dsh-split-sidebar ${sidebarOpen ? '' : 'collapsed'}` }, h('div', { className: 'dsh-split-sidebar-head' }, h('span', null, 'Chats'), h('span', { style: { opacity: 0.7 } }, `${sessionIdsList.length}`) ), h('ul', { className: 'dsh-split-sidebar-list' }, sessionIdsList.map((id) => { const isCurrent = id === sessionId; const openElsewhere = !isCurrent && store.getSessions().includes(id); const sTitle = availableSessions.byId[id]?.displayTitle || availableSessions.byId[id]?.title || id; return h('li', { key: id, className: `dsh-split-sidebar-item ${isCurrent ? 'current' : ''} ${openElsewhere ? 'open-elsewhere' : ''}`, onClick: () => { if (isCurrent) return; if (openElsewhere) { // Selecting an already open chat triggers glow highlight store.triggerGlow(id); } else { onSelectSession(index, id); } }, title: openElsewhere ? `${sTitle} (already open in another split window)` : sTitle }, h('span', { style: { width: 6, height: 6, borderRadius: '50%', flexShrink: 0, background: isCurrent ? '#58a6ff' : openElsewhere ? '#d29922' : '#8b949e' } }), h('span', { className: 'dsh-split-sidebar-item-title' }, sTitle) ); }) ) ), // Conversation pane h('div', { className: 'dsh-split-conversation-pane' }, loadError ? h('div', { style: { padding: 16, color: '#f85149', fontSize: 13 } }, `Failed to load conversation: ${loadError}`) : sessionRef && SessionProvider && typeof renderSlot === 'function' ? h(SessionProvider, { session: sessionRef }, renderSlot('dsh-chat-split.window', { sessionId }) ) : h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#888' } }, 'Loading chat...' ) ) ) ); } // ---- Main Panel Component -------------------------------------------- function SplitPanel(props) { const { ctx, renderSlot, SessionProvider } = props; const [settings, setSettings] = useState(() => store.getSettings()); const [sessions, setSessions] = useState(() => store.getSessions()); const [glowingSessionId, setGlowingSessionId] = useState(() => store.getGlowingSessionId()); const [availableSessions, setAvailableSessions] = useState(() => ctx?.sessions?.list?.getSnapshot?.() || { ids: [], byId: {} }); const gridRef = useRef(null); const [containerWidth, setContainerWidth] = useState(1000); useEffect(() => { installStyles(); store.setSplitActive(true); return () => { store.setSplitActive(false); }; }, []); useEffect(() => { return store.subscribe(() => { setSettings(store.getSettings()); setSessions(store.getSessions()); setGlowingSessionId(store.getGlowingSessionId()); }); }, []); // Subscribe to session list changes useEffect(() => { if (!ctx?.sessions?.list) return; const sub = ctx.sessions.list.subscribe?.(() => { setAvailableSessions(ctx.sessions.list.getSnapshot()); }); return () => { if (typeof sub === 'function') sub(); }; }, [ctx]); // Measure container width for responsive column calculation useEffect(() => { const el = gridRef.current; if (!el || typeof ResizeObserver === 'undefined') return; const ro = new ResizeObserver((entries) => { for (const entry of entries) { setContainerWidth(entry.contentRect.width); } }); ro.observe(el); return () => ro.disconnect(); }, []); // Calculate columns based on width and readability threshold (minWidth) const columnCount = useMemo(() => { const minW = Math.max(280, settings.minWidth || 420); const gap = settings.gap || 10; const availableW = containerWidth - 20; // 10px padding each side const cols = Math.floor((availableW + gap) / (minW + gap)); return Math.max(1, cols); }, [containerWidth, settings.minWidth, settings.gap]); const handleClose = useCallback((id) => { store.removeSession(id); }, []); const handleMove = useCallback((idx, dir) => { store.moveSession(idx, dir); }, []); const handleSelectSession = useCallback((windowIndex, newSessionId) => { const copy = [...store.getSessions()]; copy[windowIndex] = newSessionId; store.setSessions(copy); }, []); const handleAddAllKnown = useCallback(() => { const ids = availableSessions.ids.filter((id) => !availableSessions.byId[id]?.blank); store.setSessions(ids.slice(0, 6)); // take up to 6 }, [availableSessions]); return h('div', { className: 'dsh-split-container' }, // Main header h('div', { className: 'dsh-split-header' }, h('div', { className: 'dsh-split-header-left' }, h('span', { className: 'dsh-split-header-title' }, h('span', null, '⊞ Chat Split View'), h('span', { className: 'dsh-split-badge' }, `${sessions.length} chats`), h('span', { style: { fontSize: 11, color: '#8b949e' } }, `${columnCount} column${columnCount > 1 ? 's' : ''} (min-height: ${settings.minHeight}px)` ) ) ), h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, sessions.length === 0 && h('button', { className: 'dsh-split-btn dsh-split-btn-primary', onClick: handleAddAllKnown }, '+ Add open chats'), sessions.length > 0 && h('button', { className: 'dsh-split-btn', onClick: () => store.setSessions([]) }, 'Clear all') ) ), // Grid or Empty State sessions.length === 0 ? h('div', { className: 'dsh-split-empty' }, h('h3', null, 'No chats in split view'), h('p', { style: { maxWidth: 440, marginBottom: 16 } }, 'Add conversations to split view using the "..." menu on any session in the sidebar, or pick from recent chats below:' ), h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center', maxWidth: 600 } }, availableSessions.ids.filter((id) => !availableSessions.byId[id]?.blank).slice(0, 8).map((id) => { const sTitle = availableSessions.byId[id]?.displayTitle || availableSessions.byId[id]?.title || id; return h('button', { key: id, className: 'dsh-split-btn', onClick: () => store.addSession(id) }, `+ ${sTitle}`); }) ) ) : h('div', { ref: gridRef, className: 'dsh-split-grid', style: { gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`, gap: `${settings.gap || 10}px`, } }, sessions.map((id, index) => h(SplitWindowFrame, { key: `${id}-${index}`, sessionId: id, index, total: sessions.length, ctx, settings, onClose: handleClose, onMove: handleMove, onSelectSession: handleSelectSession, isGlowing: glowingSessionId === id, renderSlot, SessionProvider, availableSessions, }) ) ) ); } // ---- Settings Panel with Interactive Preview ------------------------- function SettingsPanel() { const [settings, setSettings] = useState(() => store.getSettings()); const [previewWidth, setPreviewWidth] = useState(720); const [mockChatCount, setMockChatCount] = useState(3); const handleChange = (key, val) => { const num = parseInt(val, 10); if (!isNaN(num)) { const patch = { [key]: num }; setSettings((prev) => ({ ...prev, ...patch })); store.updateSettings(patch); } }; // Preview columns calculation const minW = Math.max(200, settings.minWidth || 420); const gap = settings.gap || 10; const previewCols = Math.max(1, Math.floor((previewWidth + gap) / (minW + gap))); return h('div', { style: { padding: '8px 4px', maxWidth: 760 } }, h('h3', { style: { margin: '0 0 12px 0', fontSize: 16 } }, 'Chat Split Settings'), h('p', { style: { opacity: 0.8, fontSize: 13, marginBottom: 16 } }, 'Configure multi-chat split view dimensions, wrapping rules, and interactive preview.' ), // Controls h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 20 } }, // Minimum Height h('div', null, h('label', { style: { display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 6 } }, 'Minimum Height (px)'), h('input', { type: 'number', min: 250, max: 900, step: 10, value: settings.minHeight, onChange: (e) => handleChange('minHeight', e.target.value), style: { width: '100%', padding: '6px 8px', borderRadius: 6, background: 'var(--dsw-alias-bg-l1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', color: 'inherit', fontSize: 13 } }), h('div', { style: { fontSize: 11, opacity: 0.6, marginTop: 4 } }, 'Default: 396px. Ensures conversation readability vertically.') ), // Minimum Width (Readability Threshold) h('div', null, h('label', { style: { display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 6 } }, 'Readability Width Threshold (px)'), h('input', { type: 'number', min: 280, max: 800, step: 10, value: settings.minWidth, onChange: (e) => handleChange('minWidth', e.target.value), style: { width: '100%', padding: '6px 8px', borderRadius: 6, background: 'var(--dsw-alias-bg-l1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', color: 'inherit', fontSize: 13 } }), h('div', { style: { fontSize: 11, opacity: 0.6, marginTop: 4 } }, 'When viewport cannot fit another column at this width, chats wrap vertically into rows.') ), // Grid Gap h('div', null, h('label', { style: { display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 6 } }, 'Grid Gap (px)'), h('input', { type: 'number', min: 4, max: 30, value: settings.gap, onChange: (e) => handleChange('gap', e.target.value), style: { width: '100%', padding: '6px 8px', borderRadius: 6, background: 'var(--dsw-alias-bg-l1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', color: 'inherit', fontSize: 13 } }), h('div', { style: { fontSize: 11, opacity: 0.6, marginTop: 4 } }, 'Spacing between split windows in grid.') ) ), // Interactive Preview Section h('div', { style: { marginTop: 24 } }, h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } }, h('h4', { style: { margin: 0, fontSize: 14 } }, 'Interactive Layout Preview'), h('div', { style: { fontSize: 12, display: 'flex', gap: 12, alignItems: 'center' } }, h('span', { style: { color: '#58a6ff', fontWeight: 600 } }, `${previewCols} column${previewCols > 1 ? 's' : ''} at ${previewWidth}px width` ), h('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, 'Chats:', h('select', { value: mockChatCount, onChange: (e) => setMockChatCount(parseInt(e.target.value, 10)), style: { background: 'var(--dsw-alias-bg-l1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', color: 'inherit', padding: '2px 4px', borderRadius: 4 } }, [1, 2, 3, 4, 5, 6].map((n) => h('option', { key: n, value: n }, `${n}`)) ) ) ) ), h('div', { style: { marginBottom: 8 } }, h('label', { style: { display: 'block', fontSize: 11, opacity: 0.7, marginBottom: 4 } }, 'Drag slider to test viewport resizing & wrapping:'), h('input', { type: 'range', min: 360, max: 1200, value: previewWidth, onChange: (e) => setPreviewWidth(parseInt(e.target.value, 10)), style: { width: '100%', cursor: 'pointer' } }) ), // Resizable preview box h('div', { className: 'dsh-split-preview-box', style: { width: `${previewWidth}px`, maxWidth: '100%', overflow: 'hidden' } }, h('div', { style: { display: 'grid', gridTemplateColumns: `repeat(${previewCols}, minmax(0, 1fr))`, gap: `${settings.gap}px`, transition: 'all 0.2s ease' } }, Array.from({ length: mockChatCount }).map((_, i) => h('div', { key: i, className: 'dsh-split-mock-win', style: { height: `${Math.round(settings.minHeight * 0.55)}px` } }, h('div', { className: 'dsh-split-mock-bar' }, h('span', null, `Chat ${i + 1}`), h('span', { style: { opacity: 0.6 } }, '◀ ▶ ✕') ), h('div', { className: 'dsh-split-mock-body' }, h('div', { className: 'dsh-split-mock-side' }, h('div', { style: { height: 6, background: '#58a6ff', borderRadius: 3, marginBottom: 4 } }), h('div', { style: { height: 6, background: 'rgba(255,255,255,0.1)', borderRadius: 3, marginBottom: 4 } }), h('div', { style: { height: 6, background: 'rgba(255,255,255,0.1)', borderRadius: 3 } }) ), h('div', { className: 'dsh-split-mock-chat' }, h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, h('div', { style: { height: 12, width: '70%', background: 'rgba(255,255,255,0.08)', borderRadius: 4 } }), h('div', { style: { height: 18, width: '90%', background: 'rgba(31,111,235,0.15)', borderRadius: 4, alignSelf: 'flex-end' } }) ), h('div', { style: { height: 16, background: 'rgba(255,255,255,0.05)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.1)' } }) ) ) ) ) ) ) ) ); } // ---- Sidebar Panel List Icon ----------------------------------------- function SplitPanelIcon({ size }) { return h('svg', { 'data-dsh-panel-entry': PANEL_ID, viewBox: '0 0 16 16', width: size || 16, height: size || 16, fill: 'none', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, }, h('rect', { x: 1.5, y: 1.5, width: 13, height: 13, rx: 1.5 }), h('line', { x1: 8, y1: 1.5, x2: 8, y2: 14.5 }), h('line', { x1: 1.5, y1: 8, x2: 8, y2: 8 }) ); } // ---- Session Menu Item: Include / Exclude from Split View ------------ function SplitSessionMenuItem(props) { const { sessionId, useMenuOpenState } = props; const [, setMenuOpen] = useMenuOpenState ? useMenuOpenState() : [false, () => {}]; const [isInSplit, setIsInSplit] = useState(() => store.getSessions().includes(sessionId)); useEffect(() => { return store.subscribe(() => { setIsInSplit(store.getSessions().includes(sessionId)); }); }, [sessionId]); const handleToggle = (e) => { e?.stopPropagation?.(); setMenuOpen(false); if (isInSplit) { store.removeSession(sessionId); } else { store.addSession(sessionId); // If split mode is active and we re-selected an open chat, trigger glow if (store.isSplitActive()) { store.triggerGlow(sessionId); } } }; return h('button', { type: 'button', role: 'menuitem', className: 'dsh-split-menu-row', onClick: handleToggle, style: { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 12px', border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-primary, #e6edf3)', cursor: 'pointer', fontSize: 13, textAlign: 'left', borderRadius: 4 } }, h('span', { style: { color: isInSplit ? '#f85149' : '#58a6ff', fontSize: 14 } }, isInSplit ? '⊟' : '⊞' ), h('span', null, isInSplit ? 'Exclude from split view' : 'Include in split view') ); } // ---- Main Plugin Registration ---------------------------------------- return { inject: ['slots', 'sessions'], apply(ctx) { installStyles(); const disposers = []; // Observe main session retention changes to trigger blue glow when selecting an already-open chat in split mode if (ctx?.sessions?.list) { let lastMainId = null; const unsubscribeList = ctx.sessions.list.subscribe?.(() => { if (!store.isSplitActive()) return; const snapshot = ctx.sessions.list.getSnapshot(); const byId = snapshot?.byId || {}; const currentMainId = Object.keys(byId).find((id) => (byId[id]?.retainedBy?.mainView ?? 0) > 0); if (currentMainId && currentMainId !== lastMainId) { lastMainId = currentMainId; if (store.getSessions().includes(currentMainId)) { store.triggerGlow(currentMainId); } } }); if (typeof unsubscribeList === 'function') disposers.push(unsubscribeList); } // 1. Sidebar Panel entry disposers.push(ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: PANEL_ID, order: 75, label: 'Split view', }, SplitPanelIcon))); // 2. Child slot occupant for each split window disposers.push(ctx.slots.inject('dsh-chat-split.window', () => ctx.slots.register({ name: 'dsh-chat-split.window', }, SplitWindowConversationPanel))); // 3. Main keyed slot: Split View Grid disposers.push(ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: PANEL_ID, children: { 'dsh-chat-split.window': { kind: 'single', scope: 'session' } } }, (kitProps) => h(SplitPanel, { ...kitProps, ctx })))); // 4. Session menu item: Include / Exclude from Split View disposers.push(ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({ name: 'sidebar.workspaces.session.menu.item', id: 'dsh-chat-split-toggle', order: 450, }, SplitSessionMenuItem))); // 5. Settings section disposers.push(ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-chat-split', order: 175, label: 'Chat split', }, SettingsPanel))); return () => { disposers.forEach((d) => { try { d(); } catch {} }); }; } }; } });