@keith9681/dsh-sidebar 0.15.2
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/LICENSE +21 -0
- package/README.md +0 -0
- package/README_EN.md +0 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +34651 -0
- package/lib/client-mermaid.js +200629 -0
- package/lib/client-registry.js +13388 -0
- package/lib/client-terminal.js +8904 -0
- package/lib/client.js +13388 -0
- package/lib/index.js +3959 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/client/BrowserView.d.ts +22 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +12 -0
- package/lib/types/client/FileTree.d.ts +36 -0
- package/lib/types/client/GitView.d.ts +8 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +43 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +23 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +268 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +12 -0
- package/lib/types/client/browser.d.ts +50 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +124 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editable-state.d.ts +27 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +57 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/intercept.d.ts +33 -0
- package/lib/types/client/lang.d.ts +12 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales.d.ts +366 -0
- package/lib/types/client/mermaid-blocks.d.ts +41 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +41 -0
- package/lib/types/client/paths.d.ts +35 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +23 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +440 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +30 -0
- package/lib/types/client/state.d.ts +359 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/tab-content-memo.d.ts +33 -0
- package/lib/types/client/terminal-font.d.ts +20 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +603 -0
- package/lib/types/fs-operations.d.ts +28 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +82 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/prefs-shared.d.ts +225 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +31 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +173 -0
- package/scripts/install.ps1 +289 -0
- package/scripts/install.sh +287 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/client/BrowserView.tsx +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +525 -0
- package/src/client/FileTree.tsx +653 -0
- package/src/client/GitView.tsx +558 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1152 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +644 -0
- package/src/client/Sidebar.tsx +1148 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +305 -0
- package/src/client/TerminalView.tsx +383 -0
- package/src/client/TextEditor.tsx +500 -0
- package/src/client/TreePanel.tsx +273 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +335 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +51 -0
- package/src/client/browser.ts +119 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +331 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +351 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editable-state.ts +70 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +161 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +369 -0
- package/src/client/intercept.tsx +110 -0
- package/src/client/lang.ts +103 -0
- package/src/client/layout.css +94 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales.ts +734 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +401 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +62 -0
- package/src/client/paths.ts +47 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +87 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +207 -0
- package/src/client/produced-files.ts +91 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +838 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2577 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +322 -0
- package/src/client/state.ts +1175 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +46 -0
- package/src/client/terminal-font.ts +28 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +155 -0
- package/src/context-types.ts +610 -0
- package/src/fs-operations.ts +99 -0
- package/src/fs-search.ts +86 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +244 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1132 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/prefs-shared.ts +268 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +479 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +100 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files window's tree surface: a global file-name search box on top
|
|
3
|
+
* (300ms debounce; an in-flight search is aborted by the next keystroke)
|
|
4
|
+
* over either the shared controlled FileTree (empty query) or the flat
|
|
5
|
+
* result list (relative paths; click opens through the caller's mode-aware
|
|
6
|
+
* open). Owns its refresh tick: the icon next to the search input clears
|
|
7
|
+
* the tree cache. EditorHost docks it as the tab's right panel (wrapped in
|
|
8
|
+
* a drag-resize handle) and provides the file context-menu open escapes.
|
|
9
|
+
*
|
|
10
|
+
* Uploads (header pickers, the tree's drag-drop and "upload here" menu)
|
|
11
|
+
* all funnel through here: one session at a time, shown in a full-window
|
|
12
|
+
* progress overlay with cancel, followed by a tree refresh and a one-line
|
|
13
|
+
* hint under the search row (success fades, failures and cancels stay).
|
|
14
|
+
* OS file drags are shielded at the panel host (see Sidebar.tsx), so a
|
|
15
|
+
* drop over the file window uploads here and never reaches DSH's chat
|
|
16
|
+
* intake.
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect, useRef, useState, type InputHTMLAttributes } from 'react'
|
|
19
|
+
import clsx from 'clsx'
|
|
20
|
+
import { IconFolderOpen16, IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
21
|
+
import { api } from './api.ts'
|
|
22
|
+
import { FileTree } from './FileTree.tsx'
|
|
23
|
+
import { IconUploadOutline16 } from './icons.tsx'
|
|
24
|
+
import type { OpenWithTarget } from './open-with.ts'
|
|
25
|
+
import { t } from './locales.ts'
|
|
26
|
+
import { resolveSidebarPath } from './produced-files.ts'
|
|
27
|
+
import { UploadOverlay } from './UploadOverlay.tsx'
|
|
28
|
+
import {
|
|
29
|
+
summarizeResults, uploadHintText, uploadItemsFromFiles, uploadToDir,
|
|
30
|
+
UPLOAD_HINT_MS, type UploadItem,
|
|
31
|
+
} from './upload.ts'
|
|
32
|
+
import css from './sidebar.module.css'
|
|
33
|
+
|
|
34
|
+
/** One in-flight upload session (the overlay's progress source). */
|
|
35
|
+
interface UploadSession {
|
|
36
|
+
dir: string
|
|
37
|
+
done: number
|
|
38
|
+
total: number
|
|
39
|
+
/** Relative path of the file being uploaded ('' when none is in flight). */
|
|
40
|
+
current: string
|
|
41
|
+
controller: AbortController
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function TreePanel(props: {
|
|
45
|
+
sessionId: string
|
|
46
|
+
cwd: string | undefined
|
|
47
|
+
expanded: string[]
|
|
48
|
+
onToggle: (path: string) => void
|
|
49
|
+
onOpenFile: (path: string) => void
|
|
50
|
+
/** File context-menu "open in a new tab" (passed through to FileTree). */
|
|
51
|
+
onOpenFileNewTab?: (path: string) => void
|
|
52
|
+
/** File context-menu "open to the side" (passed through to FileTree). */
|
|
53
|
+
onOpenFileSide?: (path: string) => void
|
|
54
|
+
/** The "open with" menu surface (passed through to FileTree; absent →
|
|
55
|
+
* the whole section is hidden). */
|
|
56
|
+
openWithTargets?: OpenWithTarget[]
|
|
57
|
+
openWithPinned?: string[]
|
|
58
|
+
openWithSsh?: boolean
|
|
59
|
+
onOpenWith?: (targetId: string, path: string) => void
|
|
60
|
+
onToggleOpenWithPin?: (targetId: string) => void
|
|
61
|
+
onReferenceFile: (path: string) => void
|
|
62
|
+
/** Full-window presentation: the panel fills its host instead of docking
|
|
63
|
+
* at a fixed width. */
|
|
64
|
+
full?: boolean
|
|
65
|
+
}) {
|
|
66
|
+
const { sessionId, cwd, expanded, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, full } = props
|
|
67
|
+
const [query, setQuery] = useState('')
|
|
68
|
+
const [results, setResults] = useState<{ matches: string[]; truncated: boolean } | null>(null)
|
|
69
|
+
const [error, setError] = useState<string | null>(null)
|
|
70
|
+
const [refreshTick, setRefreshTick] = useState(0)
|
|
71
|
+
/** One-line upload status under the search row ('' hides the hint). */
|
|
72
|
+
const [uploadStatus, setUploadStatus] = useState('')
|
|
73
|
+
/** Whether the status line is a failure/cancel (error color, stays visible). */
|
|
74
|
+
const [uploadFailed, setUploadFailed] = useState(false)
|
|
75
|
+
/** The in-flight upload session (null → no overlay, buttons enabled). */
|
|
76
|
+
const [upload, setUpload] = useState<UploadSession | null>(null)
|
|
77
|
+
/** True between the cancel click and the session settling (button disabled). */
|
|
78
|
+
const [cancelling, setCancelling] = useState(false)
|
|
79
|
+
/** Set by cancelUpload; the settle path shows 'upload cancelled' instead of
|
|
80
|
+
* summarizing the partial results. */
|
|
81
|
+
const cancelledRef = useRef(false)
|
|
82
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
83
|
+
const folderInputRef = useRef<HTMLInputElement>(null)
|
|
84
|
+
|
|
85
|
+
/** Start one upload session into `dir` (absolute, inside the workspace). */
|
|
86
|
+
const startUpload = (dir: string, items: UploadItem[]): void => {
|
|
87
|
+
if (items.length === 0 || cwd === undefined || upload !== null) return
|
|
88
|
+
cancelledRef.current = false
|
|
89
|
+
const controller = new AbortController()
|
|
90
|
+
setUploadFailed(false)
|
|
91
|
+
setUploadStatus(uploadHintText(0, items.length, '', dir, t))
|
|
92
|
+
setUpload({ dir, done: 0, total: items.length, current: '', controller })
|
|
93
|
+
void uploadToDir({ sessionId, cwd }, dir, items, (done, total, current) => {
|
|
94
|
+
if (current !== '') setUploadStatus(uploadHintText(done, total, current, dir, t))
|
|
95
|
+
setUpload(session => session === null ? session : { ...session, done, total, current })
|
|
96
|
+
}, controller.signal).then((results) => {
|
|
97
|
+
setUpload(null)
|
|
98
|
+
setCancelling(false)
|
|
99
|
+
// Reload the tree whatever the outcome: files may have landed before a
|
|
100
|
+
// cancel, and failures leave whatever did succeed visible.
|
|
101
|
+
setRefreshTick(tick => tick + 1)
|
|
102
|
+
if (cancelledRef.current) {
|
|
103
|
+
setUploadStatus(t('uploadCancelled'))
|
|
104
|
+
setUploadFailed(true)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const status = summarizeResults(results, t)
|
|
108
|
+
setUploadStatus(status)
|
|
109
|
+
setUploadFailed(results.some(result => !result.ok))
|
|
110
|
+
// Success messages are transient; failures stay until the next action.
|
|
111
|
+
if (results.every(result => result.ok)) {
|
|
112
|
+
window.setTimeout(() => {
|
|
113
|
+
setUploadStatus(current => current === status ? '' : current)
|
|
114
|
+
}, UPLOAD_HINT_MS)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Cancel the in-flight upload (aborts the request; the host drops its temp). */
|
|
120
|
+
const cancelUpload = (): void => {
|
|
121
|
+
if (upload === null || cancelling) return
|
|
122
|
+
cancelledRef.current = true
|
|
123
|
+
setCancelling(true)
|
|
124
|
+
upload.controller.abort()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const folderInputProps = { webkitdirectory: '' } as InputHTMLAttributes<HTMLInputElement>
|
|
128
|
+
|
|
129
|
+
const needle = query.trim()
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (needle === '') {
|
|
132
|
+
setResults(null)
|
|
133
|
+
setError(null)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
const controller = new AbortController()
|
|
137
|
+
const timer = window.setTimeout(() => {
|
|
138
|
+
api.fsSearch({ sessionId, cwd }, needle, controller.signal).then((found) => {
|
|
139
|
+
setResults(found)
|
|
140
|
+
setError(null)
|
|
141
|
+
}).catch((failure: unknown) => {
|
|
142
|
+
if (controller.signal.aborted) return
|
|
143
|
+
setResults(null)
|
|
144
|
+
setError(failure instanceof Error ? failure.message : String(failure))
|
|
145
|
+
})
|
|
146
|
+
}, 300)
|
|
147
|
+
return () => {
|
|
148
|
+
window.clearTimeout(timer)
|
|
149
|
+
controller.abort()
|
|
150
|
+
}
|
|
151
|
+
}, [sessionId, cwd, needle])
|
|
152
|
+
|
|
153
|
+
const busy = upload !== null
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className={clsx(css.editorTreePanel, full === true && css.editorTreePanelFull)}>
|
|
157
|
+
<div className={css.editorTreeSearch}>
|
|
158
|
+
<input
|
|
159
|
+
className={css.editorSearchInput}
|
|
160
|
+
value={query}
|
|
161
|
+
placeholder={t('editorSearchPlaceholder')}
|
|
162
|
+
spellCheck={false}
|
|
163
|
+
onChange={(event) => { setQuery(event.target.value) }}
|
|
164
|
+
/>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className={css.iconButton}
|
|
168
|
+
aria-label={t('refresh')}
|
|
169
|
+
title={t('refresh')}
|
|
170
|
+
onClick={() => { setRefreshTick(tick => tick + 1) }}
|
|
171
|
+
>
|
|
172
|
+
<IconRefreshOutline16 size={14} />
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
className={css.iconButton}
|
|
177
|
+
aria-label={t('uploadFiles')}
|
|
178
|
+
title={t('uploadFiles')}
|
|
179
|
+
disabled={busy}
|
|
180
|
+
onClick={() => { fileInputRef.current?.click() }}
|
|
181
|
+
>
|
|
182
|
+
<IconUploadOutline16 size={14} />
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className={css.iconButton}
|
|
187
|
+
aria-label={t('uploadFolder')}
|
|
188
|
+
title={t('uploadFolder')}
|
|
189
|
+
disabled={busy}
|
|
190
|
+
onClick={() => { folderInputRef.current?.click() }}
|
|
191
|
+
>
|
|
192
|
+
<IconFolderOpen16 size={14} />
|
|
193
|
+
</button>
|
|
194
|
+
<input
|
|
195
|
+
ref={fileInputRef}
|
|
196
|
+
type="file"
|
|
197
|
+
multiple
|
|
198
|
+
style={{ display: 'none' }}
|
|
199
|
+
onChange={(event) => {
|
|
200
|
+
if (cwd !== undefined) startUpload(cwd, uploadItemsFromFiles(event.target.files ?? []))
|
|
201
|
+
event.target.value = ''
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
<input
|
|
205
|
+
ref={folderInputRef}
|
|
206
|
+
type="file"
|
|
207
|
+
multiple
|
|
208
|
+
{...folderInputProps}
|
|
209
|
+
style={{ display: 'none' }}
|
|
210
|
+
onChange={(event) => {
|
|
211
|
+
if (cwd !== undefined) startUpload(cwd, uploadItemsFromFiles(event.target.files ?? []))
|
|
212
|
+
event.target.value = ''
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
{uploadStatus !== '' && (
|
|
217
|
+
<div className={clsx(css.editorSearchHint, uploadFailed && css.editorError)} title={uploadStatus}>{uploadStatus}</div>
|
|
218
|
+
)}
|
|
219
|
+
{needle === '' ? (
|
|
220
|
+
<FileTree
|
|
221
|
+
sessionId={sessionId}
|
|
222
|
+
cwd={cwd}
|
|
223
|
+
expanded={expanded}
|
|
224
|
+
onToggle={onToggle}
|
|
225
|
+
onOpenFile={onOpenFile}
|
|
226
|
+
onOpenFileNewTab={onOpenFileNewTab}
|
|
227
|
+
onOpenFileSide={onOpenFileSide}
|
|
228
|
+
openWithTargets={openWithTargets}
|
|
229
|
+
openWithPinned={openWithPinned}
|
|
230
|
+
openWithSsh={openWithSsh}
|
|
231
|
+
onOpenWith={onOpenWith}
|
|
232
|
+
onToggleOpenWithPin={onToggleOpenWithPin}
|
|
233
|
+
onReferenceFile={onReferenceFile}
|
|
234
|
+
refreshTick={refreshTick}
|
|
235
|
+
onUploadRequest={startUpload}
|
|
236
|
+
busy={busy}
|
|
237
|
+
/>
|
|
238
|
+
) : (
|
|
239
|
+
<div className={css.explorerBody}>
|
|
240
|
+
{error !== null && <div className={clsx(css.editorSearchHint, css.editorError)}>{error}</div>}
|
|
241
|
+
{error === null && results === null && <div className={css.editorSearchHint}>{t('loading')}</div>}
|
|
242
|
+
{error === null && results !== null && results.matches.length === 0 && (
|
|
243
|
+
<div className={css.editorSearchHint}>{t('editorSearchNoResults')}</div>
|
|
244
|
+
)}
|
|
245
|
+
{error === null && results !== null && results.matches.map(rel => (
|
|
246
|
+
<button
|
|
247
|
+
key={rel}
|
|
248
|
+
type="button"
|
|
249
|
+
className={css.editorSearchResult}
|
|
250
|
+
title={rel}
|
|
251
|
+
onClick={() => { onOpenFile(resolveSidebarPath(cwd, rel)) }}
|
|
252
|
+
>
|
|
253
|
+
{rel}
|
|
254
|
+
</button>
|
|
255
|
+
))}
|
|
256
|
+
{error === null && results?.truncated === true && (
|
|
257
|
+
<div className={css.editorSearchHint}>{t('editorSearchTruncated')}</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
{upload !== null && (
|
|
262
|
+
<UploadOverlay
|
|
263
|
+
dir={upload.dir}
|
|
264
|
+
done={upload.done}
|
|
265
|
+
total={upload.total}
|
|
266
|
+
current={upload.current}
|
|
267
|
+
onCancel={cancelUpload}
|
|
268
|
+
cancelling={cancelling}
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-window upload progress over the files tree: a blurred scrim (same mask
|
|
3
|
+
* token as the repo's Modal primitive) with a card showing the target
|
|
4
|
+
* directory, file-level progress, and a cancel button. Esc cancels too —
|
|
5
|
+
* clicking the scrim does not, so a stray click can never abort an upload.
|
|
6
|
+
* Rendered inside TreePanel (absolute inset-0), so it covers only the file
|
|
7
|
+
* window and never the conversation column.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, type ReactNode } from 'react'
|
|
10
|
+
import { IconUploadOutline16 } from './icons.tsx'
|
|
11
|
+
import { t } from './locales.ts'
|
|
12
|
+
import { uploadHintText } from './upload.ts'
|
|
13
|
+
import css from './sidebar.module.css'
|
|
14
|
+
|
|
15
|
+
export function UploadOverlay(props: {
|
|
16
|
+
/** Absolute upload directory (the session workspace or a tree directory). */
|
|
17
|
+
dir: string
|
|
18
|
+
done: number
|
|
19
|
+
total: number
|
|
20
|
+
/** Relative path of the file being uploaded ('' when none is in flight). */
|
|
21
|
+
current: string
|
|
22
|
+
onCancel: () => void
|
|
23
|
+
/** True while cancellation is in flight (disables the cancel button). */
|
|
24
|
+
cancelling?: boolean
|
|
25
|
+
}): ReactNode {
|
|
26
|
+
const { dir, done, total, current, onCancel, cancelling } = props
|
|
27
|
+
// Esc cancels (MermaidModal-style native listener; the overlay is not a
|
|
28
|
+
// focus-trapped dialog, so a window listener is the honest scope).
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
31
|
+
if (event.key === 'Escape') onCancel()
|
|
32
|
+
}
|
|
33
|
+
window.addEventListener('keydown', onKey)
|
|
34
|
+
return () => { window.removeEventListener('keydown', onKey) }
|
|
35
|
+
}, [onCancel])
|
|
36
|
+
|
|
37
|
+
const percent = total === 0 ? 0 : Math.min(100, Math.round((done / total) * 100))
|
|
38
|
+
return (
|
|
39
|
+
<div className={css.uploadOverlay} role="dialog" aria-modal="true" aria-label={t('uploadingTo', { dir })}>
|
|
40
|
+
<div className={css.uploadOverlayCard}>
|
|
41
|
+
<div className={css.uploadOverlayTitle} title={dir}>
|
|
42
|
+
<IconUploadOutline16 size={16} />
|
|
43
|
+
<span>{t('uploadingTo', { dir })}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
className={css.uploadOverlayProgress}
|
|
47
|
+
role="progressbar"
|
|
48
|
+
aria-valuemin={0}
|
|
49
|
+
aria-valuemax={total}
|
|
50
|
+
aria-valuenow={done}
|
|
51
|
+
aria-valuetext={t('uploadProgress', { done, total, name: current })}
|
|
52
|
+
>
|
|
53
|
+
<div className={css.uploadOverlayProgressFill} style={{ width: `${percent}%` }} />
|
|
54
|
+
</div>
|
|
55
|
+
<div className={css.uploadOverlayStatus}>{uploadHintText(done, total, current, dir, t)}</div>
|
|
56
|
+
<button type="button" className={css.uploadOverlayCancel} disabled={cancelling} onClick={onCancel}>
|
|
57
|
+
{t('cancel')}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "add plugin" modals (Side card settings → the dashed cards at the
|
|
3
|
+
* end of the 侧边栏内容 / 文件预览 grids): declare that the sidebar's
|
|
4
|
+
* extension points — tab pages and file previewers — are open to plugins
|
|
5
|
+
* (registered through `ctx.betterSidebar`), point at the GitHub topic page
|
|
6
|
+
* for discovery, and show the repo's recommended plugin catalog of the
|
|
7
|
+
* matching kind (name / url / description / install script).
|
|
8
|
+
*
|
|
9
|
+
* Per entry there are two actions:
|
|
10
|
+
* - 「跳转」opens the plugin's repo in a REAL new browser tab (window.open
|
|
11
|
+
* — a button, so the sidebar link takeover cannot reroute it);
|
|
12
|
+
* - 「安装」only COPIES the install script to the clipboard (writeClipboard)
|
|
13
|
+
* with a transient "已复制" feedback on the button — the user pastes and
|
|
14
|
+
* runs it wherever they manage their DSH profile. No terminal is opened,
|
|
15
|
+
* nothing is closed, nothing can fail outward.
|
|
16
|
+
*
|
|
17
|
+
* The body is extracted as {@link PluginListBody} so tests render it
|
|
18
|
+
* directly — the Modal primitive runs hooks unconditionally, so an open
|
|
19
|
+
* Modal must never be renderToString'd (same rule as the settingsFor popup
|
|
20
|
+
* in SideCardSection); the modal itself mounts only while open.
|
|
21
|
+
*/
|
|
22
|
+
import { useState, type ReactNode } from 'react'
|
|
23
|
+
import { Modal, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
24
|
+
import type { BetterSidebarService } from './service.ts'
|
|
25
|
+
import { PLUGIN_TOPIC_URL, type PluginEntry } from './plugins-shared.ts'
|
|
26
|
+
import { builtinTabPlugins } from './plugins-tabs.ts'
|
|
27
|
+
import { builtinViewerPlugins } from './plugins-viewers.ts'
|
|
28
|
+
import { t } from './locales.ts'
|
|
29
|
+
import css from './SideCardSection.module.css'
|
|
30
|
+
|
|
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
|
+
/** How long the "已复制" feedback stays on the copy button. */
|
|
41
|
+
const COPIED_FEEDBACK_MS = 1500
|
|
42
|
+
|
|
43
|
+
/** The modal body: the GitHub topic button + the recommended plugin list
|
|
44
|
+
* with per-entry jump/copy buttons (extracted for direct testing). */
|
|
45
|
+
export function PluginListBody(props: { service: BetterSidebarService; kind: PluginKind }) {
|
|
46
|
+
const { service, kind } = props
|
|
47
|
+
// Which entry's copy button currently shows the "已复制" feedback.
|
|
48
|
+
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
49
|
+
// Live catalog filter (name / id / description). A free-text search keeps
|
|
50
|
+
// the list usable once the catalogs grow beyond a handful of entries:
|
|
51
|
+
// every keystroke narrows the rendered rows, so a long catalog never
|
|
52
|
+
// becomes a wall of scrolling.
|
|
53
|
+
const [query, setQuery] = useState('')
|
|
54
|
+
|
|
55
|
+
const catalog = catalogOf(kind)
|
|
56
|
+
const needle = query.trim().toLowerCase()
|
|
57
|
+
const matches = (entry: PluginEntry): boolean => {
|
|
58
|
+
if (needle === '') return true
|
|
59
|
+
const description = typeof entry.description === 'function' ? entry.description() : entry.description
|
|
60
|
+
return entry.name.toLowerCase().includes(needle)
|
|
61
|
+
|| entry.id.toLowerCase().includes(needle)
|
|
62
|
+
|| description.toLowerCase().includes(needle)
|
|
63
|
+
}
|
|
64
|
+
const filtered = catalog.filter(matches)
|
|
65
|
+
// Group by the optional category (stable order: category order of first
|
|
66
|
+
// appearance, uncategorized entries last under the plain list).
|
|
67
|
+
const groups = new Map<string | undefined, PluginEntry[]>()
|
|
68
|
+
for (const entry of filtered) {
|
|
69
|
+
const key = entry.category === undefined ? undefined : (typeof entry.category === 'function' ? entry.category() : entry.category)
|
|
70
|
+
const list = groups.get(key)
|
|
71
|
+
if (list === undefined) groups.set(key, [entry])
|
|
72
|
+
else list.push(entry)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Copy the entry's install script to the clipboard and flash the button's
|
|
76
|
+
* "已复制" label for a moment. The feedback ONLY appears after a
|
|
77
|
+
* successful write — when the clipboard is unavailable or denied
|
|
78
|
+
* (writeClipboard resolves false) nothing is shown, so the user is never
|
|
79
|
+
* told to paste a command that was not placed on the clipboard. Never
|
|
80
|
+
* closes anything, never throws outward. */
|
|
81
|
+
const copy = async (entry: PluginEntry): Promise<void> => {
|
|
82
|
+
const written = await writeClipboard(entry.install)
|
|
83
|
+
if (!written) return
|
|
84
|
+
setCopiedId(entry.id)
|
|
85
|
+
window.setTimeout(() => {
|
|
86
|
+
setCopiedId(current => (current === entry.id ? null : current))
|
|
87
|
+
}, COPIED_FEEDBACK_MS)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Open the plugin's repo in a REAL new browser tab (window.open — a
|
|
91
|
+
* button, so the sidebar link takeover cannot reroute it). */
|
|
92
|
+
const jump = (entry: PluginEntry): void => {
|
|
93
|
+
window.open(entry.url, '_blank', 'noopener')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One catalog row (extracted so the group render stays flat). */
|
|
97
|
+
const renderEntry = (entry: PluginEntry): ReactNode => (
|
|
98
|
+
<div key={entry.id} className={css.pluginEntry}>
|
|
99
|
+
<div className={css.pluginEntryHead}>
|
|
100
|
+
{/* The name is a BUTTON on the same window.open path as the
|
|
101
|
+
jump button: as an anchor it would be caught by the
|
|
102
|
+
document-capture link takeover (which ignores
|
|
103
|
+
target=_blank) and land in the sidebar browser. */}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className={css.pluginName}
|
|
107
|
+
aria-label={`${t('openPlugin')}: ${entry.name}`}
|
|
108
|
+
onClick={() => { jump(entry) }}
|
|
109
|
+
>
|
|
110
|
+
{entry.name}
|
|
111
|
+
</button>
|
|
112
|
+
<span className={css.pluginEntryActions}>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={css.pluginJumpBtn}
|
|
116
|
+
aria-label={`${t('openPlugin')}: ${entry.name}`}
|
|
117
|
+
onClick={() => { jump(entry) }}
|
|
118
|
+
>
|
|
119
|
+
{t('openPlugin')}
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={css.pluginCopyBtn}
|
|
124
|
+
aria-label={`${t('copyInstall')}: ${entry.name}`}
|
|
125
|
+
onClick={() => { copy(entry) }}
|
|
126
|
+
>
|
|
127
|
+
{copiedId === entry.id ? t('copied') : t('copy')}
|
|
128
|
+
</button>
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div className={css.pluginDesc}>
|
|
132
|
+
{typeof entry.description === 'function' ? entry.description() : entry.description}
|
|
133
|
+
</div>
|
|
134
|
+
<code className={css.pluginInstall}>{entry.install}</code>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className={css.pluginList}>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className={css.pluginTopicBtn}
|
|
143
|
+
onClick={() => { window.open(PLUGIN_TOPIC_URL, '_blank', 'noopener') }}
|
|
144
|
+
>
|
|
145
|
+
{t('addPluginsBrowseMore')}
|
|
146
|
+
</button>
|
|
147
|
+
<input
|
|
148
|
+
type="search"
|
|
149
|
+
className={css.pluginSearch}
|
|
150
|
+
placeholder={t('addPluginsSearch')}
|
|
151
|
+
aria-label={t('addPluginsSearch')}
|
|
152
|
+
value={query}
|
|
153
|
+
onChange={event => { setQuery(event.currentTarget.value) }}
|
|
154
|
+
/>
|
|
155
|
+
<div className={css.groupHeading}>
|
|
156
|
+
<span>{t('addPluginsRecommended')}</span>
|
|
157
|
+
<span className={css.count}>{filtered.length}</span>
|
|
158
|
+
</div>
|
|
159
|
+
{catalog.length === 0 ? (
|
|
160
|
+
<div className={css.pluginEmpty}>{t('addPluginsEmpty')}</div>
|
|
161
|
+
) : filtered.length === 0 ? (
|
|
162
|
+
<div className={css.pluginEmpty}>{t('addPluginsNoMatch')}</div>
|
|
163
|
+
) : (
|
|
164
|
+
<div className={css.pluginEntries}>
|
|
165
|
+
{[...groups.entries()].map(([category, entries]) => (
|
|
166
|
+
<div key={category ?? '\u0000'} className={css.pluginGroup}>
|
|
167
|
+
{category !== undefined && (
|
|
168
|
+
<div className={css.pluginGroupHeading}>{category}</div>
|
|
169
|
+
)}
|
|
170
|
+
{entries.map(renderEntry)}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The modal itself (mounted only while open — see the module comment). */
|
|
180
|
+
export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void; kind: PluginKind }) {
|
|
181
|
+
const { service, onClose, kind } = props
|
|
182
|
+
return (
|
|
183
|
+
<Modal
|
|
184
|
+
open
|
|
185
|
+
onClose={onClose}
|
|
186
|
+
title={kind === 'tab' ? t('addPluginsTabCard') : t('addPluginsViewerCard')}
|
|
187
|
+
description={kind === 'tab' ? t('addPluginsTabDesc') : t('addPluginsViewerDesc')}
|
|
188
|
+
closeLabel={t('close')}
|
|
189
|
+
className={css.pluginModal}
|
|
190
|
+
footer={(
|
|
191
|
+
<button type="button" className={css.done} onClick={onClose}>
|
|
192
|
+
{t('settingsDone')}
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
<PluginListBody service={service} kind={kind} />
|
|
197
|
+
</Modal>
|
|
198
|
+
)
|
|
199
|
+
}
|