@chatcode/cco-better-sidebar 0.18.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/README_EN.md +563 -0
- package/assets/logo.svg +1 -0
- package/cordis.patch.yml +79 -0
- package/lib/client-editor.js +55744 -0
- package/lib/client-mermaid.js +208256 -0
- package/lib/client-registry.js +30378 -0
- package/lib/client-terminal.js +16894 -0
- package/lib/client.js +30378 -0
- package/lib/index.js +6897 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-opens.d.ts +65 -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/chatcode-update.d.ts +47 -0
- package/lib/types/client/AccountMenu.d.ts +1 -0
- package/lib/types/client/AiRatioSection.d.ts +3 -0
- package/lib/types/client/BrowserView.d.ts +39 -0
- package/lib/types/client/ChatcodeGate.d.ts +6 -0
- package/lib/types/client/CodingPlanModelsSettings.d.ts +9 -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 +13 -0
- package/lib/types/client/FenceErrorNotice.d.ts +5 -0
- package/lib/types/client/FileTree.d.ts +41 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +12 -0
- package/lib/types/client/MarkdownHtml.d.ts +32 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/ReleaseNotesDialog.d.ts +11 -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/Sidebar.d.ts +8 -0
- package/lib/types/client/TabBar.d.ts +54 -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 +27 -0
- package/lib/types/client/UpdateDialog.d.ts +10 -0
- package/lib/types/client/UpdateSettingsPanel.d.ts +1 -0
- package/lib/types/client/UpdateSettingsSection.d.ts +10 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/UsageHeatmapSection.d.ts +4 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +345 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +17 -0
- package/lib/types/client/browser.d.ts +60 -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/chat-presentation.d.ts +13 -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/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +66 -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 +44 -0
- package/lib/types/client/lang.d.ts +14 -0
- package/lib/types/client/layout-push.d.ts +15 -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-ar.d.ts +18 -0
- package/lib/types/client/locales-de.d.ts +4 -0
- package/lib/types/client/locales-fr.d.ts +10 -0
- package/lib/types/client/locales-hi.d.ts +17 -0
- package/lib/types/client/locales-id.d.ts +15 -0
- package/lib/types/client/locales-it.d.ts +8 -0
- package/lib/types/client/locales-ja.d.ts +17 -0
- package/lib/types/client/locales-ko.d.ts +9 -0
- package/lib/types/client/locales-nl.d.ts +16 -0
- package/lib/types/client/locales-pl.d.ts +19 -0
- package/lib/types/client/locales-pt.d.ts +1 -0
- package/lib/types/client/locales-ru.d.ts +13 -0
- package/lib/types/client/locales-sv.d.ts +1 -0
- package/lib/types/client/locales-th.d.ts +17 -0
- package/lib/types/client/locales-tr.d.ts +17 -0
- package/lib/types/client/locales-vi.d.ts +17 -0
- package/lib/types/client/locales-zh-HK.d.ts +32 -0
- package/lib/types/client/locales-zh-MO.d.ts +32 -0
- package/lib/types/client/locales-zh-TW.d.ts +32 -0
- package/lib/types/client/locales.d.ts +571 -0
- package/lib/types/client/markdown-html.d.ts +96 -0
- package/lib/types/client/markdown-images.d.ts +45 -0
- package/lib/types/client/markdown-labels.d.ts +21 -0
- package/lib/types/client/md-toc.d.ts +4 -0
- package/lib/types/client/mermaid-blocks.d.ts +47 -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 +52 -0
- package/lib/types/client/paths.d.ts +43 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/pinned.d.ts +86 -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 +29 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +445 -0
- package/lib/types/client/session-pins.d.ts +14 -0
- package/lib/types/client/settings-nav-icon.d.ts +27 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/split-pane.d.ts +38 -0
- package/lib/types/client/state.d.ts +480 -0
- package/lib/types/client/tab-content-memo.d.ts +40 -0
- package/lib/types/client/terminal-font.d.ts +78 -0
- package/lib/types/client/terminal-links.d.ts +119 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/ui-tweaks/appicons.d.ts +16 -0
- package/lib/types/client/ui-tweaks/archive.d.ts +42 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +45 -0
- package/lib/types/client/ui-tweaks/cachehit.d.ts +41 -0
- package/lib/types/client/ui-tweaks/gitbar.d.ts +103 -0
- package/lib/types/client/ui-tweaks/index.d.ts +279 -0
- package/lib/types/client/ui-tweaks/mcp.d.ts +28 -0
- package/lib/types/client/ui-tweaks/notifier.d.ts +125 -0
- package/lib/types/client/ui-tweaks/whale.d.ts +55 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/useChatcodeAccount.d.ts +10 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/codingplan.d.ts +37 -0
- package/lib/types/config.d.ts +95 -0
- package/lib/types/context-types.d.ts +540 -0
- package/lib/types/fs-operations.d.ts +30 -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 +124 -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/login-route.d.ts +2 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +26 -0
- package/lib/types/prefs-shared.d.ts +257 -0
- package/lib/types/pty-deps.d.ts +79 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/ui-tweaks/archive.d.ts +71 -0
- package/lib/types/ui-tweaks/config.d.ts +186 -0
- package/lib/types/ui-tweaks/git-web.d.ts +43 -0
- package/lib/types/ui-tweaks/git.d.ts +311 -0
- package/lib/types/ui-tweaks/index.d.ts +17 -0
- package/lib/types/ui-tweaks/mcp.d.ts +103 -0
- package/lib/types/ui-tweaks/turn-outcome.d.ts +57 -0
- package/lib/types/ui-tweaks/web.d.ts +55 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +213 -0
- package/scripts/install.ps1 +310 -0
- package/scripts/install.sh +287 -0
- package/src/agent-opens.ts +265 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/chatcode-update.ts +193 -0
- package/src/client/AccountMenu.tsx +177 -0
- package/src/client/AiRatioSection.module.css +329 -0
- package/src/client/AiRatioSection.tsx +292 -0
- package/src/client/BrowserView.tsx +289 -0
- package/src/client/ChatcodeGate.tsx +35 -0
- package/src/client/CodingPlanModelsSettings.module.css +118 -0
- package/src/client/CodingPlanModelsSettings.tsx +101 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +563 -0
- package/src/client/FenceErrorNotice.tsx +40 -0
- package/src/client/FileTree.tsx +701 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +722 -0
- package/src/client/MarkdownHtml.tsx +296 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/ReleaseNotesDialog.tsx +91 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +932 -0
- package/src/client/SideCardSection.tsx +1169 -0
- package/src/client/Sidebar.tsx +1857 -0
- package/src/client/TabBar.tsx +365 -0
- package/src/client/TerminalView.tsx +424 -0
- package/src/client/TextEditor.tsx +468 -0
- package/src/client/TreePanel.tsx +279 -0
- package/src/client/UpdateDialog.tsx +186 -0
- package/src/client/UpdateSettingsPanel.tsx +29 -0
- package/src/client/UpdateSettingsSection.tsx +22 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/UsageHeatmapSection.module.css +440 -0
- package/src/client/UsageHeatmapSection.tsx +301 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +433 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +61 -0
- package/src/client/browser.ts +167 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +275 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chat-presentation.ts +95 -0
- package/src/client/chunk-loader.ts +350 -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/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +170 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +501 -0
- package/src/client/intercept.tsx +172 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +37 -0
- package/src/client/layout.css +156 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +387 -0
- package/src/client/locales-de.ts +372 -0
- package/src/client/locales-fr.ts +379 -0
- package/src/client/locales-hi.ts +386 -0
- package/src/client/locales-id.ts +384 -0
- package/src/client/locales-it.ts +377 -0
- package/src/client/locales-ja.ts +486 -0
- package/src/client/locales-ko.ts +378 -0
- package/src/client/locales-nl.ts +384 -0
- package/src/client/locales-pl.ts +388 -0
- package/src/client/locales-pt.ts +369 -0
- package/src/client/locales-ru.ts +384 -0
- package/src/client/locales-sv.ts +369 -0
- package/src/client/locales-th.ts +386 -0
- package/src/client/locales-tr.ts +386 -0
- package/src/client/locales-vi.ts +386 -0
- package/src/client/locales-zh-HK.ts +401 -0
- package/src/client/locales-zh-MO.ts +401 -0
- package/src/client/locales-zh-TW.ts +401 -0
- package/src/client/locales.ts +1229 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +31 -0
- package/src/client/md-toc.tsx +127 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +406 -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 +79 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +184 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +153 -0
- package/src/client/plugins-viewers.ts +44 -0
- package/src/client/prefs.ts +216 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +873 -0
- package/src/client/session-pins.tsx +334 -0
- package/src/client/settings-nav-icon.ts +55 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +3607 -0
- package/src/client/split-pane.tsx +333 -0
- package/src/client/state.ts +1646 -0
- package/src/client/tab-content-memo.ts +55 -0
- package/src/client/terminal-font.ts +224 -0
- package/src/client/terminal-links.ts +194 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/ui-tweaks/appicons.ts +24 -0
- package/src/client/ui-tweaks/archive.tsx +281 -0
- package/src/client/ui-tweaks/branding.tsx +232 -0
- package/src/client/ui-tweaks/cachehit.tsx +122 -0
- package/src/client/ui-tweaks/gitbar.tsx +2559 -0
- package/src/client/ui-tweaks/index.tsx +1696 -0
- package/src/client/ui-tweaks/mcp.tsx +582 -0
- package/src/client/ui-tweaks/notifier.ts +514 -0
- package/src/client/ui-tweaks/whale.tsx +171 -0
- package/src/client/upload.ts +187 -0
- package/src/client/useChatcodeAccount.ts +103 -0
- package/src/client/wco.ts +120 -0
- package/src/codingplan.ts +146 -0
- package/src/config.ts +207 -0
- package/src/context-types.ts +559 -0
- package/src/fs-operations.ts +97 -0
- package/src/fs-search.ts +113 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +446 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1524 -0
- package/src/invariant.ts +32 -0
- package/src/login-route.ts +26 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +81 -0
- package/src/prefs-shared.ts +303 -0
- package/src/pty-deps.ts +241 -0
- package/src/pty-manager.ts +376 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- package/src/ui-tweaks/archive.ts +293 -0
- package/src/ui-tweaks/config.ts +257 -0
- package/src/ui-tweaks/git-web.ts +421 -0
- package/src/ui-tweaks/git.ts +1524 -0
- package/src/ui-tweaks/index.ts +61 -0
- package/src/ui-tweaks/mcp.ts +535 -0
- package/src/ui-tweaks/turn-outcome.ts +105 -0
- package/src/ui-tweaks/web.ts +225 -0
- package/src/wire.ts +110 -0
- package/vendor/addon-fit.js +2 -0
- package/vendor/xterm.css +218 -0
- package/vendor/xterm.js +5 -0
|
@@ -0,0 +1,279 @@
|
|
|
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 type { SidebarStore } from './state.ts'
|
|
23
|
+
import { FileTree } from './FileTree.tsx'
|
|
24
|
+
import { IconUploadOutline16 } from './icons.tsx'
|
|
25
|
+
import type { OpenWithTarget } from './open-with.ts'
|
|
26
|
+
import { t } from './locales.ts'
|
|
27
|
+
import { resolveSidebarPath } from './produced-files.ts'
|
|
28
|
+
import { UploadOverlay } from './UploadOverlay.tsx'
|
|
29
|
+
import {
|
|
30
|
+
summarizeResults, uploadHintText, uploadItemsFromFiles, uploadToDir,
|
|
31
|
+
UPLOAD_HINT_MS, type UploadItem,
|
|
32
|
+
} from './upload.ts'
|
|
33
|
+
import css from './sidebar.module.css'
|
|
34
|
+
|
|
35
|
+
/** One in-flight upload session (the overlay's progress source). */
|
|
36
|
+
interface UploadSession {
|
|
37
|
+
dir: string
|
|
38
|
+
done: number
|
|
39
|
+
total: number
|
|
40
|
+
/** Relative path of the file being uploaded ('' when none is in flight). */
|
|
41
|
+
current: string
|
|
42
|
+
controller: AbortController
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function TreePanel(props: {
|
|
46
|
+
sessionId: string
|
|
47
|
+
cwd: string | undefined
|
|
48
|
+
/** The sidebar store (passed through to the tree's fence-refusal notice). */
|
|
49
|
+
store: SidebarStore
|
|
50
|
+
expanded: string[]
|
|
51
|
+
revealed: string[]
|
|
52
|
+
onToggle: (path: string) => void
|
|
53
|
+
onOpenFile: (path: string) => void
|
|
54
|
+
/** File context-menu "open in a new tab" (passed through to FileTree). */
|
|
55
|
+
onOpenFileNewTab?: (path: string) => void
|
|
56
|
+
/** File context-menu "open to the side" (passed through to FileTree). */
|
|
57
|
+
onOpenFileSide?: (path: string) => void
|
|
58
|
+
/** The "open with" menu surface (passed through to FileTree; absent →
|
|
59
|
+
* the whole section is hidden). */
|
|
60
|
+
openWithTargets?: OpenWithTarget[]
|
|
61
|
+
openWithPinned?: string[]
|
|
62
|
+
openWithSsh?: boolean
|
|
63
|
+
onOpenWith?: (targetId: string, path: string) => void
|
|
64
|
+
onToggleOpenWithPin?: (targetId: string) => void
|
|
65
|
+
onReferenceFile: (path: string) => void
|
|
66
|
+
/** Full-window presentation: the panel fills its host instead of docking
|
|
67
|
+
* at a fixed width. */
|
|
68
|
+
full?: boolean
|
|
69
|
+
}) {
|
|
70
|
+
const { sessionId, cwd, store, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, full } = props
|
|
71
|
+
const [query, setQuery] = useState('')
|
|
72
|
+
const [results, setResults] = useState<{ matches: string[]; truncated: boolean } | null>(null)
|
|
73
|
+
const [error, setError] = useState<string | null>(null)
|
|
74
|
+
const [refreshTick, setRefreshTick] = useState(0)
|
|
75
|
+
/** One-line upload status under the search row ('' hides the hint). */
|
|
76
|
+
const [uploadStatus, setUploadStatus] = useState('')
|
|
77
|
+
/** Whether the status line is a failure/cancel (error color, stays visible). */
|
|
78
|
+
const [uploadFailed, setUploadFailed] = useState(false)
|
|
79
|
+
/** The in-flight upload session (null → no overlay, buttons enabled). */
|
|
80
|
+
const [upload, setUpload] = useState<UploadSession | null>(null)
|
|
81
|
+
/** True between the cancel click and the session settling (button disabled). */
|
|
82
|
+
const [cancelling, setCancelling] = useState(false)
|
|
83
|
+
/** Set by cancelUpload; the settle path shows 'upload cancelled' instead of
|
|
84
|
+
* summarizing the partial results. */
|
|
85
|
+
const cancelledRef = useRef(false)
|
|
86
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
87
|
+
const folderInputRef = useRef<HTMLInputElement>(null)
|
|
88
|
+
|
|
89
|
+
/** Start one upload session into `dir` (absolute, inside the workspace). */
|
|
90
|
+
const startUpload = (dir: string, items: UploadItem[]): void => {
|
|
91
|
+
if (items.length === 0 || cwd === undefined || upload !== null) return
|
|
92
|
+
cancelledRef.current = false
|
|
93
|
+
const controller = new AbortController()
|
|
94
|
+
setUploadFailed(false)
|
|
95
|
+
setUploadStatus(uploadHintText(0, items.length, '', dir, t))
|
|
96
|
+
setUpload({ dir, done: 0, total: items.length, current: '', controller })
|
|
97
|
+
void uploadToDir({ sessionId, cwd }, dir, items, (done, total, current) => {
|
|
98
|
+
if (current !== '') setUploadStatus(uploadHintText(done, total, current, dir, t))
|
|
99
|
+
setUpload(session => session === null ? session : { ...session, done, total, current })
|
|
100
|
+
}, controller.signal).then((results) => {
|
|
101
|
+
setUpload(null)
|
|
102
|
+
setCancelling(false)
|
|
103
|
+
// Reload the tree whatever the outcome: files may have landed before a
|
|
104
|
+
// cancel, and failures leave whatever did succeed visible.
|
|
105
|
+
setRefreshTick(tick => tick + 1)
|
|
106
|
+
if (cancelledRef.current) {
|
|
107
|
+
setUploadStatus(t('uploadCancelled'))
|
|
108
|
+
setUploadFailed(true)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const status = summarizeResults(results, t)
|
|
112
|
+
setUploadStatus(status)
|
|
113
|
+
setUploadFailed(results.some(result => !result.ok))
|
|
114
|
+
// Success messages are transient; failures stay until the next action.
|
|
115
|
+
if (results.every(result => result.ok)) {
|
|
116
|
+
window.setTimeout(() => {
|
|
117
|
+
setUploadStatus(current => current === status ? '' : current)
|
|
118
|
+
}, UPLOAD_HINT_MS)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Cancel the in-flight upload (aborts the request; the host drops its temp). */
|
|
124
|
+
const cancelUpload = (): void => {
|
|
125
|
+
if (upload === null || cancelling) return
|
|
126
|
+
cancelledRef.current = true
|
|
127
|
+
setCancelling(true)
|
|
128
|
+
upload.controller.abort()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const folderInputProps = { webkitdirectory: '' } as InputHTMLAttributes<HTMLInputElement>
|
|
132
|
+
|
|
133
|
+
const needle = query.trim()
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (needle === '') {
|
|
136
|
+
setResults(null)
|
|
137
|
+
setError(null)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const controller = new AbortController()
|
|
141
|
+
const timer = window.setTimeout(() => {
|
|
142
|
+
api.fsSearch({ sessionId, cwd }, needle, controller.signal).then((found) => {
|
|
143
|
+
setResults(found)
|
|
144
|
+
setError(null)
|
|
145
|
+
}).catch((failure: unknown) => {
|
|
146
|
+
if (controller.signal.aborted) return
|
|
147
|
+
setResults(null)
|
|
148
|
+
setError(failure instanceof Error ? failure.message : String(failure))
|
|
149
|
+
})
|
|
150
|
+
}, 300)
|
|
151
|
+
return () => {
|
|
152
|
+
window.clearTimeout(timer)
|
|
153
|
+
controller.abort()
|
|
154
|
+
}
|
|
155
|
+
}, [sessionId, cwd, needle])
|
|
156
|
+
|
|
157
|
+
const busy = upload !== null
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className={clsx(css.editorTreePanel, full === true && css.editorTreePanelFull)}>
|
|
161
|
+
<div className={css.editorTreeSearch}>
|
|
162
|
+
<input
|
|
163
|
+
className={css.editorSearchInput}
|
|
164
|
+
value={query}
|
|
165
|
+
placeholder={t('editorSearchPlaceholder')}
|
|
166
|
+
spellCheck={false}
|
|
167
|
+
onChange={(event) => { setQuery(event.target.value) }}
|
|
168
|
+
/>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className={css.iconButton}
|
|
172
|
+
aria-label={t('refresh')}
|
|
173
|
+
title={t('refresh')}
|
|
174
|
+
onClick={() => { setRefreshTick(tick => tick + 1) }}
|
|
175
|
+
>
|
|
176
|
+
<IconRefreshOutline16 size={14} />
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
className={css.iconButton}
|
|
181
|
+
aria-label={t('uploadFiles')}
|
|
182
|
+
title={t('uploadFiles')}
|
|
183
|
+
disabled={busy}
|
|
184
|
+
onClick={() => { fileInputRef.current?.click() }}
|
|
185
|
+
>
|
|
186
|
+
<IconUploadOutline16 size={14} />
|
|
187
|
+
</button>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
className={css.iconButton}
|
|
191
|
+
aria-label={t('uploadFolder')}
|
|
192
|
+
title={t('uploadFolder')}
|
|
193
|
+
disabled={busy}
|
|
194
|
+
onClick={() => { folderInputRef.current?.click() }}
|
|
195
|
+
>
|
|
196
|
+
<IconFolderOpen16 size={14} />
|
|
197
|
+
</button>
|
|
198
|
+
<input
|
|
199
|
+
ref={fileInputRef}
|
|
200
|
+
type="file"
|
|
201
|
+
multiple
|
|
202
|
+
style={{ display: 'none' }}
|
|
203
|
+
onChange={(event) => {
|
|
204
|
+
if (cwd !== undefined) startUpload(cwd, uploadItemsFromFiles(event.target.files ?? []))
|
|
205
|
+
event.target.value = ''
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
<input
|
|
209
|
+
ref={folderInputRef}
|
|
210
|
+
type="file"
|
|
211
|
+
multiple
|
|
212
|
+
{...folderInputProps}
|
|
213
|
+
style={{ display: 'none' }}
|
|
214
|
+
onChange={(event) => {
|
|
215
|
+
if (cwd !== undefined) startUpload(cwd, uploadItemsFromFiles(event.target.files ?? []))
|
|
216
|
+
event.target.value = ''
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
{uploadStatus !== '' && (
|
|
221
|
+
<div className={clsx(css.editorSearchHint, uploadFailed && css.editorError)} title={uploadStatus}>{uploadStatus}</div>
|
|
222
|
+
)}
|
|
223
|
+
{needle === '' ? (
|
|
224
|
+
<FileTree
|
|
225
|
+
sessionId={sessionId}
|
|
226
|
+
cwd={cwd}
|
|
227
|
+
store={store}
|
|
228
|
+
expanded={expanded}
|
|
229
|
+
revealed={revealed}
|
|
230
|
+
onToggle={onToggle}
|
|
231
|
+
onOpenFile={onOpenFile}
|
|
232
|
+
onOpenFileNewTab={onOpenFileNewTab}
|
|
233
|
+
onOpenFileSide={onOpenFileSide}
|
|
234
|
+
openWithTargets={openWithTargets}
|
|
235
|
+
openWithPinned={openWithPinned}
|
|
236
|
+
openWithSsh={openWithSsh}
|
|
237
|
+
onOpenWith={onOpenWith}
|
|
238
|
+
onToggleOpenWithPin={onToggleOpenWithPin}
|
|
239
|
+
onReferenceFile={onReferenceFile}
|
|
240
|
+
refreshTick={refreshTick}
|
|
241
|
+
onUploadRequest={startUpload}
|
|
242
|
+
busy={busy}
|
|
243
|
+
/>
|
|
244
|
+
) : (
|
|
245
|
+
<div className={css.explorerBody}>
|
|
246
|
+
{error !== null && <div className={clsx(css.editorSearchHint, css.editorError)}>{error}</div>}
|
|
247
|
+
{error === null && results === null && <div className={css.editorSearchHint}>{t('loading')}</div>}
|
|
248
|
+
{error === null && results !== null && results.matches.length === 0 && (
|
|
249
|
+
<div className={css.editorSearchHint}>{t('editorSearchNoResults')}</div>
|
|
250
|
+
)}
|
|
251
|
+
{error === null && results !== null && results.matches.map(rel => (
|
|
252
|
+
<button
|
|
253
|
+
key={rel}
|
|
254
|
+
type="button"
|
|
255
|
+
className={css.editorSearchResult}
|
|
256
|
+
title={rel}
|
|
257
|
+
onClick={() => { onOpenFile(resolveSidebarPath(cwd, rel)) }}
|
|
258
|
+
>
|
|
259
|
+
{rel}
|
|
260
|
+
</button>
|
|
261
|
+
))}
|
|
262
|
+
{error === null && results?.truncated === true && (
|
|
263
|
+
<div className={css.editorSearchHint}>{t('editorSearchTruncated')}</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
{upload !== null && (
|
|
268
|
+
<UploadOverlay
|
|
269
|
+
dir={upload.dir}
|
|
270
|
+
done={upload.done}
|
|
271
|
+
total={upload.total}
|
|
272
|
+
current={upload.current}
|
|
273
|
+
onCancel={cancelUpload}
|
|
274
|
+
cancelling={cancelling}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Check for updates / update now" panel. It asks the host to compare the
|
|
3
|
+
* ChatCode package against the npm registry, then — on a single "update now"
|
|
4
|
+
* click — upgrades it globally via the host. The upgrade runs on the DSH
|
|
5
|
+
* machine (npm install -g), not in the browser, so success is followed by a
|
|
6
|
+
* "restart to apply" hint.
|
|
7
|
+
*
|
|
8
|
+
* Two mount variants:
|
|
9
|
+
* - `variant: 'dialog'`: a full-screen modal overlay (legacy, for a menu
|
|
10
|
+
* entry that floats above the app).
|
|
11
|
+
* - `variant: 'inline'` (default here): same content but NO overlay, NO modal
|
|
12
|
+
* frame — it renders embedded in the host Settings panel, so it never
|
|
13
|
+
* stacks a second white backdrop on top of the Settings dialog's own.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from 'react'
|
|
16
|
+
import {
|
|
17
|
+
api,
|
|
18
|
+
type ChatCodeUpdateCheck,
|
|
19
|
+
type ChatCodeUpdateResult,
|
|
20
|
+
} from './api.ts'
|
|
21
|
+
import { t } from './locales.ts'
|
|
22
|
+
import css from './sidebar.module.css'
|
|
23
|
+
|
|
24
|
+
type Phase = 'checking' | 'idle' | 'performing' | 'done' | 'error'
|
|
25
|
+
|
|
26
|
+
interface UpdateDialogProps {
|
|
27
|
+
open: boolean
|
|
28
|
+
onClose: () => void
|
|
29
|
+
/** 'dialog' wraps content in a full-screen overlay; 'inline' renders the
|
|
30
|
+
* same content bare so it can sit inside the host Settings panel without
|
|
31
|
+
* a second backdrop. */
|
|
32
|
+
variant?: 'dialog' | 'inline'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Describe one package row, tagging whether it is out of date. */
|
|
36
|
+
function PackageRow({ check }: { check: ChatCodeUpdateCheck }): JSX.Element {
|
|
37
|
+
const current = check.current ?? t('updateNotInstalled')
|
|
38
|
+
const latest = check.latest ?? t('updateUnknown')
|
|
39
|
+
return (
|
|
40
|
+
<li>
|
|
41
|
+
<strong>{check.packageName}</strong>
|
|
42
|
+
<span className={css.updateVersions}>{current} → {latest}</span>
|
|
43
|
+
</li>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function UpdateDialog({ open, onClose, variant = 'dialog' }: UpdateDialogProps): JSX.Element | null {
|
|
48
|
+
const [phase, setPhase] = useState<Phase>('checking')
|
|
49
|
+
const [checks, setChecks] = useState<ChatCodeUpdateCheck[]>([])
|
|
50
|
+
const [results, setResults] = useState<ChatCodeUpdateResult[]>([])
|
|
51
|
+
const [error, setError] = useState('')
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!open) return
|
|
55
|
+
setPhase('checking')
|
|
56
|
+
setResults([])
|
|
57
|
+
setError('')
|
|
58
|
+
const controller = new AbortController()
|
|
59
|
+
void api.chatcodeUpdateCheck(controller.signal)
|
|
60
|
+
.then(({ packages }) => {
|
|
61
|
+
setChecks(packages)
|
|
62
|
+
setPhase('idle')
|
|
63
|
+
})
|
|
64
|
+
.catch((reason: unknown) => {
|
|
65
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
66
|
+
setPhase('error')
|
|
67
|
+
})
|
|
68
|
+
return () => controller.abort()
|
|
69
|
+
}, [open])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!open) return
|
|
73
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
74
|
+
if (event.key !== 'Escape') return
|
|
75
|
+
event.preventDefault()
|
|
76
|
+
onClose()
|
|
77
|
+
}
|
|
78
|
+
window.addEventListener('keydown', onKeyDown)
|
|
79
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
80
|
+
}, [open, onClose])
|
|
81
|
+
|
|
82
|
+
if (!open) return null
|
|
83
|
+
|
|
84
|
+
const anyUpdate = checks.some(check => check.needsUpdate)
|
|
85
|
+
const close = (): void => onClose()
|
|
86
|
+
|
|
87
|
+
const perform = async (): Promise<void> => {
|
|
88
|
+
setPhase('performing')
|
|
89
|
+
setError('')
|
|
90
|
+
try {
|
|
91
|
+
const { results: next } = await api.chatcodeUpdatePerform()
|
|
92
|
+
setResults(next)
|
|
93
|
+
setChecks(next.map(result => ({
|
|
94
|
+
packageName: result.packageName,
|
|
95
|
+
current: result.ok ? result.to : null,
|
|
96
|
+
latest: result.to,
|
|
97
|
+
needsUpdate: false,
|
|
98
|
+
})))
|
|
99
|
+
setPhase('done')
|
|
100
|
+
} catch (reason: unknown) {
|
|
101
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
102
|
+
setPhase('error')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = (
|
|
107
|
+
<div className={css.updateBody}>
|
|
108
|
+
{phase === 'checking' && <p>{t('updateChecking')}</p>}
|
|
109
|
+
{phase === 'idle' && (
|
|
110
|
+
<>
|
|
111
|
+
<ul className={css.updateList}>
|
|
112
|
+
{checks.map(check => <PackageRow key={check.packageName} check={check} />)}
|
|
113
|
+
</ul>
|
|
114
|
+
{anyUpdate
|
|
115
|
+
? <p>{t('updateConfirm')}</p>
|
|
116
|
+
: <p>{t('updateUpToDate')}</p>}
|
|
117
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
{phase === 'performing' && <p>{t('updatePerforming')}</p>}
|
|
120
|
+
{phase === 'done' && (
|
|
121
|
+
<>
|
|
122
|
+
<ul className={css.updateList}>
|
|
123
|
+
{results.map(result => (
|
|
124
|
+
<li key={result.packageName}>
|
|
125
|
+
<strong>{result.packageName}</strong>
|
|
126
|
+
<span className={css.updateVersions}>{result.from ?? t('updateNotInstalled')} → {result.to ?? '?'}</span>
|
|
127
|
+
{result.ok && <span className={css.updateOk}> ✓</span>}
|
|
128
|
+
{!result.ok && <span className={css.updateError}> ✗</span>}
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
{results.some(result => !result.ok) ? (
|
|
133
|
+
<>
|
|
134
|
+
<p className={css.updateErrorText}>{t('updateError')}</p>
|
|
135
|
+
{results.filter(result => !result.ok).map(result => (
|
|
136
|
+
<pre key={result.packageName} className={css.updateCommand}>{result.message}</pre>
|
|
137
|
+
))}
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
<p>{t('updateRestartHint')}</p>
|
|
141
|
+
)}
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
{phase === 'error' && <p className={css.updateErrorText}>{error}</p>}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const footer = (
|
|
149
|
+
<div className={css.updateFooter}>
|
|
150
|
+
{phase === 'idle' && anyUpdate && (
|
|
151
|
+
<button type="button" className={css.releaseNotesButton} onClick={() => void perform()}>{t('updateNow')}</button>
|
|
152
|
+
)}
|
|
153
|
+
{(phase === 'done' || phase === 'error' || phase === 'idle') && (
|
|
154
|
+
<button type="button" className={css.releaseNotesButton} onClick={close}>{t('close')}</button>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if (variant === 'inline') {
|
|
160
|
+
// No overlay, no modal frame: this content lives on the Settings pane
|
|
161
|
+
// itself, whose own dialog already provides the dimmed backdrop.
|
|
162
|
+
return (
|
|
163
|
+
<div className={css.updateInline}>
|
|
164
|
+
{body}
|
|
165
|
+
{footer}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
className={css.releaseNotesOverlay}
|
|
173
|
+
role="presentation"
|
|
174
|
+
onMouseDown={event => { if (event.target === event.currentTarget) close() }}
|
|
175
|
+
>
|
|
176
|
+
<section className={css.releaseNotesDialog} role="dialog" aria-modal="true" aria-labelledby="update-notes-title">
|
|
177
|
+
<header className={css.releaseNotesHeader}>
|
|
178
|
+
<h2 id="update-notes-title">{t('updateMenu')}</h2>
|
|
179
|
+
<button type="button" className={css.releaseNotesIconButton} aria-label={t('close')} onClick={close}>×</button>
|
|
180
|
+
</header>
|
|
181
|
+
{body}
|
|
182
|
+
{footer}
|
|
183
|
+
</section>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Check for updates / update now" entry surfaced inside the host Settings
|
|
3
|
+
* dialog (rendered via the "更新" settings.section panel). Keeps a slim
|
|
4
|
+
* inline row + the reusable {@link UpdateDialog} **inline** overlay, so no
|
|
5
|
+
* second modal/backdrop stacks on top of the Settings dialog's own — the
|
|
6
|
+
* update state unfolds directly in the panel body.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import { t } from './locales.ts'
|
|
10
|
+
import { UpdateDialog } from './UpdateDialog.tsx'
|
|
11
|
+
import css from './sidebar.module.css'
|
|
12
|
+
|
|
13
|
+
export function UpdateSettingsPanel(): JSX.Element {
|
|
14
|
+
const [open, setOpen] = useState(false)
|
|
15
|
+
return (
|
|
16
|
+
<div className={css.updateSettingsBlock}>
|
|
17
|
+
<div className={css.updateSettingsRow}>
|
|
18
|
+
<div className={css.updateSettingsText}>
|
|
19
|
+
<span className={css.updateSettingsTitle}>{t('updateMenu')}</span>
|
|
20
|
+
<span className={css.updateSettingsDesc}>{t('updateSettingsDesc')}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<button type="button" className={css.releaseNotesButton} onClick={() => setOpen(v => !v)}>
|
|
23
|
+
{t('updateNow')}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
<UpdateDialog open={open} onClose={() => setOpen(false)} variant="inline" />
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "更新" settings section: a dedicated nav entry in the DSH Settings
|
|
3
|
+
* shell (sibling of 侧边卡片 / Token 用量 / AI 代码占比) hosting the
|
|
4
|
+
* one-click ChatCode upgrade entry. It surfaces the reusable
|
|
5
|
+
* {@link UpdateSettingsPanel} row in the section body.
|
|
6
|
+
*/
|
|
7
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
|
+
import { UpdateSettingsPanel } from './UpdateSettingsPanel.tsx'
|
|
9
|
+
import css from './SideCardSection.module.css'
|
|
10
|
+
|
|
11
|
+
/** Full section props: the settings shell's runtime share (close, …). */
|
|
12
|
+
export type UpdateSettingsSectionProps = PropsRuntime<'settings.section'>
|
|
13
|
+
|
|
14
|
+
export function UpdateSettingsSection(_props: UpdateSettingsSectionProps): JSX.Element {
|
|
15
|
+
return (
|
|
16
|
+
<div className={css.section}>
|
|
17
|
+
<div className={css.group}>
|
|
18
|
+
<UpdateSettingsPanel />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -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
|
+
}
|