dsh-coding-sidebar 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +27 -42
  2. package/lib/client-editor.js +449 -4278
  3. package/lib/client-registry.js +641 -1343
  4. package/lib/client-terminal.js +172 -262
  5. package/lib/client.js +645 -1347
  6. package/lib/index.js +24 -122
  7. package/lib/types/bundle-route.d.ts +1 -1
  8. package/lib/types/client/SideCardSection.d.ts +3 -3
  9. package/lib/types/client/TextEditor.d.ts +20 -10
  10. package/lib/types/client/add-plugin-modal.d.ts +0 -4
  11. package/lib/types/client/api.d.ts +8 -11
  12. package/lib/types/client/builtins/index.d.ts +10 -10
  13. package/lib/types/client/chunk-loader.d.ts +7 -8
  14. package/lib/types/client/locales.d.ts +0 -26
  15. package/lib/types/client/native-avoid.d.ts +2 -0
  16. package/lib/types/client/plugins-shared.d.ts +6 -6
  17. package/lib/types/client/service.d.ts +13 -83
  18. package/lib/types/context-types.d.ts +19 -10
  19. package/lib/types/index.d.ts +1 -1
  20. package/lib/types/prefs-shared.d.ts +1 -24
  21. package/package.json +26 -30
  22. package/src/bundle-route.ts +1 -1
  23. package/src/client/EditorHost.tsx +52 -122
  24. package/src/client/SandboxStatusBar.tsx +12 -9
  25. package/src/client/SideCardSection.module.css +2 -2
  26. package/src/client/SideCardSection.tsx +28 -81
  27. package/src/client/SideChatView.module.css +12 -12
  28. package/src/client/TextEditor.tsx +89 -266
  29. package/src/client/add-plugin-modal.tsx +16 -24
  30. package/src/client/api.ts +1 -25
  31. package/src/client/builtins/index.ts +10 -14
  32. package/src/client/chunk-loader.ts +7 -8
  33. package/src/client/index.tsx +12 -3
  34. package/src/client/locales-ar.ts +0 -6
  35. package/src/client/locales-de.ts +0 -6
  36. package/src/client/locales-fr.ts +0 -6
  37. package/src/client/locales-hi.ts +0 -6
  38. package/src/client/locales-id.ts +0 -6
  39. package/src/client/locales-it.ts +0 -6
  40. package/src/client/locales-ja.ts +0 -6
  41. package/src/client/locales-ko.ts +0 -6
  42. package/src/client/locales-nl.ts +0 -6
  43. package/src/client/locales-pl.ts +0 -6
  44. package/src/client/locales-pt.ts +0 -6
  45. package/src/client/locales-ru.ts +0 -6
  46. package/src/client/locales-sv.ts +0 -6
  47. package/src/client/locales-th.ts +0 -6
  48. package/src/client/locales-tr.ts +0 -6
  49. package/src/client/locales-vi.ts +0 -6
  50. package/src/client/locales-zh-HK.ts +0 -6
  51. package/src/client/locales-zh-MO.ts +0 -6
  52. package/src/client/locales-zh-TW.ts +0 -6
  53. package/src/client/locales.ts +0 -52
  54. package/src/client/native-avoid.ts +155 -0
  55. package/src/client/plugins-shared.ts +6 -6
  56. package/src/client/prefs.ts +0 -7
  57. package/src/client/service.ts +13 -150
  58. package/src/client/sidebar.module.css +1 -425
  59. package/src/config.ts +1 -4
  60. package/src/context-types.ts +12 -7
  61. package/src/index.ts +12 -78
  62. package/src/jobs-routes.ts +3 -2
  63. package/src/prefs-shared.ts +1 -27
  64. package/src/sidechat-routes.ts +13 -8
  65. package/src/subagent-live-route.ts +2 -1
  66. package/lib/client-mermaid.js +0 -200753
  67. package/lib/types/client/MarkdownHtml.d.ts +0 -32
  68. package/lib/types/client/PdfView.d.ts +0 -6
  69. package/lib/types/client/builtins/viewers.d.ts +0 -3
  70. package/lib/types/client/chunks/mermaid.d.ts +0 -10
  71. package/lib/types/client/editor-load.d.ts +0 -66
  72. package/lib/types/client/image-types.d.ts +0 -3
  73. package/lib/types/client/markdown-html.d.ts +0 -96
  74. package/lib/types/client/markdown-images.d.ts +0 -45
  75. package/lib/types/client/md-toc.d.ts +0 -4
  76. package/lib/types/client/mermaid-blocks.d.ts +0 -47
  77. package/lib/types/client/mermaid-sanitize.d.ts +0 -2
  78. package/lib/types/client/mermaid.d.ts +0 -12
  79. package/lib/types/client/pdf-types.d.ts +0 -2
  80. package/lib/types/client/plugins-viewers.d.ts +0 -3
  81. package/lib/types/html-route.d.ts +0 -59
  82. package/src/client/MarkdownHtml.tsx +0 -296
  83. package/src/client/PdfView.tsx +0 -110
  84. package/src/client/builtins/viewers.tsx +0 -125
  85. package/src/client/chunks/mermaid.tsx +0 -10
  86. package/src/client/editor-load.ts +0 -92
  87. package/src/client/image-types.ts +0 -8
  88. package/src/client/markdown-html.ts +0 -331
  89. package/src/client/markdown-images.ts +0 -137
  90. package/src/client/md-toc.tsx +0 -127
  91. package/src/client/mermaid-blocks.ts +0 -110
  92. package/src/client/mermaid-sanitize.ts +0 -93
  93. package/src/client/mermaid.tsx +0 -406
  94. package/src/client/pdf-types.ts +0 -4
  95. package/src/client/plugins-viewers.ts +0 -30
  96. package/src/html-route.ts +0 -106
