window.__ModuleLoader__.load({ id: 'dsh-nav-fold', factory: (require) => { const module = { exports: {} }; const key = 'dsh-sidebar-nav-collapse'; // Preserve existing fold and order settings across the rename. function selectWorkspaceViews(list = {}, statuses = new Map(), workspaces = {}, unread = new Set(), limit = 20) { const all = list.byId || {}; const running = new Map(); const queue = []; for (const session of Object.values(all)) { const active = statuses.get?.(session.id)?.running ?? session.running; running.set(session.id, active === true); if (active === true) queue.push(session.id); } for (const [parentId, projection] of Object.entries(list.projectionsBySession || {})) { if (!running.has(parentId) || running.get(parentId)) continue; const children = projection?.values?.subagentCatalog || []; if (children.some((child) => (statuses.get?.(child.id)?.running ?? all[child.id]?.running) === true)) { running.set(parentId, true); queue.push(parentId); } } for (let index = 0; index < queue.length; index++) { const session = all[queue[index]]; if (session?.origin === 'subagent' && running.has(session.parentId) && !running.get(session.parentId)) { running.set(session.parentId, true); queue.push(session.parentId); } } const archived = new Set(workspaces.archivedSessionIds || []); const pinned = new Set(workspaces.pinnedSessionIds || []); const workspaceBySession = new Map(); for (const workspace of workspaces.items || []) { const label = workspace.title || workspace.path?.split(/[\\/]/).filter(Boolean).pop() || workspace.workspaceId || 'Ungrouped'; for (const id of workspace.sessionIds || []) workspaceBySession.set(id, label); } const rows = (list.ids || []).map((id) => all[id]).filter((session) => session && session.origin !== 'subagent' && !session.blank && !archived.has(session.id)).map((session) => { const status = statuses.get?.(session.id); const pendingKind = status?.pendingInteraction?.kind; const kind = ['approval', 'plan-review', 'question'].includes(pendingKind) ? 'waiting' : running.get(session.id) ? 'running' : (unread.has(session.id) || status?.completionUnread === true) ? 'unread' : 'recent'; return { ...session, kind, pinned: pinned.has(session.id), workspace: workspaceBySession.get(session.id) || session.cwd?.split(/[\\/]/).filter(Boolean).pop() || 'Ungrouped' }; }).sort((a, b) => ({ waiting: 0, running: 1, unread: 2, recent: 3 })[a.kind] - ({ waiting: 0, running: 1, unread: 2, recent: 3 })[b.kind] || (b.updatedAt || 0) - (a.updatedAt || 0) || a.id.localeCompare(b.id)); let recent = 0; return { activity: rows.filter((row) => row.kind !== 'recent' || recent++ < limit), favorites: rows.filter((row) => row.pinned) }; } const css = ` nav[class*="panelList"][data-sidebar-nav-collapsed] { position: relative; padding-bottom: 24px; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] { display: grid; grid-template-columns: repeat(auto-fit, 36px); align-items: center; align-content: start; justify-content: space-evenly; column-gap: 0; min-width: 0; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[class*="panelRow"] { width: 36px; min-width: 36px; padding-inline: 8px; justify-content: center; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[class*="panelRow"] [class*="panelTitle"] { display: none; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[class*="panelRow"] [class*="panelGlyph"] { flex: none; } nav[class*="panelList"] > [data-sidebar-nav-toggle] { position: absolute; bottom: 0; left: 0; width: 100%; height: 24px; display: flex; align-items: center; justify-content: center; gap: 8px; border: 0; background: transparent; color: inherit; cursor: pointer; } nav[class*="panelList"] > [data-sidebar-nav-toggle]::before, nav[class*="panelList"] > [data-sidebar-nav-toggle]::after { content: ""; flex: 1 1 0; height: 0.5px; background: currentColor; opacity: .16; } nav[class*="panelList"] > [data-sidebar-nav-toggle]::before { mask-image: linear-gradient(to right, transparent, #000 22%); } nav[class*="panelList"] > [data-sidebar-nav-toggle]::after { mask-image: linear-gradient(to left, transparent, #000 22%); } nav[class*="panelList"] > [data-sidebar-nav-toggle]:hover::before, nav[class*="panelList"] > [data-sidebar-nav-toggle]:hover::after { opacity: .3; } nav[class*="panelList"] > [data-sidebar-nav-toggle]:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; } nav[class*="panelList"] > [data-sidebar-nav-toggle] svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; animation: sidebarBreathe 3.8s ease-in-out infinite; transition: opacity 90ms ease; } @keyframes sidebarBreathe { 0%, 100% { opacity: .48; } 50% { opacity: .85; } } nav[class*="panelList"] [data-sidebar-toggle-center] { display: grid; place-items: center; width: 28px; min-width: 0; max-width: calc(100% - 32px); transition: width 260ms cubic-bezier(.22,1.12,.35,1); } nav[class*="panelList"] [data-sidebar-toggle-center] > * { grid-area: 1 / 1; } nav[class*="panelList"] [data-sidebar-label] { position: absolute; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; line-height: 1; opacity: 0; pointer-events: none; transition: opacity 90ms ease; } nav[class*="panelList"] [data-sidebar-label-active] { position: static; opacity: 1; } nav[class*="panelList"] [data-sidebar-show-label] svg { animation: none; opacity: 0; } nav[class*="panelList"] > button[class*="panelRow"][data-sidebar-dragging] { opacity: .45; } nav[class*="panelList"] > button[class*="panelRow"][data-sidebar-drop] { position: relative; transition: transform 120ms ease-out; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[data-sidebar-drop] { transform: scale(1.06); } nav[class*="panelList"] > button[data-sidebar-drop]::after { content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: 6px; border: 1px solid #69aaff; background: rgba(105,170,255,.14); box-shadow: 0 0 0 2px rgba(105,170,255,.26), 0 0 14px rgba(105,170,255,.4); } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[data-sidebar-drop="before"]::after { border-left-width: 3px; } nav[class*="panelList"][data-sidebar-nav-collapsed="true"] > button[data-sidebar-drop="after"]::after { border-right-width: 3px; } nav[class*="panelList"][data-sidebar-nav-collapsed="false"] > button[data-sidebar-drop="before"]::after { border-top-width: 3px; } nav[class*="panelList"][data-sidebar-nav-collapsed="false"] > button[data-sidebar-drop="after"]::after { border-bottom-width: 3px; } [data-dsh-workspace-tabs-row] { display: grid; grid-template-columns: 28px minmax(0, 1fr) 28px; align-items: center; gap: 4px; margin: 6px 12px 8px; min-width: 0; } [data-dsh-workspace-tabs-row]::before { content: ''; width: 28px; height: 1px; } [data-dsh-workspace-tabs-row][data-collapsed="true"] { display: flex; justify-content: flex-end; margin-top: 3px; margin-bottom: 4px; } [data-dsh-workspace-tabs-row][data-collapsed="true"]::before { display: none; } .kB3qhq_root.kB3qhq_collapsed [data-dsh-workspace-tabs-row] { display: none; } [data-dsh-workspace-tabs] { --dsh-view-index: 0; position: relative; z-index: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; flex: 1; min-width: 0; margin: 0; padding: 3px; min-height: 31px; border-radius: 8px; background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.10)); box-sizing: border-box; } [data-dsh-workspace-tabs][hidden] { display: none; } [data-dsh-workspace-tabs-toggle] { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 2px; width: 28px; min-height: 28px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; transition: background 140ms ease, color 140ms ease; } [data-dsh-workspace-tabs-row][data-collapsed="true"] [data-dsh-workspace-tabs-toggle] { width: 32px; } [data-dsh-workspace-tabs-toggle]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.10)); color: var(--dsw-alias-label-primary, inherit); } [data-dsh-workspace-tabs-toggle]:focus-visible { outline: 2px solid var(--dsw-alias-label-primary, currentColor); outline-offset: -2px; } [data-dsh-workspace-tabs-toggle] svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } [data-dsh-workspace-tabs-toggle] svg[data-current-view] { width: 13px; height: 13px; } [data-dsh-workspace-tabs]::before { content: ''; position: absolute; z-index: -1; left: 3px; top: 3px; bottom: 3px; width: calc((100% - 6px) / 3); border-radius: 6px; background: var(--dsw-specific-sidebar-fill, rgba(150,150,150,.16)); box-shadow: 0 1px 3px rgba(0,0,0,.10), inset 0 0 0 .5px rgba(128,128,128,.10); transform: translateX(calc(var(--dsh-view-index) * 100%)); transition: transform 280ms cubic-bezier(.22,1.08,.32,1), box-shadow 180ms ease; } [data-dsh-workspace-tabs] > button[data-view] { border: 0; border-radius: 5px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; line-height: 1; cursor: pointer; padding: 5px 2px; min-width: 0; display: flex; align-items: center; justify-content: center; transition: color 140ms ease, transform 150ms ease; } [data-dsh-workspace-tabs] > button[data-view]:hover, [data-dsh-workspace-tabs] > button[data-view][aria-pressed="true"] { color: var(--dsw-alias-label-primary, inherit); } [data-dsh-workspace-tabs] > button[data-view] svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } [data-dsh-workspace-tabs] > button[data-view]:active { transform: scale(.97); } [data-dsh-workspace-tabs] > button[data-view]:focus-visible { outline: 2px solid var(--dsw-alias-label-primary, currentColor); outline-offset: -2px; } @keyframes dshWorkspaceEnter { from { opacity: .72; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } [data-dsh-workspace-view] { padding: 2px 8px 14px; min-width: 0; overflow-y: auto; animation: dshWorkspaceEnter 180ms cubic-bezier(.16,1,.3,1) both; } .dsh-nav-fold-session { position: relative; width: 100%; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-primary, inherit); min-width: 0; transition: background 140ms ease; } .dsh-nav-fold-session:hover, .dsh-nav-fold-session-selected { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.10)); } .dsh-nav-fold-session-main { display: flex; flex-direction: column; align-items: stretch; width: 100%; box-sizing: border-box; text-align: left; border: 0; border-radius: inherit; padding: 8px 10px; background: transparent; color: inherit; font: inherit; cursor: pointer; min-width: 0; } .dsh-nav-fold-session-main:active { transform: scale(.985); } .dsh-nav-fold-session-main:focus-visible { outline: 2px solid var(--dsw-alias-label-primary, currentColor); outline-offset: -2px; } .dsh-nav-fold-session-title { font-size: 12px; line-height: 1.4; padding-right: 82px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .dsh-nav-fold-session-subtitle { display: flex; justify-content: space-between; gap: 8px; margin-top: 2px; color: color-mix(in srgb, currentColor 72%, transparent); font-size: 11px; line-height: 1.5; white-space: nowrap; overflow: hidden; } .dsh-nav-fold-session-actions { position: absolute; top: 1px; right: 7px; display: flex; gap: 2px; opacity: 0; pointer-events: none; transition: opacity 120ms ease; } .dsh-nav-fold-session:hover .dsh-nav-fold-session-actions, .dsh-nav-fold-session:focus-within .dsh-nav-fold-session-actions { opacity: 1; pointer-events: auto; } .dsh-nav-fold-session-actions button { width: 24px; height: 24px; padding: 4px; border: 0; border-radius: 5px; background: transparent; color: inherit; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .dsh-nav-fold-session-actions button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.15)); } .dsh-nav-fold-session-actions button:focus-visible { outline: 2px solid var(--dsw-alias-label-primary, currentColor); outline-offset: -2px; } .dsh-nav-fold-session-actions button:disabled { cursor: default; opacity: .55; } .dsh-nav-fold-session-actions svg { width: 14px; height: 14px; } @media (hover: none) { .dsh-nav-fold-session-actions { opacity: 1; pointer-events: auto; } } .dsh-nav-fold-session [role="alert"] { display: block; padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-label-danger, #b94a48); } .dsh-nav-fold-archive-error { display: block; margin-top: 8px; font-size: 12px; line-height: 1.4; color: var(--dsw-alias-label-danger, #b94a48); } .dsh-nav-fold-session-subtitle span { overflow: hidden; text-overflow: ellipsis; } .dsh-nav-fold-state-dot { flex: none; width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; } .dsh-nav-fold-state-dot > * { flex: none; } @keyframes dshNavFoldRunningPulse { 0%, 100% { opacity: .58; transform: scale(.90); } 50% { opacity: 1; transform: scale(1); } } .dsh-nav-fold-state-dot-running { animation: dshNavFoldRunningPulse 1.7s ease-in-out infinite; transform-origin: center; } @media (prefers-reduced-motion: reduce) { .dsh-nav-fold-state-dot-running { animation: none; opacity: 1; transform: none; } } .dsh-nav-fold-empty { padding: 22px 12px; margin: 0; color: color-mix(in srgb, currentColor 72%, transparent); font-size: 11px; text-align: center; } @media (prefers-reduced-motion: reduce) { [data-dsh-workspace-view] { animation: none; } .dsh-nav-fold-session { transition: none; } .dsh-nav-fold-session-actions { transition: none; } .dsh-nav-fold-session-main:active { transform: none; } } @media (prefers-reduced-motion: reduce) { [data-dsh-workspace-tabs-row] [data-dsh-workspace-tabs-toggle], [data-dsh-workspace-tabs]::before, [data-dsh-workspace-tabs] > button[data-view] { transition: none; } } @media (prefers-reduced-motion: reduce) { nav[class*="panelList"] > [data-sidebar-nav-toggle] svg { animation: none; opacity: .75; } nav[class*="panelList"] [data-sidebar-toggle-center] { transition: none; } nav[class*="panelList"] > button[data-sidebar-drop] { transition: none; transform: none; } nav[class*="panelList"] > [data-dsh-workspace-tabs-toggle] { transition: none; } } `; function apply(ctx) { ctx.effect(() => { const style = document.createElement('style'); style.dataset.pluginCss = key; style.textContent = css; document.head.append(style); const managed = new Set(); const rejected = new WeakSet(); const remountedAt = new WeakMap(); const originals = new WeakMap(); const rows = (nav) => [...nav.querySelectorAll(':scope > button[class*="panelRow"]')]; let savedOrder = []; try { const value = JSON.parse(window.localStorage.getItem(`${key}:order`) || '[]'); if (Array.isArray(value)) savedOrder = value.filter((name) => typeof name === 'string'); } catch {} // ponytail: labels identify rows; use stable slot IDs if duplicate/renamed labels need independent persistent order. const syncRows = (nav, restore = false) => { let unseen = 0; for (const row of rows(nav)) { if (!originals.has(row)) originals.set(row, { title: row.getAttribute('title'), suppressed: false, draggable: row.getAttribute('draggable'), order: row.style.order }); const original = originals.get(row); const current = row.getAttribute('title'); if (!original.suppressed || current !== null) original.title = current; const title = restore || !collapsed ? original.title : null; original.suppressed = !restore && collapsed; if (title === null && current !== null) row.removeAttribute('title'); else if (current !== title) row.setAttribute('title', title); if (restore) { if (original.draggable === null) row.removeAttribute('draggable'); else row.setAttribute('draggable', original.draggable); row.style.order = original.order; row.removeAttribute('data-sidebar-dragging'); row.removeAttribute('data-sidebar-drop'); } else { if (!row.draggable) row.draggable = true; const position = savedOrder.indexOf(row.getAttribute('aria-label')); const order = String(position < 0 ? savedOrder.length + ++unseen : position + 1); if (row.style.order !== order) row.style.order = order; } } }; let collapsed = true; try { collapsed = window.localStorage.getItem(key) !== 'expanded'; } catch {} let hovered = null, pointerRow = null, focusedRow = null; const paint = () => { hovered = pointerRow = focusedRow = null; for (const nav of managed) { nav.dataset.sidebarNavCollapsed = String(collapsed); const button = nav.querySelector(':scope > [data-sidebar-nav-toggle]'); if (!button) continue; button.setAttribute('aria-expanded', String(!collapsed)); button.setAttribute('aria-label', collapsed ? '展开全局面板 / Expand global panels' : '折叠全局面板 / Collapse global panels'); button.innerHTML = ``; syncRows(nav); } }; const showLabel = (row) => { if (!collapsed || !managed.has(row?.parentElement)) row = null; hovered = row; for (const nav of managed) { if (row?.parentElement === nav) syncRows(nav); const button = nav.querySelector(':scope > [data-sidebar-nav-toggle]'); if (!button) continue; const labels = [...button.querySelectorAll('[data-sidebar-label]')]; const active = labels.find((label) => label.hasAttribute('data-sidebar-label-active')); if (!row || row.parentElement !== nav) { active?.removeAttribute('data-sidebar-label-active'); button.querySelector('[data-sidebar-toggle-center]').style.width = '28px'; button.removeAttribute('data-sidebar-show-label'); continue; } const name = row.getAttribute('aria-label') || row.textContent.trim(); const center = button.querySelector('[data-sidebar-toggle-center]'); if (active?.textContent === name) { center.style.width = `${Math.max(28, Number(active.dataset.sidebarFullWidth) + 16)}px`; continue; } const next = labels.find((label) => label !== active); next.textContent = name; next.style.maxWidth = 'none'; const labelWidth = next.getBoundingClientRect().width; next.style.maxWidth = ''; next.dataset.sidebarFullWidth = String(labelWidth); center.style.width = `${Math.max(28, labelWidth + 16)}px`; next.dataset.sidebarLabelActive = ''; active?.removeAttribute('data-sidebar-label-active'); button.dataset.sidebarShowLabel = ''; } }; const panelRow = (element) => { const row = element?.closest?.('button[class*="panelRow"]'); return managed.has(row?.parentElement) ? row : null; }; const onPointerOver = (event) => { pointerRow = panelRow(event.target); if (pointerRow) showLabel(pointerRow); }; const onPointerOut = (event) => { const row = panelRow(event.target); if (row && !row.contains(event.relatedTarget)) { pointerRow = panelRow(event.relatedTarget); showLabel(pointerRow || (focusedRow?.isConnected && focusedRow)); } }; const onFocusIn = (event) => { focusedRow = panelRow(event.target); if (focusedRow) showLabel(pointerRow || focusedRow); }; const onFocusOut = (event) => { if (panelRow(event.target)) { focusedRow = panelRow(event.relatedTarget); showLabel(pointerRow || focusedRow); } }; const animations = new Set(); const animateLayout = (update) => { if (typeof document.body.animate !== 'function' || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) { for (const animation of animations) animation.cancel(); animations.clear(); update(); return; } const before = [...managed].map((nav) => ({ nav, height: window.getComputedStyle(nav).height, items: rows(nav).map((row) => [row, row.getBoundingClientRect()]), })); for (const animation of animations) animation.cancel(); animations.clear(); update(); const after = before.map(({ nav, height, items }) => ({ nav, height, items, targetHeight: window.getComputedStyle(nav).height, targets: new Map(rows(nav).map((row) => [row, row.getBoundingClientRect()])), })); const play = (element, frames) => { const animation = element.animate(frames, { duration: 280, easing: 'cubic-bezier(.4,0,.2,1)' }); animations.add(animation); animation.onfinish = () => animations.delete(animation); animation.oncancel = () => animations.delete(animation); }; for (const { nav, height, items, targetHeight, targets } of after) { if (height !== targetHeight) play(nav, [{ height }, { height: targetHeight }]); for (const [row, rect] of items) { const target = targets.get(row); if (!target) continue; const dx = rect.left - target.left, dy = rect.top - target.top; if (dx || dy) play(row, [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'translate(0, 0)' }]); } } }; const clear = (nav) => { syncRows(nav, true); nav.querySelector('[data-sidebar-nav-toggle]')?.remove(); delete nav.dataset.sidebarNavCollapsed; managed.delete(nav); }; let dragging = null; const visualRows = (nav) => rows(nav).sort((a, b) => Number(a.style.order) - Number(b.style.order)); const saveOrder = (ordered) => { savedOrder = ordered.map((row) => row.getAttribute('aria-label') || ''); try { window.localStorage.setItem(`${key}:order`, JSON.stringify(savedOrder)); } catch {} for (const item of managed) syncRows(item); }; const clearDrag = () => { if (!dragging) return; dragging.nav.querySelector('[data-sidebar-drop]')?.removeAttribute('data-sidebar-drop'); dragging.row.removeAttribute('data-sidebar-dragging'); dragging = null; }; const dragRow = (event) => { const row = event.target.closest?.('button[class*="panelRow"]'); return row?.parentElement === dragging?.nav ? row : null; }; const onDragStart = (event) => { const row = event.target.closest?.('button[class*="panelRow"]'); if (!row || !managed.has(row.parentElement)) return; dragging = { row, nav: row.parentElement }; row.dataset.sidebarDragging = ''; if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', row.getAttribute('aria-label') || ''); } }; const onDragOver = (event) => { const row = dragRow(event); if (!row || row === dragging.row) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; const rect = row.getBoundingClientRect(); const before = dragging.nav.dataset.sidebarNavCollapsed === 'true' ? event.clientX < (rect.left + rect.right) / 2 : event.clientY < (rect.top + rect.bottom) / 2; dragging.nav.querySelector('[data-sidebar-drop]')?.removeAttribute('data-sidebar-drop'); row.dataset.sidebarDrop = before ? 'before' : 'after'; }; const onDrop = (event) => { const row = dragRow(event); if (!row || row === dragging.row) return; event.preventDefault(); const nav = dragging.nav; const side = row.dataset.sidebarDrop || 'before'; const ordered = visualRows(nav).filter((item) => item !== dragging.row); ordered.splice(ordered.indexOf(row) + (side === 'after' ? 1 : 0), 0, dragging.row); clearDrag(); animateLayout(() => saveOrder(ordered)); }; const onKeyDown = (event) => { if (!event.altKey || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return; const row = event.target.closest?.('button[class*="panelRow"]'); const nav = row?.parentElement; if (!managed.has(nav)) return; const ordered = visualRows(nav); const index = ordered.indexOf(row); const next = index + (event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1); if (next < 0 || next >= ordered.length) return; event.preventDefault(); ordered.splice(index, 1); ordered.splice(next, 0, row); animateLayout(() => saveOrder(ordered)); }; // ponytail: native grid follows sidebar width; restore animated resize only after host-safe ResizeObserver validation. const sync = () => { let sidebarMounted = false; for (const nav of managed) if (!nav.isConnected || rejected.has(nav)) clear(nav); for (const nav of document.querySelectorAll('nav[class*="panelList"]')) { if (rejected.has(nav) || !/^(全局面板|Global panels)$/.test(nav.getAttribute('aria-label') || '')) continue; if (!managed.has(nav)) { managed.add(nav); sidebarMounted = true; } const existing = nav.querySelector(':scope > [data-sidebar-nav-toggle]'); if (existing) { if (nav.lastElementChild !== existing) nav.append(existing); continue; } const button = document.createElement('button'); button.type = 'button'; button.dataset.sidebarNavToggle = ''; button.addEventListener('click', () => animateLayout(() => { collapsed = !collapsed; try { window.localStorage.setItem(key, collapsed ? 'collapsed' : 'expanded'); } catch {} paint(); })); nav.append(button); paint(); } for (const nav of managed) syncRows(nav); if (hovered) showLabel(hovered.isConnected ? hovered : null); if (sidebarMounted) window.dispatchEvent(new window.Event('dsh-nav-fold:sidebar-ready')); }; sync(); let pendingSync = null; const navSelector = 'nav[class*="panelList"]', rowSelector = 'button[class*="panelRow"]'; const observer = new window.MutationObserver((records) => { const contains = (node, selector) => node.nodeType === 1 && (node.matches(selector) || node.querySelector(selector)); const rowChanges = new Set(records.filter((record) => record.type === 'childList' && record.target.matches?.(navSelector) && [...record.addedNodes, ...record.removedNodes].some((node) => contains(node, rowSelector))).map((record) => record.target)); let relevant = false; for (const record of records) { const target = record.target; if (record.type === 'attributes') { if (target.matches?.(navSelector) || (target.matches?.(rowSelector) && managed.has(target.parentElement))) relevant = true; continue; } if (target.closest?.('[data-sidebar-nav-toggle]')) continue; const changed = [...record.addedNodes, ...record.removedNodes]; if (target.matches?.(navSelector) && managed.has(target)) { const removed = [...record.removedNodes].some((node) => node.hasAttribute?.('data-sidebar-nav-toggle') && node.parentElement !== target); if (removed) { const now = Date.now(); // ponytail: one quick host rebuild is allowed; repeated removal within 1s falls back to native nav. if (!rowChanges.has(target) || now - (remountedAt.get(target) || 0) < 1000) rejected.add(target); else remountedAt.set(target, now); relevant = true; } else if (changed.some((node) => contains(node, rowSelector))) relevant = true; } else if (changed.some((node) => contains(node, navSelector))) relevant = true; } if (relevant && pendingSync === null) pendingSync = window.setTimeout(() => { pendingSync = null; sync(); }, 0); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-label'] }); document.body.addEventListener('pointerover', onPointerOver); document.body.addEventListener('pointerout', onPointerOut); document.body.addEventListener('focusin', onFocusIn); document.body.addEventListener('focusout', onFocusOut); document.body.addEventListener('dragstart', onDragStart); document.body.addEventListener('dragover', onDragOver); document.body.addEventListener('drop', onDrop); document.body.addEventListener('dragend', clearDrag); document.body.addEventListener('keydown', onKeyDown); return () => { observer.disconnect(); if (pendingSync !== null) window.clearTimeout(pendingSync); for (const animation of animations) animation.cancel(); animations.clear(); clearDrag(); document.body.removeEventListener('pointerover', onPointerOver); document.body.removeEventListener('pointerout', onPointerOut); document.body.removeEventListener('focusin', onFocusIn); document.body.removeEventListener('focusout', onFocusOut); document.body.removeEventListener('dragstart', onDragStart); document.body.removeEventListener('dragover', onDragOver); document.body.removeEventListener('drop', onDrop); document.body.removeEventListener('dragend', clearDrag); document.body.removeEventListener('keydown', onKeyDown); for (const nav of managed) clear(nav); style.remove(); }; }, 'dsh-nav-fold: global navigation'); if (!ctx.slots?.inject) return; const form = ctx.configForms?.get?.('nav-fold'); const accepted = () => form?.getSnapshot?.()?.status === 'ready' && form.getSnapshot().value?.workspaceViewsEnabled === true; const React = require('react'); const { StateDot, Menu, MenuItemButton, IconEllipsisOutlineRegular, IconPinOutlineRegular, IconPinFillRegular, IconArchiveOutlineRegular, Modal, Button } = require('@deepseek-ai/dsh-client-ui-primitives'); let explicitRetryApproved = false; let recheckApprovedRetry = () => {}; if (form) ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-nav-fold' }, function WorkspaceSettings({ view }) { const snapshot = React.useSyncExternalStore((listener) => form.subscribe(listener), () => form.getSnapshot(), () => form.getSnapshot()); const saved = snapshot.status === 'ready' && snapshot.value?.workspaceViewsEnabled === true; const [draft, setDraft] = React.useState(saved); const [message, setMessage] = React.useState(''); React.useEffect(() => { setDraft(saved); setMessage(''); }, [saved, snapshot.revision]); if (view !== 'page') return null; return React.createElement('section', { 'data-dsh-nav-fold-settings': '' }, React.createElement('h3', null, '工作区筛选视图 / Workspace views'), React.createElement('p', null, '默认关闭。仅在这里保存开启;检测到侧栏冲突时自动撤出,原生工作区保持原状。'), React.createElement('label', null, React.createElement('input', { type: 'checkbox', checked: draft, disabled: snapshot.status !== 'ready' || !snapshot.writable, onChange: (event) => setDraft(event.target.checked) }), ' 启用工作区、活动与收藏三栏'), React.createElement('button', { type: 'button', disabled: snapshot.status !== 'ready' || !snapshot.writable || draft === saved, onClick: async () => { const retry = draft && !saved; try { const success = await form.set('workspaceViewsEnabled', draft); if (!success) { setMessage('未能保存,请重试'); return; } setMessage(''); if (retry) { explicitRetryApproved = true; recheckApprovedRetry(); } } catch { explicitRetryApproved = false; setMessage('未能保存,请重试'); } } }, '保存 / Save'), snapshot.status !== 'ready' && React.createElement('p', { role: 'status' }, '设置尚不可用;工作区保持原生视图。'), message && React.createElement('p', { role: 'alert' }, message)); })); ctx.slots.inject('sidebar.workspaces', () => { let selected = 'workspace'; let collapsed = false; let enabled = accepted(); try { const saved = window.localStorage.getItem('dsh-nav-fold:workspace-mode'); if (saved === 'activity' || saved === 'favorites') selected = saved; collapsed = window.localStorage.getItem('dsh-nav-fold:workspace-tabs-collapsed') === 'true'; } catch {} let bar = null, tabs = null, foldButton = null, observedRoot = null, observedRegion = null, observedFooter = null, observer = null, unregister = null, registeredMode = null; let slotMutationDepth = 0, missedSlotChange = false, disposed = false; const mutateOwnSlot = (action) => { slotMutationDepth++; try { return action(); } finally { slotMutationDepth--; if (!slotMutationDepth && missedSlotChange) { missedSlotChange = false; Promise.resolve().then(() => { if (!disposed) onSlotChange(); }); } } }; let quarantined = false; let sawSavedOffAfterConflict = false; const quarantine = () => { if (quarantined) return; quarantined = true; sawSavedOffAfterConflict = false; explicitRetryApproved = false; if (form?.getSnapshot?.()?.status === 'ready' && form.getSnapshot().writable) { Promise.resolve(form.set('workspaceViewsEnabled', false)).catch(() => {}); } }; const slotName = 'sidebar.workspaces'; const ownRegistrant = 'dsh-nav-fold:workspace-views'; // rc.5's live native WorkspaceBrowser registers as fiber `Gb` in this shipped build. // Unknown builds stay native-only rather than trusting peer-supplied locale/children. const nativeEntry = (entry) => entry?.registrant === 'Gb' && entry.locale === 'workspace' && entry.children?.['sidebar.workspaces.session.menu.item'] && entry.children?.['sidebar.workspaces.session.row.action']; const slotSafe = (ours = false) => { const spec = ctx.slots.spec?.(slotName); if (spec?.kind !== 'single' || spec.scope !== 'root') return false; const entries = ctx.slots.entries?.(slotName); if (!Array.isArray(entries) || entries.length !== (ours ? 2 : 1) || entries.filter(nativeEntry).length !== 1) return false; if (ours && (entries.filter((entry) => entry.registrant === ownRegistrant).length !== 1 || ctx.slots.entriesOfSlot?.(slotName)?.[0]?.registrant !== ownRegistrant)) return false; return true; }; const labels = [['workspace', '工作区', 'Workspaces'], ['activity', '活动', 'Activity'], ['favorites', '收藏', 'Favorites']]; const icons = { workspace: '', activity: '', favorites: '' }; const identity = (value) => value; function SessionRow({ row, currentId, kindText, dotState, english }) { const [busy, setBusy] = React.useState(false); const busyRef = React.useRef(false); const [error, setError] = React.useState(''); const [archiveRequest, setArchiveRequest] = React.useState(false); const [menuOpen, setMenuOpen] = React.useState(false); const pin = async () => { if (busyRef.current || archiveRequest) return; const action = row.pinned ? 'unpinSession' : 'pinSession'; if (typeof ctx.uiWorkspace?.[action] !== 'function') { setError(english ? 'Pin action unavailable' : '收藏操作不可用'); return; } busyRef.current = true; setBusy(true); setError(''); try { await ctx.uiWorkspace[action](row.id); } catch { setError(english ? 'Could not update favorite. Try again.' : '收藏未更新,请重试'); } finally { busyRef.current = false; setBusy(false); } }; const archive = async () => { setMenuOpen(false); if (busyRef.current || archiveRequest) return; if (typeof ctx.uiWorkspace?.archiveSession !== 'function') { setError(english ? 'Archive action unavailable' : '归档操作不可用'); return; } busyRef.current = true; setBusy(true); setError(''); try { await ctx.uiWorkspace.archiveSession(row.id); } catch (reason) { if (reason instanceof Error && reason.name === 'WorkspaceArchiveError' && reason.rpcError?.code === 'workspace/session-active') setArchiveRequest(true); else setError(english ? 'Could not archive session. Try again.' : '归档失败,请重试'); } finally { busyRef.current = false; setBusy(false); } }; const closeArchive = () => { if (!busyRef.current) { setArchiveRequest(false); setError(''); } }; const confirmArchive = async () => { if (busyRef.current || !archiveRequest) return; busyRef.current = true; setBusy(true); setError(''); try { await ctx.uiWorkspace.archiveSession(row.id, { stopActivity: true }); setArchiveRequest(false); } catch { setError(english ? 'Could not stop and archive. Try again.' : '停止并归档失败,请重试'); } finally { busyRef.current = false; setBusy(false); } }; const pinLabel = row.pinned ? (english ? 'Unpin session' : '取消收藏会话') : (english ? 'Pin session' : '收藏会话'); const copyId = async () => { setMenuOpen(false); setError(''); try { await window.navigator.clipboard.writeText(row.id); } catch { setError(english ? 'Could not copy session ID. Try again.' : '复制会话 ID 失败,请重试'); } }; return React.createElement('div', { className: `dsh-nav-fold-session${row.id === currentId ? ' dsh-nav-fold-session-selected' : ''}`, onContextMenu: (event) => { event.preventDefault(); setMenuOpen(true); } }, React.createElement('button', { type: 'button', className: 'dsh-nav-fold-session-main', onClick: () => ctx.uiWorkspace?.openSession?.(row.id), 'aria-current': row.id === currentId ? 'page' : undefined, 'aria-label': [row.title || row.id, row.workspace, kindText[row.kind]].filter(Boolean).join(' · ') }, React.createElement('span', { className: 'dsh-nav-fold-session-title' }, row.title || row.id), React.createElement('span', { className: 'dsh-nav-fold-session-subtitle' }, React.createElement('span', null, row.workspace), dotState[row.kind] && React.createElement('span', { className: `dsh-nav-fold-state-dot${row.kind === 'running' ? ' dsh-nav-fold-state-dot-running' : ''}`, role: 'img', 'aria-label': kindText[row.kind], title: kindText[row.kind] }, React.createElement(StateDot, { state: dotState[row.kind] })))), React.createElement('span', { className: 'dsh-nav-fold-session-actions' }, React.createElement(Menu, { open: menuOpen, onClose: () => setMenuOpen(false), portal: true, closeOnPointerLeave: true, anchor: React.createElement('button', { type: 'button', 'data-session-menu': '', 'aria-label': english ? `Actions for ${row.title || row.id}` : `${row.title || row.id}的操作`, 'aria-haspopup': 'menu', 'aria-expanded': menuOpen, onClick: () => setMenuOpen((value) => !value) }, React.createElement(IconEllipsisOutlineRegular, { size: 14 })) }, React.createElement(MenuItemButton, { onSelect: () => { setMenuOpen(false); ctx.uiWorkspace?.openSession?.(row.id); } }, english ? 'Open session' : '打开会话'), React.createElement(MenuItemButton, { onSelect: () => { setMenuOpen(false); void pin(); }, icon: row.pinned ? React.createElement(IconPinFillRegular, {}) : React.createElement(IconPinOutlineRegular, {}) }, pinLabel), React.createElement(MenuItemButton, { onSelect: archive, icon: React.createElement(IconArchiveOutlineRegular, { size: 14 }) }, english ? 'Archive session' : '归档会话'), window.navigator?.clipboard?.writeText && React.createElement(MenuItemButton, { onSelect: copyId }, english ? 'Copy session ID' : '复制会话 ID')), React.createElement('button', { type: 'button', 'data-session-archive': '', disabled: busy || archiveRequest || typeof ctx.uiWorkspace?.archiveSession !== 'function', 'aria-label': english ? 'Archive session' : '归档会话', title: english ? 'Archive session' : '归档会话', onClick: archive }, React.createElement(IconArchiveOutlineRegular, { size: 14 })), React.createElement('button', { type: 'button', 'data-session-pin': '', disabled: busy || archiveRequest || typeof ctx.uiWorkspace?.[row.pinned ? 'unpinSession' : 'pinSession'] !== 'function', 'aria-pressed': row.pinned, 'aria-label': pinLabel, title: pinLabel, onClick: pin }, React.createElement(row.pinned ? IconPinFillRegular : IconPinOutlineRegular, { size: 14 }))), archiveRequest && React.createElement(Modal, { open: true, onClose: closeArchive, closeLabel: english ? 'Close' : '关闭', title: english ? 'Stop work and archive session?' : '停止任务并归档会话?', description: english ? `Archiving ${row.title || row.id} will stop its running work. Continue?` : `归档${row.title || row.id}将停止该会话正在运行的任务。是否继续?`, footer: React.createElement(React.Fragment, null, React.createElement(Button, { variant: 'outline', disabled: busy, onClick: closeArchive }, english ? 'Cancel' : '取消'), React.createElement(Button, { variant: 'outline', disabled: busy, onClick: confirmArchive }, english ? 'Stop and archive' : '归档并停止')) }, error && React.createElement('span', { role: 'alert', className: 'dsh-nav-fold-archive-error' }, error)), !archiveRequest && error && React.createElement('span', { role: 'alert' }, error)); } function WorkspaceView(props) { const list = props.useSessions?.(identity) || {}; const statuses = props.useSessionStatus?.(identity) || new Map(); const workspaces = props.useWorkspaces?.(identity) || {}; const panelActive = props.usePanelInfo?.((info) => info.activePanelId !== null) === true; const currentId = panelActive ? null : Object.values(list.byId || {}).find((session) => (session.retainedBy?.mainView ?? 0) > 0)?.id; const views = selectWorkspaceViews(list, statuses, workspaces, new Set()); const rows = selected === 'favorites' ? views.favorites : views.activity; const english = document.documentElement.lang.toLowerCase().startsWith('en'); const kindText = { waiting: english ? 'Waiting' : '待处理', running: english ? 'Running' : '运行中', unread: english ? 'Completed, unread' : '完成未读', error: english ? 'Error stopped' : '错误停止' }; const dotState = { waiting: 'warning', running: 'ongoing', unread: 'done', error: 'error' }; const loading = list.phase === 'pending' || workspaces.phase === 'pending'; return React.createElement('section', { 'data-dsh-workspace-view': selected, 'aria-busy': loading, 'aria-label': selected === 'favorites' ? (english ? 'Favorites' : '收藏') : (english ? 'Activity' : '活动') }, rows.length ? rows.map((row) => React.createElement(SessionRow, { row, currentId, kindText, dotState, english, key: row.id })) : React.createElement('p', { className: 'dsh-nav-fold-empty' }, loading ? (english ? 'Loading sessions…' : '正在加载会话…') : selected === 'favorites' ? (english ? 'No pinned sessions' : '暂无收藏会话') : (english ? 'No recent sessions' : '暂无近期会话'))); } const syncSelection = () => { if (unregister && (!enabled || !bar || selected === 'workspace' || registeredMode !== selected)) { const dispose = unregister; unregister = null; registeredMode = null; mutateOwnSlot(dispose); } if (enabled && !quarantined && bar && selected !== 'workspace' && !unregister) { if (!slotSafe()) { quarantine(); ensure(); return; } unregister = mutateOwnSlot(() => ctx.slots.register({ name: slotName, priority: -100, registrant: ownRegistrant }, WorkspaceView)); registeredMode = selected; if (!slotSafe(true)) { quarantine(); ensure(); return; } } if (!bar) return; const current = labels.find(([mode]) => mode === selected); bar.dataset.collapsed = String(collapsed); tabs.hidden = collapsed; tabs.style.setProperty('--dsh-view-index', String(labels.findIndex(([mode]) => mode === selected))); foldButton.setAttribute('aria-expanded', String(!collapsed)); foldButton.setAttribute('aria-label', `${current[1]} · ${collapsed ? '展开' : '折叠'}工作区视图切换 / ${collapsed ? 'Expand' : 'Collapse'} workspace view tabs`); foldButton.title = document.documentElement.lang.toLowerCase().startsWith('en') ? (collapsed ? 'Expand workspace view tabs' : 'Collapse workspace view tabs') : (collapsed ? '展开工作区视图切换' : '折叠工作区视图切换'); foldButton.innerHTML = `${collapsed ? icons[selected].replace('`; for (const button of tabs.querySelectorAll('button[data-view]')) { button.setAttribute('aria-pressed', String(button.dataset.view === selected)); } }; const ensure = () => { const nav = document.querySelector('nav[class*="panelList"][data-sidebar-nav-collapsed]'); const root = nav?.closest('.kB3qhq_root'); const regions = root ? [...root.children].filter((node) => node.classList.contains('kB3qhq_regionArea')) : []; const footers = root ? [...root.children].filter((node) => node.classList.contains('kB3qhq_footArea')) : []; const region = regions.length === 1 && footers.length === 1 && [...root.children].indexOf(regions[0]) < [...root.children].indexOf(footers[0]) ? regions[0] : null; const slotEntries = ctx.slots.entries?.(slotName); if (enabled && !quarantined && Array.isArray(slotEntries) && slotEntries.length > 0 && !slotSafe(Boolean(unregister))) quarantine(); if (enabled && bar && observedRoot?.isConnected && (!bar.isConnected || bar.children.length !== 2 || bar.firstElementChild !== tabs || bar.lastElementChild !== foldButton)) quarantine(); if (!enabled || quarantined || !region || !slotSafe(Boolean(unregister))) { bar?.remove(); bar = null; tabs = null; foldButton = null; observer?.disconnect(); observer = null; observedRoot = null; observedRegion = null; observedFooter = null; syncSelection(); return; } if (!bar) { bar = document.createElement('div'); bar.dataset.dshWorkspaceTabsRow = ''; tabs = document.createElement('div'); tabs.dataset.dshWorkspaceTabs = ''; tabs.setAttribute('role', 'group'); tabs.setAttribute('aria-label', '工作区视图 / Workspace views'); for (const [value, zh, en] of labels) { const button = document.createElement('button'); button.type = 'button'; button.dataset.view = value; button.setAttribute('aria-label', `${zh} / ${en}`); button.innerHTML = icons[value]; button.title = document.documentElement.lang.toLowerCase().startsWith('en') ? en : zh; button.addEventListener('click', () => { if (selected === value) return; selected = value; try { window.localStorage.setItem('dsh-nav-fold:workspace-mode', selected); } catch {} syncSelection(); }); tabs.append(button); } foldButton = document.createElement('button'); foldButton.type = 'button'; foldButton.dataset.dshWorkspaceTabsToggle = ''; foldButton.addEventListener('click', () => { collapsed = !collapsed; try { window.localStorage.setItem('dsh-nav-fold:workspace-tabs-collapsed', String(collapsed)); } catch {} syncSelection(); }); bar.append(tabs, foldButton); } if (bar.parentNode && (bar.parentNode !== root || bar.nextElementSibling !== region)) { quarantine(); ensure(); return; } if (!bar.parentNode) root.insertBefore(bar, region); if (observedRoot !== root || observedRegion !== region || observedFooter !== footers[0]) { observer?.disconnect(); observedRoot = root; observedRegion = region; observedFooter = footers[0]; observer = new window.MutationObserver(() => { if (enabled) ensure(); // Revalidate unique region and footer before retaining a singleton override. }); observer.observe(root, { childList: true }); observer.observe(bar, { childList: true }); observer.observe(region, { attributes: true, attributeFilter: ['class'] }); observer.observe(footers[0], { attributes: true, attributeFilter: ['class'] }); } syncSelection(); }; const onAcceptedChange = () => { const snapshot = form?.getSnapshot?.(); const next = accepted(); if (quarantined && snapshot?.status === 'ready') { if (!next) sawSavedOffAfterConflict = true; else if (sawSavedOffAfterConflict && explicitRetryApproved) { quarantined = false; sawSavedOffAfterConflict = false; } } if (!quarantined) explicitRetryApproved = false; enabled = next; ensure(); }; const onSlotChange = () => { if (disposed) return; if (slotMutationDepth) { missedSlotChange = true; return; } // Reconcile once after our transition finishes. if (unregister && !slotSafe(true)) quarantine(); ensure(); }; recheckApprovedRetry = onAcceptedChange; const unsubscribeForm = form?.subscribe?.(onAcceptedChange); const unsubscribeSlot = ctx.slots.subscribe?.(slotName, onSlotChange); window.addEventListener('dsh-nav-fold:sidebar-ready', ensure); ensure(); return () => { disposed = true; if (recheckApprovedRetry === onAcceptedChange) recheckApprovedRetry = () => {}; unsubscribeForm?.(); unsubscribeSlot?.(); window.removeEventListener('dsh-nav-fold:sidebar-ready', ensure); observer?.disconnect(); bar?.remove(); unregister?.(); }; }); } module.exports.inject = ['slots', 'sessions', 'uiWorkspace', 'configForms']; module.exports.apply = apply; module.exports.__test__ = { selectWorkspaceViews }; module.exports.name = 'dsh-nav-fold-client'; return module.exports; } });