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.
- package/README.md +27 -42
- package/lib/client-editor.js +449 -4278
- package/lib/client-registry.js +641 -1343
- package/lib/client-terminal.js +172 -262
- package/lib/client.js +645 -1347
- package/lib/index.js +24 -122
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/SideCardSection.d.ts +3 -3
- package/lib/types/client/TextEditor.d.ts +20 -10
- package/lib/types/client/add-plugin-modal.d.ts +0 -4
- package/lib/types/client/api.d.ts +8 -11
- package/lib/types/client/builtins/index.d.ts +10 -10
- package/lib/types/client/chunk-loader.d.ts +7 -8
- package/lib/types/client/locales.d.ts +0 -26
- package/lib/types/client/native-avoid.d.ts +2 -0
- package/lib/types/client/plugins-shared.d.ts +6 -6
- package/lib/types/client/service.d.ts +13 -83
- package/lib/types/context-types.d.ts +19 -10
- package/lib/types/index.d.ts +1 -1
- package/lib/types/prefs-shared.d.ts +1 -24
- package/package.json +26 -30
- package/src/bundle-route.ts +1 -1
- package/src/client/EditorHost.tsx +52 -122
- package/src/client/SandboxStatusBar.tsx +12 -9
- package/src/client/SideCardSection.module.css +2 -2
- package/src/client/SideCardSection.tsx +28 -81
- package/src/client/SideChatView.module.css +12 -12
- package/src/client/TextEditor.tsx +89 -266
- package/src/client/add-plugin-modal.tsx +16 -24
- package/src/client/api.ts +1 -25
- package/src/client/builtins/index.ts +10 -14
- package/src/client/chunk-loader.ts +7 -8
- package/src/client/index.tsx +12 -3
- package/src/client/locales-ar.ts +0 -6
- package/src/client/locales-de.ts +0 -6
- package/src/client/locales-fr.ts +0 -6
- package/src/client/locales-hi.ts +0 -6
- package/src/client/locales-id.ts +0 -6
- package/src/client/locales-it.ts +0 -6
- package/src/client/locales-ja.ts +0 -6
- package/src/client/locales-ko.ts +0 -6
- package/src/client/locales-nl.ts +0 -6
- package/src/client/locales-pl.ts +0 -6
- package/src/client/locales-pt.ts +0 -6
- package/src/client/locales-ru.ts +0 -6
- package/src/client/locales-sv.ts +0 -6
- package/src/client/locales-th.ts +0 -6
- package/src/client/locales-tr.ts +0 -6
- package/src/client/locales-vi.ts +0 -6
- package/src/client/locales-zh-HK.ts +0 -6
- package/src/client/locales-zh-MO.ts +0 -6
- package/src/client/locales-zh-TW.ts +0 -6
- package/src/client/locales.ts +0 -52
- package/src/client/native-avoid.ts +155 -0
- package/src/client/plugins-shared.ts +6 -6
- package/src/client/prefs.ts +0 -7
- package/src/client/service.ts +13 -150
- package/src/client/sidebar.module.css +1 -425
- package/src/config.ts +1 -4
- package/src/context-types.ts +12 -7
- package/src/index.ts +12 -78
- package/src/jobs-routes.ts +3 -2
- package/src/prefs-shared.ts +1 -27
- package/src/sidechat-routes.ts +13 -8
- package/src/subagent-live-route.ts +2 -1
- package/lib/client-mermaid.js +0 -200753
- package/lib/types/client/MarkdownHtml.d.ts +0 -32
- package/lib/types/client/PdfView.d.ts +0 -6
- package/lib/types/client/builtins/viewers.d.ts +0 -3
- package/lib/types/client/chunks/mermaid.d.ts +0 -10
- package/lib/types/client/editor-load.d.ts +0 -66
- package/lib/types/client/image-types.d.ts +0 -3
- package/lib/types/client/markdown-html.d.ts +0 -96
- package/lib/types/client/markdown-images.d.ts +0 -45
- package/lib/types/client/md-toc.d.ts +0 -4
- package/lib/types/client/mermaid-blocks.d.ts +0 -47
- package/lib/types/client/mermaid-sanitize.d.ts +0 -2
- package/lib/types/client/mermaid.d.ts +0 -12
- package/lib/types/client/pdf-types.d.ts +0 -2
- package/lib/types/client/plugins-viewers.d.ts +0 -3
- package/lib/types/html-route.d.ts +0 -59
- package/src/client/MarkdownHtml.tsx +0 -296
- package/src/client/PdfView.tsx +0 -110
- package/src/client/builtins/viewers.tsx +0 -125
- package/src/client/chunks/mermaid.tsx +0 -10
- package/src/client/editor-load.ts +0 -92
- package/src/client/image-types.ts +0 -8
- package/src/client/markdown-html.ts +0 -331
- package/src/client/markdown-images.ts +0 -137
- package/src/client/md-toc.tsx +0 -127
- package/src/client/mermaid-blocks.ts +0 -110
- package/src/client/mermaid-sanitize.ts +0 -93
- package/src/client/mermaid.tsx +0 -406
- package/src/client/pdf-types.ts +0 -4
- package/src/client/plugins-viewers.ts +0 -30
- package/src/html-route.ts +0 -106
|
@@ -1,45 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The sidebar text editor: a CodeMirror 6 editor with line wrapping,
|
|
3
3
|
* syntax highlighting (extension-keyed language), a dirty dot and Ctrl/Cmd+S
|
|
4
|
-
* save
|
|
5
|
-
*
|
|
6
|
-
* fetches the content through the fsRead strategy and passes it in props,
|
|
7
|
-
* so this component never fetches or dispatches — it only edits.
|
|
4
|
+
* save. The editor tab host fetches the content through fs.read and passes
|
|
5
|
+
* it in props, so this component never fetches — it only edits.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* (v1.0.4: the preview/edit mode toggle, markdown/mermaid/HTML preview
|
|
8
|
+
* surfaces and the HTML sandbox machinery were retired with the file-viewer
|
|
9
|
+
* line — file preview is the host's job now; this component edits.)
|
|
10
|
+
*
|
|
11
|
+
* The toolbar (dirty dot / save / status) renders as its own row below the
|
|
12
|
+
* host's title bar — unless the host passes `toolbar: 'host'` (the merged
|
|
13
|
+
* editor-explorer mode), in which case this component skips the row and
|
|
14
|
+
* reports state + registers commands through the toolbar callbacks so the
|
|
15
|
+
* host's path-input header renders the controls instead.
|
|
15
16
|
*/
|
|
16
|
-
import { useEffect,
|
|
17
|
+
import { useEffect, useRef, useState } from 'react'
|
|
17
18
|
import { createPortal } from 'react-dom'
|
|
18
19
|
import clsx from 'clsx'
|
|
19
20
|
import { EditorState } from '@codemirror/state'
|
|
20
21
|
import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view'
|
|
21
22
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
|
22
|
-
import { IconCheckOutline16
|
|
23
|
-
import {
|
|
24
|
-
import { api, htmlUrl } from './api.ts'
|
|
25
|
-
import { rewriteLocalImageUrls } from './markdown-images.ts'
|
|
23
|
+
import { IconCheckOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
24
|
+
import { api } from './api.ts'
|
|
26
25
|
import { languageForPath } from './lang.ts'
|
|
27
26
|
import { cmSurfaceTheme, CmThemeCompartment } from './cm-themes.ts'
|
|
28
27
|
import { isDarkScheme, subscribeColorScheme } from './theme.ts'
|
|
29
|
-
import { SandboxStatusBar } from './SandboxStatusBar.tsx'
|
|
30
28
|
import { appendToDraft } from './conversation-draft.ts'
|
|
31
|
-
import { buildSelectionInsert
|
|
32
|
-
import { lazyChunkComponent } from './lazy-chunk.tsx'
|
|
33
|
-
import { analyzeMarkdownHtml } from './markdown-html.ts'
|
|
34
|
-
import { LazyMermaidMarkdown, MarkdownDocument, type MarkdownHtmlMedia } from './MarkdownHtml.tsx'
|
|
35
|
-
import { MdToc } from './md-toc.tsx'
|
|
36
|
-
import { splitMermaidBlocks } from './mermaid-blocks.ts'
|
|
29
|
+
import { buildSelectionInsert } from './selection-payload.ts'
|
|
37
30
|
import { t } from './locales.ts'
|
|
38
|
-
import type {
|
|
31
|
+
import type { EditorToolbarControls, EditorToolbarState } from './service.ts'
|
|
32
|
+
import type { Context } from '../context-types.ts'
|
|
33
|
+
import type { SessionScope } from './api.ts'
|
|
34
|
+
import type { SidebarStore } from './state.ts'
|
|
39
35
|
import css from './sidebar.module.css'
|
|
40
36
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
37
|
+
/** Props of the sidebar text editor (the editor tab's content). */
|
|
38
|
+
export interface TextEditorProps {
|
|
39
|
+
ctx: Context
|
|
40
|
+
store?: SidebarStore
|
|
41
|
+
scope: SessionScope
|
|
42
|
+
path: string
|
|
43
|
+
/** fs.read text content (undefined while loading / for non-editable reads). */
|
|
44
|
+
content?: string
|
|
45
|
+
truncated?: boolean
|
|
46
|
+
/** 'host' skips the own toolbar row — the editor host's merged-mode header
|
|
47
|
+
* renders it instead, fed through the two callbacks below. */
|
|
48
|
+
toolbar?: 'self' | 'host'
|
|
49
|
+
onToolbarState?: (state: EditorToolbarState) => void
|
|
50
|
+
onToolbarControls?: (controls: EditorToolbarControls | null) => void
|
|
51
|
+
}
|
|
43
52
|
|
|
44
53
|
/** The floating "add to conversation" action: payload + viewport anchor. */
|
|
45
54
|
interface SelectionPopup {
|
|
@@ -48,20 +57,8 @@ interface SelectionPopup {
|
|
|
48
57
|
top: number
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* preview must stay in an opaque origin — with the route's own origin it
|
|
54
|
-
* could read session data) and NO allow-top-navigation (a previewed page
|
|
55
|
-
* must not hijack the GUI). The user can disable the sandbox per-feature
|
|
56
|
-
* in the side card settings (warned); the toggle below reflects it.
|
|
57
|
-
*/
|
|
58
|
-
export const HTML_IFRAME_SANDBOX = 'allow-scripts allow-popups allow-downloads allow-modals'
|
|
59
|
-
|
|
60
|
-
export function TextEditor(props: FileViewerProps) {
|
|
61
|
-
const { ctx, scope, path, viewerId, content, truncated } = props
|
|
62
|
-
const [mode, setMode] = useState<ViewMode>('preview')
|
|
63
|
-
/** The editor's current text (null while clean); preview renders this. */
|
|
64
|
-
const [draft, setDraft] = useState<string | null>(null)
|
|
60
|
+
export function TextEditor(props: TextEditorProps) {
|
|
61
|
+
const { ctx, scope, path, content, truncated } = props
|
|
65
62
|
const [dirty, setDirty] = useState(false)
|
|
66
63
|
const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved' | 'failed'>('idle')
|
|
67
64
|
const hostRef = useRef<HTMLDivElement>(null)
|
|
@@ -75,8 +72,6 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
75
72
|
const [popup, setPopup] = useState<SelectionPopup | null>(null)
|
|
76
73
|
/** Live mirror of the popup state for click-time reads (no re-render race). */
|
|
77
74
|
const popupRef = useRef<SelectionPopup | null>(null)
|
|
78
|
-
/** The markdown preview container (selection-containment + line lookup). */
|
|
79
|
-
const mdRef = useRef<HTMLDivElement>(null)
|
|
80
75
|
|
|
81
76
|
const hidePopup = (): void => {
|
|
82
77
|
popupRef.current = null
|
|
@@ -104,21 +99,18 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
104
99
|
|
|
105
100
|
useEffect(() => subscribeColorScheme(() => { setDark(isDarkScheme()) }), [])
|
|
106
101
|
|
|
107
|
-
// A new file (tab switch) starts clean:
|
|
102
|
+
// A new file (tab switch) starts clean: no draft.
|
|
108
103
|
useEffect(() => {
|
|
109
|
-
setMode('preview')
|
|
110
|
-
setDraft(null)
|
|
111
104
|
setDirty(false)
|
|
112
105
|
setSaveState('idle')
|
|
113
106
|
hidePopup()
|
|
114
107
|
}, [content])
|
|
115
108
|
|
|
116
109
|
// Create the CodeMirror editor once the content is loaded. The view owns
|
|
117
|
-
// the document; React only tracks dirty
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// part — the document, undo history and scroll position survive.
|
|
110
|
+
// the document; React only tracks dirty state through the update listener.
|
|
111
|
+
// The theme + syntax colors live in a compartment so a scheme flip
|
|
112
|
+
// reconfigures only that part — the document, undo history and scroll
|
|
113
|
+
// position survive.
|
|
122
114
|
useEffect(() => {
|
|
123
115
|
if (content === undefined) return
|
|
124
116
|
const host = hostRef.current
|
|
@@ -139,7 +131,6 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
139
131
|
...(language !== null ? [language] : []),
|
|
140
132
|
CodeMirrorView.updateListener.of((update) => {
|
|
141
133
|
if (update.docChanged) {
|
|
142
|
-
setDraft(update.state.doc.toString())
|
|
143
134
|
setDirty(true)
|
|
144
135
|
}
|
|
145
136
|
}),
|
|
@@ -152,49 +143,47 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
152
143
|
...defaultKeymap,
|
|
153
144
|
...historyKeymap,
|
|
154
145
|
]),
|
|
155
|
-
// Selection popup
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}),
|
|
197
|
-
] : []),
|
|
146
|
+
// Selection popup: a non-empty selection anchors the floating
|
|
147
|
+
// "add to conversation" button above its head. Scrolling (geometry/
|
|
148
|
+
// viewport change) or losing focus hides it; typing collapses the
|
|
149
|
+
// selection and hides it too.
|
|
150
|
+
CodeMirrorView.updateListener.of((update) => {
|
|
151
|
+
if (update.geometryChanged || update.viewportChanged) {
|
|
152
|
+
hidePopup()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (!update.view.hasFocus) {
|
|
156
|
+
hidePopup()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (!(update.selectionSet || update.docChanged || update.focusChanged)) return
|
|
160
|
+
const sel = update.state.selection.main
|
|
161
|
+
if (sel.empty) {
|
|
162
|
+
hidePopup()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const text = update.state.sliceDoc(sel.from, sel.to)
|
|
166
|
+
if (text.trim() === '') {
|
|
167
|
+
hidePopup()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
// Page coordinates (the document root may scroll); the popup is
|
|
171
|
+
// position:fixed, so convert to viewport coordinates.
|
|
172
|
+
const rect = update.view.coordsAtPos(sel.head)
|
|
173
|
+
if (rect === null) {
|
|
174
|
+
hidePopup()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
const doc = update.state.doc
|
|
178
|
+
showPopup(
|
|
179
|
+
buildSelectionInsert(path, scope.cwd, {
|
|
180
|
+
start: doc.lineAt(sel.from).number,
|
|
181
|
+
end: doc.lineAt(sel.to).number,
|
|
182
|
+
}, text),
|
|
183
|
+
rect.left - window.scrollX + (rect.right - rect.left) / 2,
|
|
184
|
+
rect.top - window.scrollY,
|
|
185
|
+
)
|
|
186
|
+
}),
|
|
198
187
|
],
|
|
199
188
|
})
|
|
200
189
|
const view = new CodeMirrorView({ state, parent: host })
|
|
@@ -218,14 +207,6 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
218
207
|
view.dispatch({ effects: themeComp.reconfigure(dark) })
|
|
219
208
|
}, [dark])
|
|
220
209
|
|
|
221
|
-
// The editor may have been display:none while previewing; re-measure when
|
|
222
|
-
// it becomes visible again (CodeMirror sizes itself on reveal). A mode
|
|
223
|
-
// flip also invalidates any anchored selection popup.
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
hidePopup()
|
|
226
|
-
if (mode === 'edit') viewRef.current?.requestMeasure()
|
|
227
|
-
}, [mode])
|
|
228
|
-
|
|
229
210
|
const save = (): void => {
|
|
230
211
|
const view = viewRef.current
|
|
231
212
|
if (view === null || savingRef.current) return
|
|
@@ -233,7 +214,6 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
233
214
|
setSaveState('saving')
|
|
234
215
|
api.fsWrite(scope, path, view.state.doc.toString()).then(() => {
|
|
235
216
|
savingRef.current = false
|
|
236
|
-
setDraft(null)
|
|
237
217
|
setDirty(false)
|
|
238
218
|
setSaveState('saved')
|
|
239
219
|
}).catch(() => {
|
|
@@ -242,91 +222,8 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
242
222
|
})
|
|
243
223
|
}
|
|
244
224
|
|
|
245
|
-
const markdown = viewerId === 'markdown'
|
|
246
|
-
const html = viewerId === 'html'
|
|
247
|
-
/** The markdown source the preview renders (draft wins over saved content). */
|
|
248
|
-
const mdText = draft ?? content ?? ''
|
|
249
|
-
/** The preview source: `mdText` with local image destinations rewritten to
|
|
250
|
-
* absolute media URLs (see {@link rewriteLocalImageUrls}); the raw
|
|
251
|
-
* `mdText` stays untouched for selection/line lookup and for mermaid-block
|
|
252
|
-
* detection, which are unaffected by image syntax. */
|
|
253
|
-
const previewText = markdown
|
|
254
|
-
? rewriteLocalImageUrls(mdText, scope, path, window.location.origin)
|
|
255
|
-
: mdText
|
|
256
|
-
/** md/mermaid block split for the preview (mermaid fences lift out). Split
|
|
257
|
-
* only in preview mode: edit-mode keystrokes must not re-scan the source. */
|
|
258
|
-
const mdBlocks = useMemo(
|
|
259
|
-
() => (markdown && mode === 'preview' ? splitMermaidBlocks(mdText) : []),
|
|
260
|
-
[markdown, mode, mdText],
|
|
261
|
-
)
|
|
262
|
-
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null only
|
|
263
|
-
* for documents that actually contain HTML — plain markdown keeps the
|
|
264
|
-
* legacy single-pass render path below, byte-for-byte. */
|
|
265
|
-
const htmlInfo = useMemo(
|
|
266
|
-
() => (markdown && mode === 'preview' ? analyzeMarkdownHtml(mdText) : null),
|
|
267
|
-
[markdown, mode, mdText],
|
|
268
|
-
)
|
|
269
|
-
const hasMermaid = useMemo(
|
|
270
|
-
() => htmlInfo !== null
|
|
271
|
-
? htmlInfo.segments.some((segment) => segment.kind === 'markdown'
|
|
272
|
-
&& splitMermaidBlocks(segment.text).some((block) => block.kind === 'mermaid'))
|
|
273
|
-
: mdBlocks.some((block) => block.kind === 'mermaid'),
|
|
274
|
-
[htmlInfo, mdBlocks],
|
|
275
|
-
)
|
|
276
|
-
/** The media context for the split renderer (local-src rewriting inside
|
|
277
|
-
* sanitized HTML). Memoized on primitives: MarkdownDocument sanitizes per
|
|
278
|
-
* `media` identity, so a fresh object per render would re-sanitize every
|
|
279
|
-
* keystroke. */
|
|
280
|
-
const htmlMedia = useMemo<MarkdownHtmlMedia>(
|
|
281
|
-
() => ({ scope, path, origin: window.location.origin }),
|
|
282
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
283
|
-
[scope.sessionId, scope.cwd, path],
|
|
284
|
-
)
|
|
285
|
-
const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Selection popup for the markdown preview: a mouse-up inside the preview
|
|
289
|
-
* container anchors the floating "add to conversation" button above the
|
|
290
|
-
* selection. Line numbers come from a best-effort reverse-search of the
|
|
291
|
-
* selected text in the source ({@link linesOfSelection} — an ambiguous or
|
|
292
|
-
* missing hit omits them). The button's own mousedown preventDefaults so
|
|
293
|
-
* the selection survives until the click commits.
|
|
294
|
-
*/
|
|
295
|
-
const handlePreviewMouseUp = (): void => {
|
|
296
|
-
const sel = window.getSelection()
|
|
297
|
-
if (sel === null || sel.isCollapsed || sel.anchorNode === null || sel.focusNode === null) {
|
|
298
|
-
hidePopup()
|
|
299
|
-
return
|
|
300
|
-
}
|
|
301
|
-
const host = mdRef.current
|
|
302
|
-
if (host === null || !host.contains(sel.anchorNode) || !host.contains(sel.focusNode)) {
|
|
303
|
-
hidePopup()
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
const text = sel.toString()
|
|
307
|
-
if (text.trim() === '') {
|
|
308
|
-
hidePopup()
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
const rect = sel.getRangeAt(0).getBoundingClientRect()
|
|
312
|
-
const lines = linesOfSelection(mdText, text)
|
|
313
|
-
showPopup(
|
|
314
|
-
buildSelectionInsert(path, scope.cwd, lines ?? undefined, text),
|
|
315
|
-
rect.left + rect.width / 2,
|
|
316
|
-
rect.top,
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
225
|
const editable = content !== undefined
|
|
320
226
|
const saveLabel = saveState === 'saving' ? t('loading') : saveState === 'saved' ? t('saved') : saveState === 'failed' ? t('saveFailed') : ''
|
|
321
|
-
// Per-feature sandbox escape hatch: the global side card setting (warned)
|
|
322
|
-
// plus a per-surface temporary unlock. The unlock state starts at the
|
|
323
|
-
// "default unsafe" pref so a preview can open straight into the red
|
|
324
|
-
// unsandboxed state (still restorable from the status row). With the
|
|
325
|
-
// sandbox OFF the preview iframe drops its sandbox attribute entirely —
|
|
326
|
-
// the previewed page then runs on the GUI's own origin with full session
|
|
327
|
-
// access.
|
|
328
|
-
const [localUnlock, setLocalUnlock] = useState(() => props.store?.getPrefs().htmlViewerDefaultUnsafe === true)
|
|
329
|
-
const htmlNoSandbox = props.store?.getPrefs().htmlViewerNoSandbox === true || localUnlock
|
|
330
227
|
|
|
331
228
|
// Host-toolbar mode (the merged editor header renders the controls): skip
|
|
332
229
|
// the own toolbar row, report the state after every relevant render (the
|
|
@@ -335,7 +232,7 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
335
232
|
const lastToolbarRef = useRef('')
|
|
336
233
|
useEffect(() => {
|
|
337
234
|
if (!hostToolbar) return
|
|
338
|
-
const state: EditorToolbarState = {
|
|
235
|
+
const state: EditorToolbarState = { dirty, editable, saveState }
|
|
339
236
|
const key = JSON.stringify(state)
|
|
340
237
|
if (lastToolbarRef.current === key) return
|
|
341
238
|
lastToolbarRef.current = key
|
|
@@ -343,9 +240,9 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
343
240
|
})
|
|
344
241
|
useEffect(() => {
|
|
345
242
|
if (!hostToolbar) return
|
|
346
|
-
// `save` reads live refs only
|
|
347
|
-
//
|
|
348
|
-
props.onToolbarControls?.({
|
|
243
|
+
// `save` reads live refs only — registering this render's closure is
|
|
244
|
+
// safe for the mount's lifetime.
|
|
245
|
+
props.onToolbarControls?.({ save })
|
|
349
246
|
return () => { props.onToolbarControls?.(null) }
|
|
350
247
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
351
248
|
}, [hostToolbar])
|
|
@@ -354,24 +251,6 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
354
251
|
<>
|
|
355
252
|
{!hostToolbar && (
|
|
356
253
|
<div className={css.editorHeader}>
|
|
357
|
-
{(markdown || html) && (
|
|
358
|
-
<div className={css.editorModeToggle}>
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
className={clsx(css.editorModeButton, mode === 'preview' && css.editorModeActive)}
|
|
362
|
-
onClick={() => { setMode('preview') }}
|
|
363
|
-
>
|
|
364
|
-
{t('preview')}
|
|
365
|
-
</button>
|
|
366
|
-
<button
|
|
367
|
-
type="button"
|
|
368
|
-
className={clsx(css.editorModeButton, mode === 'edit' && css.editorModeActive)}
|
|
369
|
-
onClick={() => { setMode('edit') }}
|
|
370
|
-
>
|
|
371
|
-
{t('edit')}
|
|
372
|
-
</button>
|
|
373
|
-
</div>
|
|
374
|
-
)}
|
|
375
254
|
{dirty && <span className={css.dirtyDot} title={t('unsaved')} />}
|
|
376
255
|
{editable && (
|
|
377
256
|
<button
|
|
@@ -389,64 +268,8 @@ export function TextEditor(props: FileViewerProps) {
|
|
|
389
268
|
)}
|
|
390
269
|
{editable && (
|
|
391
270
|
<>
|
|
392
|
-
{truncated === true &&
|
|
393
|
-
<div
|
|
394
|
-
className={clsx(css.editorCm, (markdown || html) && mode === 'preview' && css.editorCmHidden)}
|
|
395
|
-
ref={hostRef}
|
|
396
|
-
/>
|
|
397
|
-
</>
|
|
398
|
-
)}
|
|
399
|
-
{markdown && mode === 'preview' && (
|
|
400
|
-
<div
|
|
401
|
-
className={css.editorMd}
|
|
402
|
-
ref={mdRef}
|
|
403
|
-
onMouseUp={handlePreviewMouseUp}
|
|
404
|
-
onScroll={hidePopup}
|
|
405
|
-
>
|
|
406
|
-
{/* The fence copy-button labels must come from this plugin's own
|
|
407
|
-
dictionary: the DSH MarkdownText/CodeBlock are cordis-free and
|
|
408
|
-
fall back to hardcoded Chinese otherwise (same pattern as the
|
|
409
|
-
chat's AssistantMarkdown). Render-time t() keeps them following
|
|
410
|
-
the active locale on live switches. Plain markdown (no HTML)
|
|
411
|
-
renders exactly as before — one MarkdownText pass for the whole
|
|
412
|
-
document, or the mermaid lazy chunk (single markdown parse;
|
|
413
|
-
cross-fence references/footnotes stay intact) when a mermaid
|
|
414
|
-
fence exists. Documents containing HTML (block runs or inline
|
|
415
|
-
tags) render through the split document renderer: markdown runs
|
|
416
|
-
keep the MarkdownText/mermaid path while raw-HTML runs render
|
|
417
|
-
as sanitized DOM (see markdown-html.tsx). */}
|
|
418
|
-
{/* The outline button rides on top of the preview scroll container
|
|
419
|
-
(sticky, zero-height — first child so it pins from the very
|
|
420
|
-
top) once the document has enough headings. */}
|
|
421
|
-
<MdToc />
|
|
422
|
-
{htmlInfo !== null
|
|
423
|
-
? <MarkdownDocument info={htmlInfo} media={htmlMedia} codeLabels={codeLabels} />
|
|
424
|
-
: hasMermaid
|
|
425
|
-
? <LazyMermaidMarkdown text={previewText} codeLabels={codeLabels} />
|
|
426
|
-
: <MarkdownText {...markdownTextProps(previewText, codeLabels)} />}
|
|
427
|
-
</div>
|
|
428
|
-
)}
|
|
429
|
-
{html && mode === 'preview' && (
|
|
430
|
-
<>
|
|
431
|
-
<SandboxStatusBar
|
|
432
|
-
sandboxed={!htmlNoSandbox}
|
|
433
|
-
local={localUnlock}
|
|
434
|
-
dangerCopy={t('htmlNoSandboxWarning')}
|
|
435
|
-
onUnlock={() => { setLocalUnlock(true) }}
|
|
436
|
-
onRestore={() => { setLocalUnlock(false) }}
|
|
437
|
-
/>
|
|
438
|
-
{/* Route-src (never srcdoc — a srcdoc frame inherits the parent
|
|
439
|
-
origin when unsandboxed; the route URL keeps the frame
|
|
440
|
-
cross-origin by construction). The preview shows the SAVED
|
|
441
|
-
file; the draft is only visible in edit mode. */}
|
|
442
|
-
<iframe
|
|
443
|
-
className={css.editorHtml}
|
|
444
|
-
src={htmlUrl(scope, path)}
|
|
445
|
-
sandbox={htmlNoSandbox ? undefined : HTML_IFRAME_SANDBOX}
|
|
446
|
-
referrerPolicy="no-referrer"
|
|
447
|
-
allow=""
|
|
448
|
-
title={path}
|
|
449
|
-
/>
|
|
271
|
+
{truncated === true && <div className={css.editorBanner}>{t('truncation')}</div>}
|
|
272
|
+
<div className={css.editorCm} ref={hostRef} />
|
|
450
273
|
</>
|
|
451
274
|
)}
|
|
452
275
|
{popup !== null && createPortal(
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The "add plugin"
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The "add plugin" modal (Side card settings → the dashed card at the end
|
|
3
|
+
* of the 侧边栏内容 grid): declares that the sidebar's tab extension point
|
|
4
|
+
* is open to plugins (registered through `ctx.betterSidebar`), points at
|
|
5
|
+
* the GitHub topic page for discovery, and shows the repo's recommended
|
|
6
|
+
* plugin catalog (name / url / description / install script).
|
|
7
|
+
*
|
|
8
|
+
* (v1.0.4: the file-viewer extension point was retired with the file-viewer
|
|
9
|
+
* registry — this modal now catalogs tab plugins only.)
|
|
8
10
|
*
|
|
9
11
|
* Per entry there are two actions:
|
|
10
12
|
* - 「跳转」opens the plugin's repo in a REAL new browser tab (window.open
|
|
@@ -24,26 +26,16 @@ import { Modal, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
24
26
|
import type { BetterSidebarService } from './service.ts'
|
|
25
27
|
import { PLUGIN_TOPIC_URL, type PluginEntry } from './plugins-shared.ts'
|
|
26
28
|
import { builtinTabPlugins } from './plugins-tabs.ts'
|
|
27
|
-
import { builtinViewerPlugins } from './plugins-viewers.ts'
|
|
28
29
|
import { t } from './locales.ts'
|
|
29
30
|
import css from './SideCardSection.module.css'
|
|
30
31
|
|
|
31
|
-
/** Which extension point the modal is adding a plugin for. */
|
|
32
|
-
export type PluginKind = 'tab' | 'viewer'
|
|
33
|
-
|
|
34
|
-
/** The catalog of one kind (kept in two repo files: plugins-tabs.ts /
|
|
35
|
-
* plugins-viewers.ts). */
|
|
36
|
-
function catalogOf(kind: PluginKind): readonly PluginEntry[] {
|
|
37
|
-
return kind === 'tab' ? builtinTabPlugins : builtinViewerPlugins
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
/** How long the "已复制" feedback stays on the copy button. */
|
|
41
33
|
const COPIED_FEEDBACK_MS = 1500
|
|
42
34
|
|
|
43
35
|
/** The modal body: the GitHub topic button + the recommended plugin list
|
|
44
36
|
* with per-entry jump/copy buttons (extracted for direct testing). */
|
|
45
|
-
export function PluginListBody(props: { service: BetterSidebarService
|
|
46
|
-
const { service
|
|
37
|
+
export function PluginListBody(props: { service: BetterSidebarService }) {
|
|
38
|
+
const { service } = props
|
|
47
39
|
// Which entry's copy button currently shows the "已复制" feedback.
|
|
48
40
|
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
49
41
|
// Live catalog filter (name / id / description). A free-text search keeps
|
|
@@ -52,7 +44,7 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
|
|
|
52
44
|
// becomes a wall of scrolling.
|
|
53
45
|
const [query, setQuery] = useState('')
|
|
54
46
|
|
|
55
|
-
const catalog =
|
|
47
|
+
const catalog = builtinTabPlugins
|
|
56
48
|
const needle = query.trim().toLowerCase()
|
|
57
49
|
const matches = (entry: PluginEntry): boolean => {
|
|
58
50
|
if (needle === '') return true
|
|
@@ -177,14 +169,14 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
|
|
|
177
169
|
}
|
|
178
170
|
|
|
179
171
|
/** The modal itself (mounted only while open — see the module comment). */
|
|
180
|
-
export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void
|
|
181
|
-
const { service, onClose
|
|
172
|
+
export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void }) {
|
|
173
|
+
const { service, onClose } = props
|
|
182
174
|
return (
|
|
183
175
|
<Modal
|
|
184
176
|
open
|
|
185
177
|
onClose={onClose}
|
|
186
|
-
title={
|
|
187
|
-
description={
|
|
178
|
+
title={t('addPluginsTabCard')}
|
|
179
|
+
description={t('addPluginsTabDesc')}
|
|
188
180
|
closeLabel={t('close')}
|
|
189
181
|
className={css.pluginModal}
|
|
190
182
|
footer={(
|
|
@@ -193,7 +185,7 @@ export function AddPluginModal(props: { service: BetterSidebarService; onClose:
|
|
|
193
185
|
</button>
|
|
194
186
|
)}
|
|
195
187
|
>
|
|
196
|
-
<PluginListBody service={service}
|
|
188
|
+
<PluginListBody service={service} />
|
|
197
189
|
</Modal>
|
|
198
190
|
)
|
|
199
191
|
}
|
package/src/client/api.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* hydrating at page load (a detached session would otherwise fail the
|
|
7
7
|
* request). Failures surface as {@link SidebarApiError} with the wire code.
|
|
8
8
|
*/
|
|
9
|
-
import { encodeHtmlUrl } from '../html-route.ts'
|
|
10
9
|
import type { LastActivity } from '../subagent-activity.ts'
|
|
11
10
|
import type { SidechatThreadInfo } from '../sidechat-core.ts'
|
|
12
11
|
import type { BrowserProbeResult } from './browser.ts'
|
|
@@ -325,33 +324,10 @@ export const api = {
|
|
|
325
324
|
call<{ started: boolean }>('open.external', payload),
|
|
326
325
|
}
|
|
327
326
|
|
|
328
|
-
/** Absolute URL of the media route for one path (images only). */
|
|
329
|
-
export function mediaUrl(scope: SessionScope, path: string): string {
|
|
330
|
-
return fileUrl(scope, path, false)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
327
|
/** Absolute URL of the download route: serves raw bytes (binary-safe) with
|
|
334
328
|
* `Content-Disposition: attachment`, so the browser saves the file. */
|
|
335
329
|
export function downloadUrl(scope: SessionScope, path: string): string {
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Shared URL builder for the /sidebar/file route (media vs download). */
|
|
340
|
-
function fileUrl(scope: SessionScope, path: string, download: boolean): string {
|
|
341
|
-
const params = new URLSearchParams({ sessionId: scope.sessionId, path })
|
|
330
|
+
const params = new URLSearchParams({ sessionId: scope.sessionId, path, download: '1' })
|
|
342
331
|
if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
|
|
343
|
-
if (download) params.set('download', '1')
|
|
344
332
|
return `/sidebar/file?${params.toString()}`
|
|
345
333
|
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Absolute URL of the HTML preview route (see html-route.ts): the path is
|
|
349
|
-
* fully encoded so the previewed page's relative assets resolve back into
|
|
350
|
-
* the same route with the session scope intact. The UNC marker is
|
|
351
|
-
* platform-neutral — the host's requireAbsolute resolves the decoded
|
|
352
|
-
* forward-slash `//server/share/...` form on both win32 and POSIX — so no
|
|
353
|
-
* client-side platform signal is needed.
|
|
354
|
-
*/
|
|
355
|
-
export function htmlUrl(scope: SessionScope, path: string): string {
|
|
356
|
-
return encodeHtmlUrl(scope.sessionId, path)
|
|
357
|
-
}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in registration: the plugin registers its own tab pages
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Built-in registration: the plugin registers its own tab pages through the
|
|
3
|
+
* same {@link BetterSidebarService} external plugins use — eating its own
|
|
4
|
+
* dogfood. The descriptors live next to their feature modules (tabs.tsx);
|
|
5
|
+
* this module only aggregates them and owns the disposer lifecycle (cordis
|
|
6
|
+
* auto-invokes it on fiber disposal, HMR-safe).
|
|
7
|
+
*
|
|
8
|
+
* (v1.0.4: the built-in file viewers were retired with the file-viewer
|
|
9
|
+
* registry — file preview is the host's job now.)
|
|
8
10
|
*/
|
|
9
11
|
import type { Context } from '../../context-types.ts'
|
|
10
12
|
import type { BetterSidebarService } from '../service.ts'
|
|
11
13
|
import { builtinTabs, type BuiltinTabOptions } from './tabs.tsx'
|
|
12
|
-
import { builtinViewers } from './viewers.tsx'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Register all built-in tabs
|
|
16
|
-
*
|
|
17
|
-
* disposal). The `ctx` is threaded into tab descriptors that need it
|
|
18
|
-
* (EditorHost reads `ctx.betterSidebar` for file-viewer matching).
|
|
16
|
+
* Register all built-in tabs with the service. Returns a disposer that
|
|
17
|
+
* unregisters everything (cordis auto-invokes it on fiber disposal).
|
|
19
18
|
*/
|
|
20
19
|
export function registerBuiltins(
|
|
21
20
|
ctx: Context,
|
|
@@ -26,9 +25,6 @@ export function registerBuiltins(
|
|
|
26
25
|
for (const tab of builtinTabs(ctx, options)) {
|
|
27
26
|
disposers.push(service.registerTab(tab))
|
|
28
27
|
}
|
|
29
|
-
for (const viewer of builtinViewers()) {
|
|
30
|
-
disposers.push(service.registerFileViewer(viewer))
|
|
31
|
-
}
|
|
32
28
|
return () => {
|
|
33
29
|
for (const d of disposers) {
|
|
34
30
|
try { d() } catch { /* already disposed */ }
|