@@ -1418,17 +1418,6 @@ body[data-dsh-sidebar-dragging] .panel {
1418
1418
  background: var(--dsw-alias-interactive-bg-hover);
1419
1419
  }
1420
1420
 
1421
- /* The HTML preview iframe: fills the editor body. Route-src (never srcdoc)
1422
- so the frame is cross-origin by construction even if the sandbox
1423
- attribute is ever dropped; sandboxed by default (see TextEditor). */
1424
- .editorHtml {
1425
- flex: 1;
1426
- min-height: 0;
1427
- width: 100%;
1428
- border: none;
1429
- background: var(--dsw-alias-bg-base);
1430
- }
1431
-
1432
1421
  /* ── Browser tab ──────────────────────────────────────────────────────── */
1433
1422
 
1434
1423
  .browser {
@@ -1545,10 +1534,6 @@ body[data-dsh-sidebar-dragging] .panel {
1545
1534
  background: transparent;
1546
1535
  }
1547
1536
 
1548
- .editorCmHidden {
1549
- display: none;
1550
- }
1551
-
1552
1537
  /* CodeMirror fills the pane; the theme supplies its own token palette
1553
1538
  (a designed dark theme, same policy as the xterm palette). */
1554
1539
  .editorCm :global(.cm-editor) {
@@ -1565,229 +1550,6 @@ body[data-dsh-sidebar-dragging] .panel {
1565
1550
  outline: none;
1566
1551
  }
1567
1552
 
1568
- /* Preview / edit segmented toggle (markdown and other previewable files). */
1569
- .editorModeToggle {
1570
- flex: none;
1571
- display: inline-flex;
1572
- align-items: center;
1573
- gap: 2px;
1574
- padding: 2px;
1575
- border: 1px solid var(--dsw-alias-border-l1);
1576
- border-radius: 6px;
1577
- background: var(--dsw-alias-bg-layer-1);
1578
- }
1579
-
1580
- .editorModeButton {
1581
- padding: 2px 8px;
1582
- border: none;
1583
- border-radius: 4px;
1584
- background: transparent;
1585
- color: var(--dsw-alias-label-tertiary);
1586
- font: var(--dsw-font-xxxs-11);
1587
- cursor: pointer;
1588
- }
1589
-
1590
- .editorModeButton:hover {
1591
- color: var(--dsw-alias-label-primary);
1592
- }
1593
-
1594
- .editorModeActive {
1595
- background: var(--dsw-alias-bg-base);
1596
- color: var(--dsw-alias-label-primary);
1597
- }
1598
-
1599
- .editorImageWrap {
1600
- flex: 1;
1601
- min-height: 0;
1602
- display: flex;
1603
- align-items: center;
1604
- justify-content: center;
1605
- overflow: auto;
1606
- padding: 12px;
1607
- }
1608
-
1609
- .editorImage {
1610
- max-width: 100%;
1611
- max-height: 100%;
1612
- object-fit: contain;
1613
- }
1614
-
1615
- .editorMd {
1616
- flex: 1;
1617
- min-height: 0;
1618
- overflow-y: auto;
1619
- padding: 12px 16px;
1620
- font: var(--dsw-font-xs-13);
1621
- }
1622
-
1623
- /* ── Mermaid diagram block (markdown preview, mermaid lazy chunk) ────────
1624
- Mirrors the md-code-block chrome the DSH CodeBlock renders, so a diagram
1625
- reads as a peer of the surrounding fences: hairline border, layer-2
1626
- header with the info string + copy action, layer-1 body. The SVG body is
1627
- static (no bindFunctions) and sanitized (no foreignObject/script/@attrs/
1628
- hrefs), so it carries no event surface beyond the copy button; the body
1629
- click opens the zoom modal on a sanitized clone. */
1630
-
1631
- .mermaidWrap {
1632
- margin: 6px 0;
1633
- border: 1px solid var(--dsw-alias-border-l1);
1634
- border-radius: 6px;
1635
- background: var(--dsw-alias-bg-layer-1);
1636
- overflow: hidden;
1637
- }
1638
-
1639
- .mermaidHeader {
1640
- display: flex;
1641
- align-items: center;
1642
- justify-content: space-between;
1643
- gap: 6px;
1644
- padding: 4px 8px;
1645
- border-bottom: 1px solid var(--dsw-alias-border-l1);
1646
- background: var(--dsw-alias-bg-layer-2);
1647
- }
1648
-
1649
- .mermaidInfo {
1650
- font: var(--dsw-font-xxxs-strong-11);
1651
- color: var(--dsw-alias-label-tertiary);
1652
- }
1653
-
1654
- .mermaidCopy {
1655
- display: inline-flex;
1656
- align-items: center;
1657
- gap: 4px;
1658
- padding: 0 6px;
1659
- height: 20px;
1660
- border: none;
1661
- border-radius: 4px;
1662
- background: transparent;
1663
- color: var(--dsw-alias-label-secondary);
1664
- font: var(--dsw-font-xxxs-11);
1665
- cursor: pointer;
1666
- }
1667
-
1668
- .mermaidCopy:hover {
1669
- background: var(--dsw-alias-interactive-bg-hover);
1670
- color: var(--dsw-alias-label-primary);
1671
- }
1672
-
1673
- .mermaidBody {
1674
- display: flex;
1675
- justify-content: center;
1676
- padding: 10px;
1677
- overflow: auto;
1678
- cursor: zoom-in;
1679
- }
1680
-
1681
- .mermaidBody :global(svg) {
1682
- max-width: 100%;
1683
- height: auto;
1684
- }
1685
-
1686
- .mermaidError {
1687
- padding: 6px 10px;
1688
- border-bottom: 1px solid var(--dsw-alias-border-l1);
1689
- color: var(--dsw-alias-state-error-primary);
1690
- font: var(--dsw-font-xxxs-11);
1691
- }
1692
-
1693
- /* The diagram source shown under a render error (helps fixing the fence). */
1694
- .mermaidCode {
1695
- margin: 0;
1696
- padding: 8px 10px;
1697
- overflow: auto;
1698
- font: var(--dsw-font-xxxs-11);
1699
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1700
- }
1701
-
1702
- /* The mermaid markdown container (single MarkdownText pass): a swapped
1703
- code block keeps its React-managed .md-code-block wrapper in the tree
1704
- (reconciliation safety), but the wrapper's own box is suppressed so only
1705
- the diagram chrome renders; the swapped-in host div flows as a block. */
1706
- .mermaidMarkdown :global(.md-code-block[data-mermaid-processed]) {
1707
- display: contents;
1708
- }
1709
-
1710
- /* ── Mermaid zoom modal (click-to-enlarge; portalled to document.body) ──
1711
- Fixed overlay above the DSH overlay stack (1000+), token-driven surface.
1712
- The stage centers the (sanitized, cloned) svg and the modal owns the
1713
- transform; the drag/zoom listeners live in the component, nothing here
1714
- targets global class names. */
1715
-
1716
- .mermaidModal {
1717
- position: fixed;
1718
- inset: 0;
1719
- z-index: 1000;
1720
- display: flex;
1721
- flex-direction: column;
1722
- align-items: center;
1723
- justify-content: center;
1724
- background: var(--dsw-alias-bg-mask-1);
1725
- backdrop-filter: blur(2px);
1726
- }
1727
-
1728
- .mermaidModalToolbar {
1729
- position: absolute;
1730
- top: 16px;
1731
- right: 16px;
1732
- display: flex;
1733
- gap: 8px;
1734
- z-index: 10;
1735
- }
1736
-
1737
- .mermaidModalButton {
1738
- width: 36px;
1739
- height: 36px;
1740
- border-radius: 8px;
1741
- border: 1px solid var(--dsw-alias-border-l1);
1742
- background: var(--dsw-alias-bg-layer-2);
1743
- color: var(--dsw-alias-label-primary);
1744
- font: var(--dsw-font-xs-strong-13);
1745
- cursor: pointer;
1746
- display: inline-flex;
1747
- align-items: center;
1748
- justify-content: center;
1749
- }
1750
-
1751
- .mermaidModalButton:hover {
1752
- background: var(--dsw-alias-interactive-bg-hover);
1753
- }
1754
-
1755
- .mermaidModalStage {
1756
- width: 90vw;
1757
- height: 80vh;
1758
- overflow: hidden;
1759
- position: relative;
1760
- display: flex;
1761
- align-items: center;
1762
- justify-content: center;
1763
- }
1764
-
1765
- .mermaidModalStage :global(svg) {
1766
- max-width: none;
1767
- max-height: none;
1768
- cursor: grab;
1769
- transform-origin: center center;
1770
- user-select: none;
1771
- -webkit-user-drag: none;
1772
- background: var(--dsw-alias-bg-layer-1);
1773
- padding: 16px;
1774
- border-radius: 12px;
1775
- }
1776
-
1777
- .mermaidModalStage :global(svg):active {
1778
- cursor: grabbing;
1779
- }
1780
-
1781
- .mermaidModalHint {
1782
- position: absolute;
1783
- bottom: 16px;
1784
- left: 50%;
1785
- transform: translateX(-50%);
1786
- color: var(--dsw-alias-label-tertiary);
1787
- font: var(--dsw-font-xxxs-11);
1788
- pointer-events: none;
1789
- }
1790
-
1791
1553
  /* The floating "add to conversation" button anchored above a text
1792
1554
  selection in the preview/code viewers. Portalled to document.body and
1793
1555
  position:fixed, so the editor's overflow clips cannot crop it; flat
@@ -1814,64 +1576,6 @@ body[data-dsh-sidebar-dragging] .panel {
1814
1576
  background: var(--dsw-alias-interactive-bg-hover);
1815
1577
  }
1816
1578
 
1817
- .editorPdf {
1818
- flex: 1;
1819
- min-height: 0;
1820
- display: flex;
1821
- flex-direction: column;
1822
- background: var(--dsw-alias-bg-base);
1823
- }
1824
-
1825
- .editorPdfToolbar {
1826
- flex: none;
1827
- display: flex;
1828
- justify-content: flex-end;
1829
- padding: 6px 8px;
1830
- border-bottom: 1px solid var(--dsw-alias-border-l1);
1831
- }
1832
-
1833
- .editorPdfStage {
1834
- position: relative;
1835
- flex: 1;
1836
- min-height: 0;
1837
- display: flex;
1838
- }
1839
-
1840
- .editorPdfFrame {
1841
- flex: 1;
1842
- min-height: 0;
1843
- width: 100%;
1844
- border: none;
1845
- background: var(--dsw-alias-bg-base);
1846
- }
1847
-
1848
- .editorPdfFrameBlocked {
1849
- pointer-events: none;
1850
- }
1851
-
1852
- .editorPdfDragShield {
1853
- position: absolute;
1854
- inset: 0;
1855
- z-index: 4;
1856
- pointer-events: none;
1857
- background: transparent;
1858
- }
1859
-
1860
- .editorPdfDragShieldActive {
1861
- pointer-events: auto;
1862
- }
1863
-
1864
- /* Tab drag must switch iframe hit-testing before the pointer can enter the
1865
- Chromium PDF browsing context. The body flag is set synchronously by the
1866
- drag source, avoiding React render timing entirely. */
1867
- :global(body[data-dsh-tab-dragging]) .editorPdfFrame {
1868
- pointer-events: none !important;
1869
- }
1870
-
1871
- :global(body[data-dsh-tab-dragging]) .editorPdfDragShield {
1872
- pointer-events: auto !important;
1873
- }
1874
-
1875
1579
  /* ── Terminal ──────────────────────────────────────────────────────────── */
1876
1580
 
1877
1581
  .terminalWrap {
@@ -2552,10 +2256,7 @@ body[data-dsh-sidebar-dragging] .panel {
2552
2256
  .gitDiffFile:focus-visible,
2553
2257
  .gitDiffExpand:focus-visible,
2554
2258
  .terminalRetry:focus-visible,
2555
- .editorModeButton:focus-visible,
2556
- .editorDownloadLink:focus-visible,
2557
- .editorPptxButton:focus-visible,
2558
- .editorDocxZoomRange:focus-visible {
2259
+ .editorDownloadLink:focus-visible {
2559
2260
  outline: 2px solid var(--dsw-alias-interactive-bg-hover-accent);
2560
2261
  outline-offset: -1px;
2561
2262
  }
@@ -2655,134 +2356,9 @@ body[data-dsh-sidebar-dragging] .panel {
2655
2356
  color: var(--dsw-alias-state-business-primary);
2656
2357
  }
2657
2358
 
2658
- /* ── Markdown preview: raw-HTML runs + table of contents ─────────────────
2659
- The HTML runs lifted out of the markdown source (MarkdownHtml.tsx) render
2660
- as sanitized block leaves; the TOC is a zero-height sticky bar keeping the
2661
- outline button pinned at the preview's top-right without consuming layout.
2662
- All visuals ride the alias tokens (skin system follows automatically). */
2663
-
2664
- /* A sanitized HTML run leaf (block-level box; content is DOMPurify output). */
2665
- .editorHtmlBlock {
2666
- margin: 8px 0;
2667
- }
2668
-
2669
- .editorHtmlBlock img,
2670
- .editorHtmlBlock video {
2671
- max-width: 100%;
2672
- }
2673
-
2674
- .editorHtmlBlock details {
2675
- margin: 4px 0;
2676
- padding: 4px 0;
2677
- }
2678
-
2679
- .editorHtmlBlock summary {
2680
- cursor: pointer;
2681
- }
2682
-
2683
- /* The zero-height sticky bar: occupies no layout, keeps the button visible
2684
- while the preview scrolls, and lets pointer events pass through the bar
2685
- itself (only the interactive children re-enable them). */
2686
- .tocBar {
2687
- position: sticky;
2688
- top: 0;
2689
- z-index: 3;
2690
- display: flex;
2691
- justify-content: flex-end;
2692
- height: 0;
2693
- pointer-events: none;
2694
- }
2695
-
2696
- .tocButton {
2697
- pointer-events: auto;
2698
- display: inline-flex;
2699
- align-items: center;
2700
- justify-content: center;
2701
- width: 26px;
2702
- height: 26px;
2703
- margin: 4px 2px 0 0;
2704
- padding: 0;
2705
- border: 1px solid var(--dsw-alias-border-l1);
2706
- border-radius: 6px;
2707
- background: var(--dsw-alias-bg-layer-1);
2708
- color: var(--dsw-alias-label-secondary);
2709
- cursor: pointer;
2710
- }
2711
-
2712
- .tocButton:hover {
2713
- background: var(--dsw-alias-interactive-bg-hover);
2714
- color: var(--dsw-alias-label-primary);
2715
- }
2716
-
2717
- /* The outline popover, anchored under the button (the bar is the containing
2718
- block; its height is 0, so the panel drops below the pinned button row). */
2719
- .tocPanel {
2720
- position: absolute;
2721
- top: 32px;
2722
- right: 2px;
2723
- display: flex;
2724
- flex-direction: column;
2725
- width: min(300px, 82%);
2726
- max-height: 60vh;
2727
- overflow-y: auto;
2728
- padding: 4px;
2729
- border: 1px solid var(--dsw-alias-border-l1);
2730
- border-radius: 8px;
2731
- background: var(--dsw-alias-bg-layer-2);
2732
- box-shadow: var(--dsw-shadow-lv2);
2733
- pointer-events: auto;
2734
- }
2735
-
2736
- .tocItem {
2737
- display: flex;
2738
- align-items: baseline;
2739
- gap: 8px;
2740
- min-width: 0;
2741
- padding: 4px 8px;
2742
- border: none;
2743
- border-radius: 6px;
2744
- background: transparent;
2745
- color: var(--dsw-alias-label-secondary);
2746
- font: var(--dsw-font-xxs-12);
2747
- text-align: left;
2748
- cursor: pointer;
2749
- }
2750
-
2751
- .tocItem:hover {
2752
- background: var(--dsw-alias-interactive-bg-hover);
2753
- color: var(--dsw-alias-label-primary);
2754
- }
2755
-
2756
- /* Indent by heading level (h2 = one step, h3 = two steps…). */
2757
- .tocItem[data-level='2'] { padding-left: 18px; }
2758
- .tocItem[data-level='3'] { padding-left: 28px; }
2759
- .tocItem[data-level='4'] { padding-left: 38px; }
2760
- .tocItem[data-level='5'] { padding-left: 48px; }
2761
- .tocItem[data-level='6'] { padding-left: 58px; }
2762
-
2763
- /* The tiny level number keeps deep outlines scannable. */
2764
- .tocItemLevel {
2765
- flex: none;
2766
- font: var(--dsw-font-xxxs-11);
2767
- color: var(--dsw-alias-label-tertiary);
2768
- }
2769
-
2770
- .tocItemText {
2771
- flex: 1 1 auto;
2772
- min-width: 0;
2773
- overflow: hidden;
2774
- text-overflow: ellipsis;
2775
- white-space: nowrap;
2776
- }
2777
-
2778
2359
  /* The jumped-to heading flashes once (the class is added to the host-rendered
2779
2360
  heading element by md-toc.tsx; @keyframes rides the CSS module). */
2780
2361
  @keyframes dsh-toc-flash {
2781
2362
  0%, 60% { background: var(--dsw-alias-interactive-bg-hover); }
2782
2363
  100% { background: transparent; }
2783
2364
  }
2784
-
2785
- .tocFlash {
2786
- animation: dsh-toc-flash 1.2s ease-out;
2787
- border-radius: 4px;
2788
- }
package/src/config.ts CHANGED
@@ -134,18 +134,15 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
134
134
  customCss: z.string(),
135
135
  titleBarCompat: z.boolean().default(false),
136
136
  titleBarStripPx: z.number().step(1).min(TITLE_BAR_STRIP_MIN).max(TITLE_BAR_STRIP_MAX).default(TITLE_BAR_STRIP_DEFAULT),
137
- htmlViewerNoSandbox: z.boolean().default(false),
138
- htmlViewerDefaultUnsafe: z.boolean().default(false),
139
137
  browserNoSandbox: z.boolean().default(false),
140
138
  browserInterceptLinks: z.boolean().default(true),
141
139
  browserInterceptHttp: z.boolean().default(true),
142
140
  browserInterceptHttps: z.boolean().default(false),
143
141
  browserAllowedLoopback: z.string().default(''),
144
- // Per-feature enable switches are OPEN maps (any tab/viewer id, built-in or
142
+ // Per-feature enable switches are OPEN maps (any tab id, built-in or
145
143
  // external): an absent key means enabled, so old documents resolve to {}
146
144
  // (everything on) with no migration. Non-boolean values fail validation.
147
145
  tabsEnabled: z.dict(z.boolean()).default({}),
148
- viewersEnabled: z.dict(z.boolean()).default({}),
149
146
  // Plugin-owned settings blobs (v0.12.0+) are an OPEN nested map: any
150
147
  // descriptor id may carry any JSON-serializable values. This is the
151
148
  // "settings seam" opening — without it the seam would drop third-party
@@ -87,11 +87,11 @@ export interface SidebarSessionStore {
87
87
  get(id: string): {
88
88
  header: SidebarSessionHeader
89
89
  /**
90
- * The live session's append-only event log (immutable snapshot; absent
90
+ * The live session's immutable event snapshot (0.1.5 Session API; absent
91
91
  * on sessions the runtime has not hydrated). Read-only access — the
92
92
  * jobs.output route replays `job_output` tool/result rows from it.
93
93
  */
94
- events?: readonly SidebarSessionEvent[]
94
+ snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SidebarSessionEvent[]
95
95
  } | undefined
96
96
  }
97
97
 
@@ -295,13 +295,18 @@ export interface SidebarSessionTitleService {
295
295
  }
296
296
 
297
297
  /** The host session-persistence face (mirror of the sessionPersistence
298
- * service): detached inspection of a persisted session, used to compose the
299
- * recorded preset when a Side Chat thread cold-resumes. */
300
- export interface SidebarSessionPersistenceService {
301
- inspect(sessionId: string): Promise<{
302
- meta: { cwd?: string; agentPreset?: string }
298
+ * service, 0.1.5 handle-seam form): a short-lived read handle over one
299
+ * persisted session, used to compose the recorded preset when a Side Chat
300
+ * thread cold-resumes. */
301
+ export interface SidebarSessionPersistenceHandle {
302
+ readonly header: { cwd?: string; agentPreset?: string }
303
+ read(fromSeq?: number, toSeqExclusive?: number, options?: { signal?: AbortSignal }): Promise<{
303
304
  events: readonly SidebarSessionEvent[]
304
305
  }>
306
+ close(): Promise<void>
307
+ }
308
+ export interface SidebarSessionPersistenceService {
309
+ open(sessionId: string, access: 'read', options?: { signal?: AbortSignal }): Promise<SidebarSessionPersistenceHandle>
305
310
  }
306
311
 
307
312
  /** RPC result slot mirror (`RpcResult<T>` on the wire). */
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dsh-coding-sidebar host half: the /sidebar JSON API (explorer listing, file
3
- * read/write, git), the /sidebar/file media route (images), the /sidebar/html
4
- * preview route, the /sidebar/bundle lazy-chunk route (client code splits),
3
+ * read/write, git), the /sidebar/file media route (file bytes / downloads),
4
+ * the /sidebar/bundle lazy-chunk route (client code splits),
5
5
  * and the terminal WebSocket upgrade. Every route passes the same
6
6
  * browser-trust fence as the /api gateway — Host-header loopback or the
7
7
  * web runtime's `trustedHosts` (LAN IP literals sampled at boot plus
@@ -18,7 +18,7 @@ import { basename, dirname, extname, isAbsolute, join } from 'node:path'
18
18
  import type { IncomingMessage } from 'node:http'
19
19
  import type { Duplex } from 'node:stream'
20
20
  import { WebSocket, WebSocketServer } from 'ws'
21
- import type { Context, SidebarHttpRequest } from './context-types.ts'
21
+ import type { Context, SidebarHttpRequest, SidebarSessionPersistenceService } from './context-types.ts'
22
22
  import {
23
23
  Config,
24
24
  PrefsSchema,
@@ -33,7 +33,6 @@ import { parentOf, requireAbsolute, listDirectory, rootLabel } from './fs-tree.t
33
33
  import { writeWorkspaceUpload } from './fs-operations.ts'
34
34
  import { ensureWorkspacePath, ensureWorkspaceWritePath } from './path-security.ts'
35
35
  import { searchFiles } from './fs-search.ts'
36
- import { decodeHtmlUrl } from './html-route.ts'
37
36
  import { extractFrameAncestors } from './browser-probe.ts'
38
37
  import { isTrustedApiRequest, isLoopbackHostname } from './trust-fence.ts'
39
38
  import { registerBundleRoute } from './bundle-route.ts'
@@ -62,15 +61,14 @@ export type { SidebarConfig, ResolvedSidebarConfig }
62
61
  // `ctx.betterSidebar`; the Context re-export below is the vendored cordis
63
62
  // Context intersected with the structural service faces.
64
63
  // Also re-export the service descriptor types so consumers can type their
65
- // registerTab / registerFileViewer arguments without reaching into /client.
64
+ // registerTab arguments without reaching into /client. (v1.0.4: the file
65
+ // viewer types were retired with the file-viewer registry — file preview is
66
+ // the host's job now.)
66
67
  export type { Context } from './context-types.ts'
67
68
  export type {
68
69
  BetterSidebarService,
69
70
  TabDescriptor,
70
71
  TabComponentProps,
71
- FileViewerDescriptor,
72
- FileViewerProps,
73
- FileFetchStrategy,
74
72
  } from './client/service.ts'
75
73
 
76
74
  /** Plugin identity for cordis.yml rows. */
@@ -90,9 +88,6 @@ const MEDIA_TYPES: Record<string, string> = {
90
88
  '.bmp': 'image/bmp',
91
89
  '.ico': 'image/x-icon',
92
90
  '.avif': 'image/avif',
93
- '.pdf': 'application/pdf',
94
- '.html': 'text/html',
95
- '.htm': 'text/html',
96
91
  }
97
92
 
98
93
  /** Content type served by /sidebar/file (binary-safe fallback for unknowns). */
@@ -126,10 +121,11 @@ async function sessionCwdOf(ctx: Context, sessionId: string, clientCwd?: string)
126
121
  throw new SidebarError('bad-request', `invalid working directory "${clientCwd}"`)
127
122
  }
128
123
  }
129
- const persistence = ctx.get('sessionPersistence')
124
+ const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined
130
125
  if (persistence !== undefined) {
131
- const inspected = await persistence.inspect(sessionId)
132
- const metaCwd = inspected.meta.cwd
126
+ const handle = await persistence.open(sessionId, 'read')
127
+ const metaCwd = handle.header.cwd
128
+ await handle.close()
133
129
  if (metaCwd !== undefined && metaCwd !== '') {
134
130
  try {
135
131
  return requireAbsolute(metaCwd)
@@ -848,11 +844,11 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
848
844
 
849
845
  // ── Lazy chunk route (client bundle splits) ─────────────────────────────
850
846
  // Serves the client half's split bundles (lib/client-<name>.js) so the
851
- // heavy preview/terminal libraries load on first use, not at page start
847
+ // heavy editor/terminal libraries load on first use, not at page start
852
848
  // (see bundle-route.ts / src/client/chunk-loader.ts).
853
849
  ctx.effect(() => registerBundleRoute(ctx, fence), 'dsh-coding-sidebar: /sidebar/bundle chunk route')
854
850
 
855
- // ── Media route (images for the editor) ─────────────────────────────────
851
+ // ── Media route (file bytes; the binary pane's download link) ───────────
856
852
  ctx.effect(() => ctx.webServer.register({
857
853
  kind: 'prefix',
858
854
  path: '/sidebar/file',
@@ -894,68 +890,6 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
894
890
  },
895
891
  }), 'dsh-coding-sidebar: /sidebar/file media route')
896
892
 
897
- // ── HTML preview route (sandboxed HTML + its relative assets) ───────────
898
- // Serves files under the session cwd for the built-in HTML previewer. The
899
- // URL is path-encoded (see html-route.ts) so the previewed page's relative
900
- // assets (./style.css, img/x.png) resolve back into this route with the
901
- // session scope intact — a query-encoded URL would drop the scope when the
902
- // browser resolves relatives. Every response carries the CSP `sandbox`
903
- // directive: inside the editor's iframe the sandbox ATTRIBUTE is the
904
- // boundary, this header is defense-in-depth so even a top-level load of
905
- // the URL (e.g. a popup opened by a previewed page) stays in an opaque
906
- // origin with no same-origin access to the GUI.
907
- ctx.effect(() => ctx.webServer.register({
908
- kind: 'prefix',
909
- path: '/sidebar/html',
910
- handler: async (req, res) => {
911
- if (!fence(req)) {
912
- res.writeHead(403)
913
- res.end('forbidden')
914
- return
915
- }
916
- if (req.method !== 'GET') {
917
- res.writeHead(405)
918
- res.end()
919
- return
920
- }
921
- try {
922
- const url = new URL(req.url ?? '/', 'http://dsh.internal')
923
- const decoded = decodeHtmlUrl(url.pathname)
924
- if (!decoded.ok) {
925
- writeError(res, new SidebarError('bad-request', decoded.message, decoded.status))
926
- return
927
- }
928
- const { sessionId, path } = decoded.ref
929
- // The session's authoritative cwd (client cwd cannot ride in the URL
930
- // — the path encoding has no query; a detached first request falls
931
- // back to the process cwd and is normally refused by the workspace
932
- // real-path guard, with the same semantics as the media route's
933
- // fallback.
934
- const cwd = await sessionCwdOf(ctx, sessionId)
935
- const absolute = await ensureWorkspacePath(cwd, path)
936
- const info = await stat(absolute)
937
- if (!info.isFile() || info.size > resolved.mediaLimit) {
938
- throw new SidebarError('fs-error', 'not a file or too large', 400)
939
- }
940
- const type = mediaTypeForPath(absolute)
941
- const body = await readFile(absolute)
942
- res.writeHead(200, {
943
- 'content-type': type === 'text/html' ? 'text/html; charset=utf-8' : type,
944
- 'cache-control': 'no-cache',
945
- 'x-content-type-options': 'nosniff',
946
- 'referrer-policy': 'no-referrer',
947
- // The sandbox directive (no allow-same-origin → opaque origin) is
948
- // the previewer's security boundary even for top-level loads;
949
- // object-src 'none' blocks plugin embeds.
950
- 'content-security-policy': "sandbox allow-scripts allow-popups allow-downloads allow-modals; object-src 'none'",
951
- })
952
- res.end(body)
953
- } catch (error) {
954
- writeError(res, error)
955
- }
956
- },
957
- }), 'dsh-coding-sidebar: /sidebar/html preview route')
958
-
959
893
  // ── Terminal WebSocket ──────────────────────────────────────────────────
960
894
  // One upgrade endpoint serves both UI-tab terminals (?tab=...) and
961
895
  // agent-owned terminals (?uuid=...). The two paths attach to different
@@ -135,7 +135,7 @@ const MIRROR_MAX_ENTRIES = 200
135
135
  * The live job_output mirror: subscribes to the session append feed and
136
136
  * caches the job_output traces the session store's own log can lag behind
137
137
  * (after a host restart the store session stays frozen at its rehydration
138
- * boundary, so `session.events` misses everything appended since — the very
138
+ * boundary, so a stale snapshot misses everything appended since — the very
139
139
  * reads the pane exists to show). Zero DSH writes: the api-proxy pushes the
140
140
  * same feed to browsers.
141
141
  */
@@ -211,7 +211,8 @@ export function buildJobsApi(ctx: Context, outputLimit: number): SidebarJobsRout
211
211
  // Merge the store's event log (durable seed + whatever it received)
212
212
  // with the live mirror, deduped by seq — a trace never double-counts.
213
213
  const bySeq = new Map<number, JobOutputTrace>()
214
- for (const event of ctx.sessions.get(sessionId)?.events ?? []) {
214
+ const store = ctx.sessions.get(sessionId)
215
+ for (const event of (store?.snapshotEvents !== undefined ? store.snapshotEvents() : []) ?? []) {
215
216
  const trace = traceOf(event)
216
217
  if (trace !== undefined) bySeq.set(trace.seq, trace)
217
218
  }