dsh-better-sidebar 0.10.0

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/README_EN.md +139 -0
  4. package/lib/client-docx.js +7649 -0
  5. package/lib/client-editor.js +32677 -0
  6. package/lib/client-pptx.js +63438 -0
  7. package/lib/client-registry.js +8021 -0
  8. package/lib/client-terminal.js +8060 -0
  9. package/lib/client-xlsx.js +359755 -0
  10. package/lib/client.js +8021 -0
  11. package/lib/index.js +2604 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-pty.d.ts +220 -0
  14. package/lib/types/browser-probe.d.ts +15 -0
  15. package/lib/types/bundle-route.d.ts +13 -0
  16. package/lib/types/client/BrowserView.d.ts +22 -0
  17. package/lib/types/client/DiffTab.d.ts +6 -0
  18. package/lib/types/client/DiffView.d.ts +51 -0
  19. package/lib/types/client/EditorHost.d.ts +10 -0
  20. package/lib/types/client/ExplorerView.d.ts +9 -0
  21. package/lib/types/client/GitView.d.ts +8 -0
  22. package/lib/types/client/OrphanedTab.d.ts +11 -0
  23. package/lib/types/client/PdfView.d.ts +6 -0
  24. package/lib/types/client/PptxView.d.ts +6 -0
  25. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  26. package/lib/types/client/SideCardSection.d.ts +27 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +38 -0
  30. package/lib/types/client/TerminalView.d.ts +8 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/api.d.ts +170 -0
  33. package/lib/types/client/binary-download.d.ts +12 -0
  34. package/lib/types/client/breakpoints.d.ts +12 -0
  35. package/lib/types/client/browser.d.ts +50 -0
  36. package/lib/types/client/builtins/index.d.ts +17 -0
  37. package/lib/types/client/builtins/tabs.d.ts +6 -0
  38. package/lib/types/client/builtins/viewers.d.ts +3 -0
  39. package/lib/types/client/chunk-loader.d.ts +74 -0
  40. package/lib/types/client/chunks/docx.d.ts +8 -0
  41. package/lib/types/client/chunks/editor.d.ts +9 -0
  42. package/lib/types/client/chunks/pptx.d.ts +8 -0
  43. package/lib/types/client/chunks/terminal.d.ts +9 -0
  44. package/lib/types/client/chunks/xlsx.d.ts +9 -0
  45. package/lib/types/client/cm-themes.d.ts +24 -0
  46. package/lib/types/client/conversation-draft.d.ts +14 -0
  47. package/lib/types/client/docx-view.d.ts +8 -0
  48. package/lib/types/client/editor-load.d.ts +66 -0
  49. package/lib/types/client/icons.d.ts +47 -0
  50. package/lib/types/client/image-types.d.ts +3 -0
  51. package/lib/types/client/ime-guard.d.ts +36 -0
  52. package/lib/types/client/index.d.ts +10 -0
  53. package/lib/types/client/intercept.d.ts +20 -0
  54. package/lib/types/client/lang.d.ts +12 -0
  55. package/lib/types/client/lazy-chunk.d.ts +25 -0
  56. package/lib/types/client/link-intercept.d.ts +35 -0
  57. package/lib/types/client/locales.d.ts +237 -0
  58. package/lib/types/client/office-shared.d.ts +33 -0
  59. package/lib/types/client/office-types.d.ts +36 -0
  60. package/lib/types/client/office-view.d.ts +12 -0
  61. package/lib/types/client/openpath-intercept.d.ts +41 -0
  62. package/lib/types/client/paths.d.ts +19 -0
  63. package/lib/types/client/pdf-types.d.ts +2 -0
  64. package/lib/types/client/prefs.d.ts +30 -0
  65. package/lib/types/client/produced-files.d.ts +27 -0
  66. package/lib/types/client/selection-payload.d.ts +27 -0
  67. package/lib/types/client/service.d.ts +214 -0
  68. package/lib/types/client/split-pane.d.ts +29 -0
  69. package/lib/types/client/state.d.ts +322 -0
  70. package/lib/types/client/subagent-activity.d.ts +35 -0
  71. package/lib/types/client/subagent-detect.d.ts +47 -0
  72. package/lib/types/client/subagent-jobs.d.ts +63 -0
  73. package/lib/types/client/theme.d.ts +28 -0
  74. package/lib/types/client/xlsx-to-univer.d.ts +33 -0
  75. package/lib/types/client/xlsx-view.d.ts +10 -0
  76. package/lib/types/config.d.ts +41 -0
  77. package/lib/types/context-types.d.ts +431 -0
  78. package/lib/types/fs-tree.d.ts +41 -0
  79. package/lib/types/git.d.ts +75 -0
  80. package/lib/types/html-route.d.ts +48 -0
  81. package/lib/types/index.d.ts +39 -0
  82. package/lib/types/invariant.d.ts +15 -0
  83. package/lib/types/jobs-routes.d.ts +45 -0
  84. package/lib/types/prefs-shared.d.ts +106 -0
  85. package/lib/types/pty-manager.d.ts +76 -0
  86. package/lib/types/tools.d.ts +28 -0
  87. package/lib/types/trust-fence.d.ts +24 -0
  88. package/lib/types/wire.d.ts +38 -0
  89. package/package.json +168 -0
  90. package/src/agent-pty.ts +519 -0
  91. package/src/browser-probe.ts +26 -0
  92. package/src/bundle-route.ts +130 -0
  93. package/src/client/BrowserView.tsx +253 -0
  94. package/src/client/DiffTab.tsx +110 -0
  95. package/src/client/DiffView.tsx +266 -0
  96. package/src/client/EditorHost.tsx +101 -0
  97. package/src/client/ExplorerView.tsx +283 -0
  98. package/src/client/GitView.tsx +540 -0
  99. package/src/client/OrphanedTab.tsx +27 -0
  100. package/src/client/PdfView.tsx +110 -0
  101. package/src/client/PptxView.tsx +131 -0
  102. package/src/client/SandboxStatusBar.tsx +60 -0
  103. package/src/client/SideCardSection.module.css +271 -0
  104. package/src/client/SideCardSection.tsx +426 -0
  105. package/src/client/Sidebar.tsx +861 -0
  106. package/src/client/SubagentView.module.css +547 -0
  107. package/src/client/SubagentView.tsx +866 -0
  108. package/src/client/TabBar.tsx +188 -0
  109. package/src/client/TerminalView.tsx +233 -0
  110. package/src/client/TextEditor.tsx +382 -0
  111. package/src/client/api.ts +215 -0
  112. package/src/client/binary-download.tsx +23 -0
  113. package/src/client/breakpoints.ts +51 -0
  114. package/src/client/browser.ts +119 -0
  115. package/src/client/builtins/index.ts +33 -0
  116. package/src/client/builtins/tabs.tsx +211 -0
  117. package/src/client/builtins/viewers.tsx +151 -0
  118. package/src/client/chunk-loader.ts +196 -0
  119. package/src/client/chunks/docx.tsx +8 -0
  120. package/src/client/chunks/editor.tsx +9 -0
  121. package/src/client/chunks/pptx.tsx +8 -0
  122. package/src/client/chunks/terminal.tsx +9 -0
  123. package/src/client/chunks/xlsx.tsx +9 -0
  124. package/src/client/cm-themes.ts +129 -0
  125. package/src/client/conversation-draft.ts +29 -0
  126. package/src/client/css-modules.d.ts +5 -0
  127. package/src/client/docx-view.tsx +113 -0
  128. package/src/client/editor-load.ts +92 -0
  129. package/src/client/icons.tsx +137 -0
  130. package/src/client/image-types.ts +8 -0
  131. package/src/client/ime-guard.ts +47 -0
  132. package/src/client/index.tsx +238 -0
  133. package/src/client/intercept.tsx +94 -0
  134. package/src/client/lang.ts +95 -0
  135. package/src/client/layout.css +54 -0
  136. package/src/client/lazy-chunk.tsx +89 -0
  137. package/src/client/link-intercept.ts +66 -0
  138. package/src/client/locales.ts +476 -0
  139. package/src/client/office-shared.tsx +40 -0
  140. package/src/client/office-types.ts +48 -0
  141. package/src/client/office-view.tsx +12 -0
  142. package/src/client/openpath-intercept.ts +62 -0
  143. package/src/client/paths.ts +28 -0
  144. package/src/client/pdf-types.ts +4 -0
  145. package/src/client/prefs.ts +100 -0
  146. package/src/client/produced-files.ts +87 -0
  147. package/src/client/selection-payload.ts +86 -0
  148. package/src/client/service.ts +434 -0
  149. package/src/client/sidebar.module.css +1917 -0
  150. package/src/client/split-pane.tsx +292 -0
  151. package/src/client/state.ts +1048 -0
  152. package/src/client/subagent-activity.ts +67 -0
  153. package/src/client/subagent-detect.ts +125 -0
  154. package/src/client/subagent-jobs.ts +161 -0
  155. package/src/client/theme.ts +44 -0
  156. package/src/client/xlsx-to-univer.ts +260 -0
  157. package/src/client/xlsx-view.tsx +116 -0
  158. package/src/config.ts +95 -0
  159. package/src/context-types.ts +445 -0
  160. package/src/fs-tree.ts +115 -0
  161. package/src/git.ts +236 -0
  162. package/src/html-route.ts +70 -0
  163. package/src/index.ts +897 -0
  164. package/src/invariant.ts +32 -0
  165. package/src/jobs-routes.ts +257 -0
  166. package/src/prefs-shared.ts +127 -0
  167. package/src/pty-manager.ts +199 -0
  168. package/src/tools.ts +477 -0
  169. package/src/trust-fence.ts +77 -0
  170. package/src/wire.ts +94 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Icons the sidebar needs beyond the primitives set: a terminal glyph (the
3
+ * icon library has none), a diff glyph, and the two panel-toggle glyphs for
4
+ * the top-right cluster. Per-tab icons live on the tab descriptors
5
+ * (`descriptor.icon`), not in a type-keyed switch — the icon mapping was
6
+ * registry-ized with the tab types.
7
+ */
8
+ import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives'
9
+
10
+ /**
11
+ * Right-panel toggle glyph (the "侧拉" button): a frame with a filled strip
12
+ * along its RIGHT edge, in the app's outline style (1.5px stroke,
13
+ * currentColor).
14
+ */
15
+ export const IconPanelRightOutline16 = ({ size = 16, className }: IconProps) => (
16
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
17
+ <rect x="1.5" y="2" width="13" height="12" rx="2.5" stroke="currentColor" strokeWidth="1.5" />
18
+ <rect x="10.5" y="3.25" width="2.75" height="9.5" rx="1" fill="currentColor" stroke="none" />
19
+ </svg>
20
+ )
21
+
22
+ /**
23
+ * Bottom-panel toggle glyph (the "底栏" button): a frame with a filled strip
24
+ * along its BOTTOM edge, in the app's outline style.
25
+ */
26
+ export const IconPanelBottomOutline16 = ({ size = 16, className }: IconProps) => (
27
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
28
+ <rect x="1.5" y="2" width="13" height="12" rx="2.5" stroke="currentColor" strokeWidth="1.5" />
29
+ <rect x="3.25" y="10" width="9.5" height="2.75" rx="1" fill="currentColor" stroke="none" />
30
+ </svg>
31
+ )
32
+
33
+ /**
34
+ * Terminal glyph in the app's outline style (1.5px stroke, currentColor):
35
+ * a rounded frame with a prompt chevron and underscore cursor.
36
+ */
37
+ export const IconTerminalOutline16 = ({ size = 16, className }: IconProps) => (
38
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
39
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" strokeWidth="1.5" />
40
+ <path d="M4.5 6.25 6.75 8 4.5 9.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
41
+ <path d="M8.5 10.4h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
42
+ </svg>
43
+ )
44
+
45
+ /** Diff glyph in the app's outline style: a file frame with a plus and a minus row. */
46
+ export const IconDiffOutline16 = ({ size = 16, className }: IconProps) => (
47
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
48
+ <rect x="1.5" y="1.5" width="13" height="13" rx="2.5" stroke="currentColor" strokeWidth="1.5" />
49
+ <path d="M4 5h3M5.5 3.5v3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
50
+ <path d="M9.5 12.5h2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
51
+ </svg>
52
+ )
53
+
54
+ /**
55
+ * Stop glyph for the background-job kill button: a filled square in the
56
+ * app's outline scale (16), the universal "halt this work" mark.
57
+ */
58
+ export const IconStopOutline16 = ({ size = 16, className }: IconProps) => (
59
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
60
+ <rect x="4" y="4" width="8" height="8" rx="1.5" fill="currentColor" stroke="none" />
61
+ </svg>
62
+ )
63
+
64
+ // ── File-viewer inventory glyphs (Side card settings page) ────────────────
65
+
66
+ /** Image viewer glyph: a picture frame with a sun and a mountain. */
67
+ export const IconImageOutline16 = ({ size = 16, className }: IconProps) => (
68
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
69
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" strokeWidth="1.5" />
70
+ <circle cx="5.5" cy="6" r="1.2" stroke="currentColor" strokeWidth="1.5" />
71
+ <path d="m3.5 12 3-3 2.25 2.25L11.5 8.5 13 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
72
+ </svg>
73
+ )
74
+
75
+ /** PDF viewer glyph: a document frame with the "PDF" label. */
76
+ export const IconPdfOutline16 = ({ size = 16, className }: IconProps) => (
77
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
78
+ <path d="M3.5 1.5h6.5L13.5 5v9.5h-10z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
79
+ <path d="M9.5 1.5V5h4" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
80
+ <path d="M5 13.5v-3h1.4c.75 0 1.1.32 1.1.85 0 .54-.35.85-1.1.85H5.3" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
81
+ <path d="M8.3 13.5v-3h1.05c.8 0 1.35.5 1.35 1.5s-.55 1.5-1.35 1.5z" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
82
+ <path d="M11.6 13.5v-3h1.3" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
83
+ </svg>
84
+ )
85
+
86
+ /** Word viewer glyph: a document frame with a "W". */
87
+ export const IconDocxOutline16 = ({ size = 16, className }: IconProps) => (
88
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
89
+ <path d="M3.5 1.5h6.5L13.5 5v9.5h-10z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
90
+ <path d="M9.5 1.5V5h4" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
91
+ <path d="M6.2 13.4 7.4 10l1.2 3.4M7.4 10.6l-.35-1.1c-.2-.62.2-1.25.85-1.25h.2c.65 0 1.05.63.85 1.25l-.35 1.1" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
92
+ <path d="M8.75 10.6 9.2 9.4" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
93
+ </svg>
94
+ )
95
+
96
+ /** Excel viewer glyph: a spreadsheet grid. */
97
+ export const IconXlsxOutline16 = ({ size = 16, className }: IconProps) => (
98
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
99
+ <rect x="1.5" y="2" width="13" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
100
+ <path d="M1.5 6h13M1.5 9.5h13M6 6v8M10.5 6v8" stroke="currentColor" strokeWidth="1.25" />
101
+ <path d="m3.8 13.2 2-3M5.8 13.2l-2-3" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
102
+ </svg>
103
+ )
104
+
105
+ /** PowerPoint viewer glyph: a chart with rising bars. */
106
+ export const IconPptxOutline16 = ({ size = 16, className }: IconProps) => (
107
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
108
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" strokeWidth="1.5" />
109
+ <path d="M4 12.5v-3M7 12.5V7M10 12.5V4.5M13 12.5v-1.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
110
+ </svg>
111
+ )
112
+
113
+ /** Markdown viewer glyph: the classic "M with a down arrow" badge. */
114
+ export const IconMarkdownOutline16 = ({ size = 16, className }: IconProps) => (
115
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
116
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" strokeWidth="1.5" />
117
+ <path d="M4 10.5V5.5l2 2.5 2-2.5v5M9.5 10.5v-5l2 2.5 2-2.5v5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
118
+ </svg>
119
+ )
120
+
121
+ /** HTML viewer glyph: a document frame with a "‹/›" tag pair. */
122
+ export const IconHtmlOutline16 = ({ size = 16, className }: IconProps) => (
123
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
124
+ <path d="M3.5 1.5h6.5L13.5 5v9.5h-10z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
125
+ <path d="M9.5 1.5V5h4" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
126
+ <path d="M5.6 13.2 4.2 10l1.4-3.2M7.4 6.8 8.8 10l-1.4 3.2" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
127
+ </svg>
128
+ )
129
+
130
+ /** Browser tab glyph: a globe with meridians. */
131
+ export const IconGlobeOutline16 = ({ size = 16, className }: IconProps) => (
132
+ <svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
133
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.5" />
134
+ <ellipse cx="8" cy="8" rx="2.8" ry="6.5" stroke="currentColor" strokeWidth="1.5" />
135
+ <path d="M1.5 8h13M8 1.5c-2.4 1.8-2.4 11.2 0 13M8 1.5c2.4 1.8 2.4 11.2 0 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
136
+ </svg>
137
+ )
@@ -0,0 +1,8 @@
1
+ /** Image extension dispatch, separate from React for unit testing. */
2
+ export const IMAGE_EXTENSIONS = [
3
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico', '.avif',
4
+ ] as readonly string[]
5
+
6
+ export function isImageExt(ext: string): boolean {
7
+ return IMAGE_EXTENSIONS.includes(ext)
8
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * IME-composition key guard.
3
+ *
4
+ * While a Chinese/Japanese/Korean input method is composing (the user is
5
+ * picking a candidate from the IME window), every pressed key BELONGS to the
6
+ * input method: arrows move the candidate highlight, Enter/Space confirm the
7
+ * composition, Escape cancels it. Page code must not process those keys —
8
+ * a component that does (a number stepper calling preventDefault() on
9
+ * ArrowUp/ArrowDown, a submit handler reacting to Enter, ...) silently
10
+ * breaks the IME: candidates stop responding, the composition gets torn
11
+ * apart, and only bare letters come out.
12
+ *
13
+ * This guard enforces that rule at the document boundary: a capture-phase
14
+ * keydown/keyup listener that stops the event from propagating further
15
+ * whenever a composition is in progress. Because it runs in the capture
16
+ * phase on `document` — the outermost node — it fires BEFORE React's
17
+ * delegated handlers (attached at the root container) and before any native
18
+ * target/bubble listener, so an inlined third-party component (e.g. the
19
+ * Univer office UI bundled into this plugin) can never intercept
20
+ * composition keys. The browser's native IME processing is untouched:
21
+ * stopPropagation only silences page JS, not the default action.
22
+ *
23
+ * The composition signal follows the DSH core convention (InputBar's IME
24
+ * guard, issue #535): `isComposing` for modern engines, keyCode 229 as the
25
+ * legacy signal engines emit without isComposing.
26
+ */
27
+
28
+ /** The pure decision: is this keyboard event part of an IME composition? */
29
+ export function isImeComposition(event: { isComposing: boolean; keyCode: number }): boolean {
30
+ return event.isComposing || event.keyCode === 229
31
+ }
32
+
33
+ /**
34
+ * Register the document-level capture guard. Returns the disposer
35
+ * (HMR-safe; call through `ctx.effect`).
36
+ */
37
+ export function registerImeGuard(): () => void {
38
+ const onKey = (event: KeyboardEvent): void => {
39
+ if (isImeComposition(event)) event.stopPropagation()
40
+ }
41
+ document.addEventListener('keydown', onKey, true)
42
+ document.addEventListener('keyup', onKey, true)
43
+ return () => {
44
+ document.removeEventListener('keydown', onKey, true)
45
+ document.removeEventListener('keyup', onKey, true)
46
+ }
47
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Client half of dsh-better-sidebar: resolves the user's "Side card"
3
+ * preferences through the plugin's own fenced settings route, mounts the
4
+ * right sidebar portal (inside an error boundary so a rendering failure
5
+ * shows an error strip instead of a blank panel), registers the turn-tail
6
+ * interception, and contributes the Side card settings section to the DSH
7
+ * Settings shell. Requires the runtime's slots and sessions services; the
8
+ * bundle itself is a module-table consumer only (react + ui-primitives +
9
+ * xterm, all provided or inlined).
10
+ */
11
+ import { Component, createElement, type ErrorInfo, type ReactNode } from 'react'
12
+ import { createRoot, type Root } from 'react-dom/client'
13
+ import type { Context } from '../context-types.ts'
14
+ import { createSidebarStore } from './state.ts'
15
+ import { createBetterSidebarService } from './service.ts'
16
+ import { resetChunks } from './chunk-loader.ts'
17
+ import { registerBuiltins } from './builtins/index.ts'
18
+ import { Sidebar } from './Sidebar.tsx'
19
+ import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
20
+ import { registerLinkInterception } from './link-intercept.ts'
21
+ import { registerImeGuard } from './ime-guard.ts'
22
+ import { loadPrefs } from './prefs.ts'
23
+ import { SideCardSection } from './SideCardSection.tsx'
24
+ import { api } from './api.ts'
25
+ import { LOCALE_NS, attachLocale, t, zh, en } from './locales.ts'
26
+ import css from './sidebar.module.css'
27
+ import './layout.css'
28
+
29
+ /** Services required before mounting (provided by the client runtime; the
30
+ * locale service backs the sidebar's copy — see locales.ts). */
31
+ export const inject = ['slots', 'sessions', 'connection', 'workspaces', 'locale']
32
+
33
+ /**
34
+ * Error boundary over the sidebar tree: a render error must never blank the
35
+ * whole panel silently — it shows a dismissible error strip and logs the
36
+ * stack for diagnosis.
37
+ */
38
+ class SidebarBoundary extends Component<{ children: ReactNode }, { error: string | null }> {
39
+ state = { error: null as string | null }
40
+
41
+ static getDerivedStateFromError(error: unknown): { error: string } {
42
+ return { error: error instanceof Error ? error.message : String(error) }
43
+ }
44
+
45
+ componentDidCatch(error: Error, info: ErrorInfo): void {
46
+ console.error('[dsh-better-sidebar] render error:', error, info.componentStack)
47
+ }
48
+
49
+ render(): ReactNode {
50
+ if (this.state.error !== null) {
51
+ return (
52
+ <div className={css.boundaryError}>
53
+ <span>dsh-better-sidebar: {this.state.error}</span>
54
+ <button
55
+ type="button"
56
+ className={css.terminalRetry}
57
+ onClick={() => { this.setState({ error: null }) }}
58
+ >
59
+ {t('terminalRetry')}
60
+ </button>
61
+ </div>
62
+ )
63
+ }
64
+ return this.props.children
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Client plugin body.
70
+ * @param ctx - the client cordis context (slots, sessions).
71
+ */
72
+ export function apply(ctx: Context): void {
73
+ // The sidebar follows the DSH i18n system: attach the locale service so
74
+ // the module-level t()/isZh() resolve the Host-backed language preference
75
+ // (and switch live — the Sidebar root subscribes to it), and register the
76
+ // plugin's dictionaries into the shared locale registry. The disposers
77
+ // run on fiber disposal, so re-activation (HMR) re-registers cleanly.
78
+ attachLocale(ctx.locale)
79
+ ctx.effect(() => {
80
+ const offZh = ctx.locale.register(LOCALE_NS, 'zh', zh)
81
+ const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
82
+ return () => { offZh(); offEn() }
83
+ }, 'dsh-better-sidebar: dictionaries')
84
+ // One store instance per activation: production code creates it only here,
85
+ // then hands it to the mounted panel and closes over it in the slot
86
+ // registrations (the official createXXXStore() factory rule — no
87
+ // module-level singleton).
88
+ const sidebarStore = createSidebarStore()
89
+ // The sidebar registry service: external plugins register tab types and
90
+ // file previewers through `ctx.betterSidebar.registerTab/registerFileViewer`.
91
+ // Published before the panel mounts so consumers injecting 'betterSidebar'
92
+ // are ready by the time the sidebar renders.
93
+ const service = createBetterSidebarService(sidebarStore)
94
+ ctx.provide('betterSidebar', service)
95
+ // Register the plugin's own built-in tabs and viewers through the same
96
+ // service (eating our own dogfood). The disposer unregisters them on
97
+ // fiber disposal (HMR-safe).
98
+ ctx.effect(
99
+ () => registerBuiltins(ctx, service),
100
+ 'dsh-better-sidebar: register built-in tabs and viewers',
101
+ )
102
+ // A failure anywhere in the client lifecycle must never take the app down
103
+ // silently: log with the plugin prefix and pin a visible diagnostic strip
104
+ // to the page so a blank panel is never the only symptom.
105
+ const fail = (phase: string, error: unknown): void => {
106
+ console.error(`[dsh-better-sidebar] ${phase} error:`, error)
107
+ try {
108
+ const bar = document.createElement('div')
109
+ bar.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483000;max-width:70vw;padding:8px 12px;'
110
+ + 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f2a1a1;background:#1b1b22;'
111
+ + 'border:1px solid #f2a1a1;border-radius:8px;white-space:pre-wrap'
112
+ bar.textContent = `[dsh-better-sidebar] ${phase} error: ${error instanceof Error ? error.message : String(error)}`
113
+ document.body.appendChild(bar)
114
+ } catch {
115
+ // Nothing left to report with.
116
+ }
117
+ }
118
+ try {
119
+ // Fresh chunk state for this activation: invalidate any chunk factories
120
+ // registered by a previous fiber (HMR) and drop the in-memory load cache
121
+ // so the next lazy open re-fetches the current chunk scripts.
122
+ resetChunks()
123
+ ctx.effect(() => {
124
+ let disposed = false
125
+ let root: Root | undefined
126
+ let host: HTMLDivElement | undefined
127
+ void (async () => {
128
+ // Resolve the user's side card prefs BEFORE the first session seeds,
129
+ // so a brand-new conversation opens (or stays closed) at the chosen
130
+ // width from first paint. A settings route failure falls back to the
131
+ // schema defaults; the sidebar still mounts (a stalled wire gives up
132
+ // after the timeout and mounts on the defaults).
133
+ const prefs = await Promise.race([
134
+ loadPrefs(api),
135
+ new Promise<null>(resolve => { const timer = window.setTimeout(() => resolve(null), 2000) }),
136
+ ])
137
+ if (prefs !== null) sidebarStore.setPrefs(prefs)
138
+ if (disposed) return
139
+ try {
140
+ host = document.createElement('div')
141
+ host.setAttribute('data-dsh-better-sidebar', '')
142
+ document.body.appendChild(host)
143
+ root = createRoot(host)
144
+ root.render(createElement(SidebarBoundary, null, createElement(Sidebar, { ctx, store: sidebarStore })))
145
+ } catch (error) {
146
+ fail('mount', error)
147
+ }
148
+ })()
149
+ return () => {
150
+ disposed = true
151
+ root?.unmount()
152
+ host?.remove()
153
+ }
154
+ }, 'dsh-better-sidebar: sidebar mount')
155
+
156
+ ctx.effect(
157
+ () => {
158
+ try {
159
+ return registerTurnTailInterception(ctx, sidebarStore)
160
+ } catch (error) {
161
+ fail('interception', error)
162
+ return undefined
163
+ }
164
+ },
165
+ 'dsh-better-sidebar: turn-tail interception',
166
+ )
167
+
168
+ ctx.effect(
169
+ () => {
170
+ try {
171
+ return registerOpenPathInterception(ctx, sidebarStore)
172
+ } catch (error) {
173
+ fail('interception', error)
174
+ return undefined
175
+ }
176
+ },
177
+ 'dsh-better-sidebar: open-path interception',
178
+ )
179
+
180
+ ctx.effect(
181
+ () => {
182
+ try {
183
+ // External http(s) links in the chat/GUI open the sidebar browser
184
+ // instead of a new window (gated on the browserInterceptLinks pref
185
+ // AND the browser tab's enable switch; Ctrl/Cmd+click bypasses).
186
+ return registerLinkInterception({
187
+ takeoverEnabled: () => sidebarStore.getPrefs().browserInterceptLinks !== false
188
+ && sidebarStore.getPrefs().tabsEnabled['browser'] !== false,
189
+ openInSidebar: (url) => {
190
+ let title: string | undefined
191
+ try { title = new URL(url).hostname } catch { /* keep the default title */ }
192
+ ctx.betterSidebar?.openTab({ type: 'browser', url, title })
193
+ },
194
+ selfOrigin: window.location.origin,
195
+ })
196
+ } catch (error) {
197
+ fail('interception', error)
198
+ return undefined
199
+ }
200
+ },
201
+ 'dsh-better-sidebar: link interception',
202
+ )
203
+
204
+ // The IME guard: composition keys (candidate arrows, confirm, cancel)
205
+ // belong to the input method, never to page JS. Inlined third-party UI
206
+ // (Univer's office controls) has shipped unguarded keydown handlers that
207
+ // hijack ArrowUp/ArrowDown and break Chinese input (#562 regression); the
208
+ // document-capture guard neutralizes the whole class before React or any
209
+ // native listener sees the event. Registered as early as possible so no
210
+ // other capture-phase listener can win the ordering race.
211
+ ctx.effect(
212
+ () => {
213
+ try {
214
+ return registerImeGuard()
215
+ } catch (error) {
216
+ fail('ime guard', error)
217
+ return undefined
218
+ }
219
+ },
220
+ 'dsh-better-sidebar: IME composition guard',
221
+ )
222
+
223
+ // The "Side card" settings section: appears in the DSH Settings shell
224
+ // once the shell's declaration is on the ledger (slots.inject waits for
225
+ // it); the section reads/writes the prefs through the plugin's own
226
+ // fenced settings route, keeps the shared store in sync, and renders the
227
+ // declarative enable/disable inventory from the tab/viewer registry.
228
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
229
+ name: 'settings.section',
230
+ id: 'better-sidebar',
231
+ order: 100,
232
+ label: () => t('settingsNav'),
233
+ inject: () => ({ store: sidebarStore, service }),
234
+ }, SideCardSection))
235
+ } catch (error) {
236
+ fail('load', error)
237
+ }
238
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Interception of the chat's produced-files row: the turn-tail chain entry
3
+ * that replaces ui-deliverables' row when the closing turn produced files.
4
+ * The takeover looks identical (same chip row); the chips open the file in
5
+ * the sidebar instead of the host OS. Priority -1 runs before the default-0
6
+ * deliverables entry; when nothing was produced the selector returns null
7
+ * and the original row renders unchanged.
8
+ */
9
+ import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
10
+ import type { Context } from '../context-types.ts'
11
+ import type { SidebarStore } from './state.ts'
12
+ import { t } from './locales.ts'
13
+ import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
14
+ import { wrapOpenPath } from './openpath-intercept.ts'
15
+ import css from './sidebar.module.css'
16
+
17
+ /** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
18
+ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): void {
19
+ const summary = ctx.sessions.list.getSnapshot().byId[sessionId]
20
+ const absolute = resolveSidebarPath(summary?.cwd, path)
21
+ const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\'))
22
+ const title = at === -1 ? absolute : absolute.slice(at + 1)
23
+ // Route through the sidebar service so the editor descriptor's dedupeKey
24
+ // (per-path) applies; the id is path-derived so multiple editors coexist.
25
+ ctx.betterSidebar?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
26
+ }
27
+
28
+ /** The intercepted produced-files row (visual twin of the deliverables chips). */
29
+ export function SidebarProducedFiles(props: {
30
+ matched: readonly string[]
31
+ openInSidebar: (path: string) => void
32
+ }) {
33
+ const { matched, openInSidebar } = props
34
+ const shown = matched.slice(0, 6)
35
+ const hidden = matched.length - shown.length
36
+ return (
37
+ <div className={css.producedRow}>
38
+ <span className={css.producedLabel}>{t('produced')}</span>
39
+ {shown.map(path => {
40
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
41
+ const name = at === -1 ? path : path.slice(at + 1)
42
+ return (
43
+ <button
44
+ key={path}
45
+ type="button"
46
+ className={css.producedChip}
47
+ title={path}
48
+ onClick={() => { openInSidebar(path) }}
49
+ >
50
+ <IconCodeOutline16 size={12} />
51
+ <span>{name}</span>
52
+ </button>
53
+ )
54
+ })}
55
+ {hidden > 0 && <span className={css.producedMore}>+{hidden}</span>}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ /** Register the turn-tail interception (returns the disposer). */
61
+ export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void {
62
+ return ctx.slots.register({
63
+ name: 'conversation.chat.turnTail',
64
+ // Decline the takeover while the editor tab type is disabled in the side
65
+ // card settings: the produced-files row falls back to the default
66
+ // deliverables behavior instead of offering chips that cannot open.
67
+ select: (owner) => {
68
+ if (store.getPrefs().tabsEnabled['editor'] === false) return null
69
+ return selectProducedFiles(owner)
70
+ },
71
+ priority: -1,
72
+ registrant: 'dsh-better-sidebar',
73
+ inject: (sessionId: string) => ({
74
+ openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
75
+ }),
76
+ }, SidebarProducedFiles)
77
+ }
78
+
79
+ /**
80
+ * Register the chat file-open interception: wraps `ctx.workspaces.openPath`
81
+ * — the single funnel every chat-side file open goes through (tool-row path
82
+ * links, the produced-files row, prose mentions) — so opens land in the
83
+ * sidebar editor instead of the Host OS. Gated by BOTH the `interceptOpenPath`
84
+ * pref and the editor tab's enable switch; declined opens fall through to
85
+ * the original method. Returns the disposer restoring the original (HMR-safe).
86
+ */
87
+ export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
88
+ return wrapOpenPath(ctx.workspaces, {
89
+ takeoverEnabled: () => store.getPrefs().interceptOpenPath !== false
90
+ && store.getPrefs().tabsEnabled['editor'] !== false,
91
+ currentSessionId: () => ctx.sessions.list.getSnapshot().current,
92
+ openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
93
+ })
94
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Syntax highlighting for the file editor: extension → CodeMirror language
3
+ * mapping. The key derivation is pure and unit-tested; the factories pull in
4
+ * the CodeMirror language packages (bundled into the client).
5
+ */
6
+ import { Language, LanguageSupport, StreamLanguage } from '@codemirror/language'
7
+ import { javascript } from '@codemirror/lang-javascript'
8
+ import { json } from '@codemirror/lang-json'
9
+ import { markdown } from '@codemirror/lang-markdown'
10
+ import { python } from '@codemirror/lang-python'
11
+ import { html } from '@codemirror/lang-html'
12
+ import { css } from '@codemirror/lang-css'
13
+ import { xml } from '@codemirror/lang-xml'
14
+ import { yaml } from '@codemirror/lang-yaml'
15
+ import { sql } from '@codemirror/lang-sql'
16
+ import { java } from '@codemirror/lang-java'
17
+ import { cpp } from '@codemirror/lang-cpp'
18
+ import { rust } from '@codemirror/lang-rust'
19
+ import { go } from '@codemirror/lang-go'
20
+ import { php } from '@codemirror/lang-php'
21
+ import { shell } from '@codemirror/legacy-modes/mode/shell'
22
+ import { toml } from '@codemirror/legacy-modes/mode/toml'
23
+ import { nginx } from '@codemirror/legacy-modes/mode/nginx'
24
+ import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
25
+ import { properties } from '@codemirror/legacy-modes/mode/properties'
26
+
27
+ /** The lowercased file extension of a path ('' when none). */
28
+ export function extOf(path: string): string {
29
+ const at = path.lastIndexOf('.')
30
+ if (at === -1) return ''
31
+ const base = path.slice(at + 1).toLowerCase()
32
+ return base.includes('/') || base.includes('\\') ? '' : base
33
+ }
34
+
35
+ /** Language key for an extension, or null for plain text. Pure (tested). */
36
+ export function languageKeyForExt(ext: string): string | null {
37
+ switch (ext) {
38
+ case 'js': case 'mjs': case 'cjs': return 'js'
39
+ case 'jsx': return 'jsx'
40
+ case 'ts': case 'mts': case 'cts': return 'ts'
41
+ case 'tsx': return 'tsx'
42
+ case 'json': case 'jsonc': return 'json'
43
+ case 'md': case 'markdown': return 'md'
44
+ case 'py': case 'pyw': return 'python'
45
+ case 'html': case 'htm': return 'html'
46
+ case 'css': return 'css'
47
+ case 'xml': case 'xsl': return 'xml'
48
+ case 'yaml': case 'yml': return 'yaml'
49
+ case 'sql': return 'sql'
50
+ case 'java': return 'java'
51
+ case 'c': case 'h': return 'c'
52
+ case 'cc': case 'cpp': case 'cxx': case 'hpp': case 'hh': case 'hxx': return 'cpp'
53
+ case 'rs': return 'rust'
54
+ case 'go': return 'go'
55
+ case 'php': return 'php'
56
+ case 'sh': case 'bash': case 'zsh': return 'shell'
57
+ case 'toml': return 'toml'
58
+ case 'nginx': case 'conf': return 'nginx'
59
+ case 'dockerfile': case 'docker': return 'dockerfile'
60
+ case 'properties': case 'env': return 'properties'
61
+ default: return null
62
+ }
63
+ }
64
+
65
+ const FACTORIES: Record<string, () => Language | LanguageSupport> = {
66
+ js: () => javascript({ jsx: true }),
67
+ jsx: () => javascript({ jsx: true }),
68
+ ts: () => javascript({ typescript: true }),
69
+ tsx: () => javascript({ typescript: true, jsx: true }),
70
+ json: () => json(),
71
+ md: () => markdown(),
72
+ python: () => python(),
73
+ html: () => html(),
74
+ css: () => css(),
75
+ xml: () => xml(),
76
+ yaml: () => yaml(),
77
+ sql: () => sql(),
78
+ java: () => java(),
79
+ c: () => cpp(),
80
+ cpp: () => cpp(),
81
+ rust: () => rust(),
82
+ go: () => go(),
83
+ php: () => php(),
84
+ shell: () => StreamLanguage.define(shell),
85
+ toml: () => StreamLanguage.define(toml),
86
+ nginx: () => StreamLanguage.define(nginx),
87
+ dockerfile: () => StreamLanguage.define(dockerFile),
88
+ properties: () => StreamLanguage.define(properties),
89
+ }
90
+
91
+ /** The CodeMirror language support for a path, or null for plain text. */
92
+ export function languageForPath(path: string): Language | LanguageSupport | null {
93
+ const key = languageKeyForExt(extOf(path))
94
+ return key === null ? null : FACTORIES[key]!()
95
+ }