// dsh-conversation-flat — browser half. // Registered with the web module loader via the package's ./client export; // the cordis loader adopts `exports` (apply/inject) as the plugin object. // // Delivers: // 1. Document-flow conversation layout (CSS): full-width column, user // messages as full-width neutral bars, sender label above assistant // replies, markdown tables stretching with the column. // 2. Conversation minimap (JS): a vertical ruler at the left edge of the // message area — one tick per message, hover for a text preview, // click to jump, thumb tracking the viewport. // // All colors come from the theme token layer so both features adapt to any // theme (kimino, default, ...). Every side effect is registered through // ctx.effect so disable/remove fully reverts the page. // // MAINTENANCE NOTE: selectors match CSS-module content hashes of // @deepseek-ai/dsh-client-ui-conversation at dsh 0.1.1-rc.2 // (gdEzaW = message rows/bubble, Sxvs8a = assistant block, Md3f7G = // conversation scroll/flow, qDHVXG = sidebar list). A dsh upgrade that // rebuilds those packages changes the hashes — re-derive them from the new // client bundles and update the stems below. window.__ModuleLoader__.load({ id: 'dsh-conversation-flat', factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); // Load after the theme layer so token values this feature reads are final. const inject = ['theme']; const apply = (ctx) => { /* ---------------- stylesheet ---------------- */ const styleEl = document.createElement('style'); styleEl.id = 'dsh-conversation-flat'; styleEl.textContent = ` /* ---- conversation column: centered 748px card -> fill the content area ---- */ [class*="Md3f7G_scroll"] [class*="Md3f7G_column"] { max-width: none !important; margin: 0 !important; } /* ---- user messages: right-aligned bubble -> full-width neutral bar ---- */ [class*="Md3f7G_scroll"] [class*="gdEzaW_userRow"] { align-items: stretch !important; } [class*="Md3f7G_scroll"] [class*="gdEzaW_userStack"] { max-width: 100% !important; width: 100% !important; align-items: stretch !important; } [class*="Md3f7G_scroll"] [class*="gdEzaW_bubble"] { border-radius: 10px !important; padding: 12px 16px !important; /* 45% mix keeps it a quiet bar in dark themes and stays subtle in light ones */ background: color-mix(in srgb, var(--dsw-specific-bubble, #8a8a8a) 45%, transparent) !important; } /* ---- assistant messages: plain sender label above the markdown body ---- */ [class*="Md3f7G_scroll"] [class*="Sxvs8a_root"]::before { content: "DeepSeek"; display: block; font-size: 12px; line-height: 18px; opacity: 0.55; margin-bottom: 4px; } /* ---- markdown tables: stretch with the column, never shrink-wrap/center ---- */ [class*="Md3f7G_scroll"] [class*="_tableScroll_"] { width: 100% !important; margin: 0 !important; /* dsh reserves a scrollbar strip (padding-bottom 8px, overflow hidden) and swaps to auto/0 on :hover — with full-width tables that toggle is a no-op that still jolts layout. Pin the hover state so nothing changes on hover. */ overflow-x: auto !important; padding-bottom: 0 !important; } [class*="Md3f7G_scroll"] [class*="_tableScroll_"] table { width: 100% !important; /* the renderer JS sets an inline max-width:max-content to enable its scrollable-table mode — a stylesheet !important beats a plain inline style */ max-width: 100% !important; margin: 0 !important; } /* ---- sidebar: drop the session-list scroll fade ---- */ /* The fade is a transparent->glass gradient block pinned to the bottom of the list area. With few sessions it floats in the empty glass column and its square edge shows against the wallpaper; the glass column itself already extends to the bottom, so the hint adds nothing. */ [class*="qDHVXG_fade"] { display: none !important; } /* ---- conversation minimap ---- */ .dcf-minimap { position: fixed; width: 14px; z-index: 40; border-radius: 7px; background: color-mix(in srgb, var(--dsw-alias-label-tertiary, #888) 8%, transparent); } .dcf-minimap-tick { position: absolute; left: 1px; right: 1px; height: 9px; cursor: pointer; background: linear-gradient(color-mix(in srgb, var(--dsw-alias-label-secondary, #aaa) 60%, transparent), color-mix(in srgb, var(--dsw-alias-label-secondary, #aaa) 60%, transparent)); background-size: 100% 2px; background-repeat: no-repeat; background-position: center; border-radius: 2px; } .dcf-minimap-tick.dcf-k-user { left: 0; right: 4px; background: linear-gradient(color-mix(in srgb, var(--dsw-alias-state-business-primary, #7ab8ff) 80%, transparent), color-mix(in srgb, var(--dsw-alias-state-business-primary, #7ab8ff) 80%, transparent)); background-size: 100% 2px; background-repeat: no-repeat; background-position: center; } .dcf-minimap-tick:hover { background: linear-gradient(var(--dsw-alias-label-primary, #eee), var(--dsw-alias-label-primary, #eee)); background-size: 100% 2px; background-repeat: no-repeat; background-position: center; } .dcf-minimap-thumb { position: absolute; left: 0; right: 0; border-radius: 2px; background: color-mix(in srgb, var(--dsw-alias-label-tertiary, #888) 35%, transparent); pointer-events: none; } .dcf-minimap-tip { display: none; position: fixed; z-index: 60; max-width: 340px; padding: 8px 10px; border-radius: 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary, #ccc); background: var(--dsw-specific-menu, rgba(20, 24, 38, 0.95)); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-tertiary, #888) 30%, transparent); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); pointer-events: none; } `; document.head.append(styleEl); ctx.effect(() => () => styleEl.remove()); /* ---------------- conversation minimap ---------------- */ let scroller = null; let strip = null; let thumb = null; let tip = null; let raf = 0; let tipTimer = 0; let tickTimer = 0; let lastTickCount = -1; const getScroller = () => document.querySelector('[class*="Md3f7G_scroll"]'); // Real message content: the user bubble or the assistant markdown body. // Flow items without either are chrome (timestamp/stats/action rows) — // they get no minimap tick and would only pollute previews. const contentEl = (item) => item.querySelector('[class*="gdEzaW_bubble"]') || item.querySelector('[class*="Sxvs8a_body"]') || null; const itemKind = (item) => { if (item.querySelector('[class*="gdEzaW_userRow"]')) return 'user'; return 'ai'; }; const previewText = (item) => { const el = contentEl(item) || item; const clone = el.cloneNode(true); // thinking disclosures and code/table blocks drown the preview — prose only clone.querySelectorAll('[class*="QWLzlG_row"], table, pre').forEach((n) => n.remove()); const txt = clone.textContent || ''; return txt.replace(/\s+/g, ' ').trim().slice(0, 160); }; const positionStrip = () => { if (!scroller || !strip || !strip.isConnected) return; const r = scroller.getBoundingClientRect(); strip.style.left = Math.round(r.left + 3) + 'px'; strip.style.top = Math.round(r.top + 10) + 'px'; strip.style.height = Math.round(r.height - 20) + 'px'; }; const updateThumb = () => { if (!scroller || !thumb || !strip || !strip.isConnected) return; const H = scroller.scrollHeight; const C = scroller.clientHeight; if (H <= C + 4) { thumb.style.opacity = '0'; return; } const stripH = strip.clientHeight; thumb.style.opacity = '1'; thumb.style.top = (scroller.scrollTop / H) * stripH + 'px'; thumb.style.height = Math.max((C / H) * stripH, 24) + 'px'; }; const refreshTicks = () => { if (!scroller || !strip || !strip.isConnected) return; const sc = scroller; strip.querySelectorAll('.dcf-minimap-tick').forEach((t) => t.remove()); const H = sc.scrollHeight; if (!H) return; const stripH = strip.clientHeight; for (const item of sc.querySelectorAll('[class*="Md3f7G_flowItem"]')) { if (!contentEl(item)) continue; const docTop = item.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop; const tick = document.createElement('div'); tick.className = 'dcf-minimap-tick dcf-k-' + itemKind(item); tick.style.top = Math.min((docTop / H) * stripH, Math.max(stripH - 9, 0)) + 'px'; tick.dataset.dcfPreview = previewText(item); tick.dataset.dcfDocTop = String(docTop); tick.addEventListener('mouseenter', onTickEnter); tick.addEventListener('mouseleave', onTickLeave); tick.addEventListener('click', onTickClick); strip.appendChild(tick); } lastTickCount = sc.querySelectorAll('[class*="Md3f7G_flowItem"]').length; updateThumb(); }; const scheduleTicks = () => { clearTimeout(tickTimer); tickTimer = setTimeout(() => { if (!scroller) return; const n = scroller.querySelectorAll('[class*="Md3f7G_flowItem"]').length; if (n !== lastTickCount) refreshTicks(); }, 250); }; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; positionStrip(); updateThumb(); }); }; const onTickEnter = (e) => { clearTimeout(tipTimer); const t = e.currentTarget; tipTimer = setTimeout(() => { tip.textContent = t.dataset.dcfPreview || '(空)'; tip.style.display = 'block'; const r = t.getBoundingClientRect(); const top = Math.min(Math.max(r.top - 6, 8), window.innerHeight - tip.offsetHeight - 8); tip.style.top = top + 'px'; // appear to the right of the ruler, never over the sidebar const stripR = strip.getBoundingClientRect(); tip.style.left = Math.round(stripR.right + 8) + 'px'; }, 120); }; const onTickLeave = () => { clearTimeout(tipTimer); if (tip) tip.style.display = 'none'; }; const onTickClick = (e) => { const sc = scroller; if (!sc) return; const docTop = parseFloat(e.currentTarget.dataset.dcfDocTop || '0'); sc.scrollTo({ top: Math.max(docTop - 24, 0), behavior: 'smooth' }); }; const buildStrip = () => { if (strip && strip.isConnected) return; strip = document.createElement('div'); strip.className = 'dcf-minimap'; thumb = document.createElement('div'); thumb.className = 'dcf-minimap-thumb'; strip.appendChild(thumb); document.body.appendChild(strip); if (!tip || !tip.isConnected) { tip = document.createElement('div'); tip.className = 'dcf-minimap-tip'; document.body.appendChild(tip); } }; const bind = () => { const sc = getScroller(); if (!sc) return; if (sc !== scroller) { if (scroller) scroller.removeEventListener('scroll', onScroll); scroller = sc; scroller.addEventListener('scroll', onScroll, { passive: true }); lastTickCount = -1; buildStrip(); positionStrip(); refreshTicks(); } else if (strip && !strip.isConnected) { buildStrip(); positionStrip(); } }; // Re-bind on session switches, rebuild ticks as messages stream in. const mo = new MutationObserver(() => { bind(); scheduleTicks(); if (strip && strip.isConnected) positionStrip(); }); mo.observe(document.body, { childList: true, subtree: true }); window.addEventListener('resize', onScroll); bind(); positionStrip(); refreshTicks(); ctx.effect(() => () => { mo.disconnect(); clearTimeout(tipTimer); clearTimeout(tickTimer); if (raf) cancelAnimationFrame(raf); if (scroller) scroller.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); strip?.remove(); tip?.remove(); }); }; exports.apply = apply; exports.inject = inject; return module.exports; }, });