dsh-better-sidebar 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/README_EN.md +139 -0
- package/lib/client-docx.js +7649 -0
- package/lib/client-editor.js +32677 -0
- package/lib/client-pptx.js +63438 -0
- package/lib/client-registry.js +8021 -0
- package/lib/client-terminal.js +8060 -0
- package/lib/client-xlsx.js +359755 -0
- package/lib/client.js +8021 -0
- package/lib/index.js +2604 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-pty.d.ts +220 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +13 -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 +10 -0
- package/lib/types/client/ExplorerView.d.ts +9 -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/PptxView.d.ts +6 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +27 -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 +38 -0
- package/lib/types/client/TerminalView.d.ts +8 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/api.d.ts +170 -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 +17 -0
- package/lib/types/client/builtins/tabs.d.ts +6 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +74 -0
- package/lib/types/client/chunks/docx.d.ts +8 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/pptx.d.ts +8 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/chunks/xlsx.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/docx-view.d.ts +8 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/icons.d.ts +47 -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 +10 -0
- package/lib/types/client/intercept.d.ts +20 -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 +35 -0
- package/lib/types/client/locales.d.ts +237 -0
- package/lib/types/client/office-shared.d.ts +33 -0
- package/lib/types/client/office-types.d.ts +36 -0
- package/lib/types/client/office-view.d.ts +12 -0
- package/lib/types/client/openpath-intercept.d.ts +41 -0
- package/lib/types/client/paths.d.ts +19 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/prefs.d.ts +30 -0
- package/lib/types/client/produced-files.d.ts +27 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +214 -0
- package/lib/types/client/split-pane.d.ts +29 -0
- package/lib/types/client/state.d.ts +322 -0
- package/lib/types/client/subagent-activity.d.ts +35 -0
- package/lib/types/client/subagent-detect.d.ts +47 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/theme.d.ts +28 -0
- package/lib/types/client/xlsx-to-univer.d.ts +33 -0
- package/lib/types/client/xlsx-view.d.ts +10 -0
- package/lib/types/config.d.ts +41 -0
- package/lib/types/context-types.d.ts +431 -0
- package/lib/types/fs-tree.d.ts +41 -0
- package/lib/types/git.d.ts +75 -0
- package/lib/types/html-route.d.ts +48 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/prefs-shared.d.ts +106 -0
- package/lib/types/pty-manager.d.ts +76 -0
- package/lib/types/tools.d.ts +28 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +168 -0
- package/src/agent-pty.ts +519 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +130 -0
- package/src/client/BrowserView.tsx +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +266 -0
- package/src/client/EditorHost.tsx +101 -0
- package/src/client/ExplorerView.tsx +283 -0
- package/src/client/GitView.tsx +540 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/PptxView.tsx +131 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +271 -0
- package/src/client/SideCardSection.tsx +426 -0
- package/src/client/Sidebar.tsx +861 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +866 -0
- package/src/client/TabBar.tsx +188 -0
- package/src/client/TerminalView.tsx +233 -0
- package/src/client/TextEditor.tsx +382 -0
- package/src/client/api.ts +215 -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 +33 -0
- package/src/client/builtins/tabs.tsx +211 -0
- package/src/client/builtins/viewers.tsx +151 -0
- package/src/client/chunk-loader.ts +196 -0
- package/src/client/chunks/docx.tsx +8 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/pptx.tsx +8 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/chunks/xlsx.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/docx-view.tsx +113 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/icons.tsx +137 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +238 -0
- package/src/client/intercept.tsx +94 -0
- package/src/client/lang.ts +95 -0
- package/src/client/layout.css +54 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +66 -0
- package/src/client/locales.ts +476 -0
- package/src/client/office-shared.tsx +40 -0
- package/src/client/office-types.ts +48 -0
- package/src/client/office-view.tsx +12 -0
- package/src/client/openpath-intercept.ts +62 -0
- package/src/client/paths.ts +28 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/prefs.ts +100 -0
- package/src/client/produced-files.ts +87 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +434 -0
- package/src/client/sidebar.module.css +1917 -0
- package/src/client/split-pane.tsx +292 -0
- package/src/client/state.ts +1048 -0
- package/src/client/subagent-activity.ts +67 -0
- package/src/client/subagent-detect.ts +125 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/theme.ts +44 -0
- package/src/client/xlsx-to-univer.ts +260 -0
- package/src/client/xlsx-view.tsx +116 -0
- package/src/config.ts +95 -0
- package/src/context-types.ts +445 -0
- package/src/fs-tree.ts +115 -0
- package/src/git.ts +236 -0
- package/src/html-route.ts +70 -0
- package/src/index.ts +897 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/prefs-shared.ts +127 -0
- package/src/pty-manager.ts +199 -0
- package/src/tools.ts +477 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +94 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL policy for the built-in browser tab: normalize user input into
|
|
3
|
+
* an http(s) URL, and refuse destinations that would be dangerous to embed
|
|
4
|
+
* in the sidebar iframe. Kept dependency-free so it is unit-testable.
|
|
5
|
+
*
|
|
6
|
+
* The iframe sandbox (opaque origin, no allow-same-origin / top-navigation)
|
|
7
|
+
* is the primary security boundary; this module is the address-bar gate on
|
|
8
|
+
* top of it: only http/https may be navigated, and loopback addresses are
|
|
9
|
+
* refused so a browsed page cannot probe local services by user action.
|
|
10
|
+
* The GUI's OWN origin is explicitly ALLOWED — the user may open the GUI
|
|
11
|
+
* itself in the sidebar (debugging, mirroring); the sandbox still renders
|
|
12
|
+
* it in an opaque origin with no same-origin privileges, exactly like any
|
|
13
|
+
* other site.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Why a navigation attempt was refused. */
|
|
17
|
+
export type BrowserBlockReason = 'scheme' | 'loopback'
|
|
18
|
+
|
|
19
|
+
/** Result of normalizing one address-bar input. */
|
|
20
|
+
export type BrowserNavigateResult =
|
|
21
|
+
| { kind: 'ok'; url: string }
|
|
22
|
+
| { kind: 'blocked'; reason: BrowserBlockReason }
|
|
23
|
+
| { kind: 'invalid' }
|
|
24
|
+
|
|
25
|
+
/** One browser.probe wire result (host fetch of the target's headers). */
|
|
26
|
+
export interface BrowserProbeResult {
|
|
27
|
+
reachable: boolean
|
|
28
|
+
/** The final (post-redirect) URL; present when reachable. */
|
|
29
|
+
url?: string
|
|
30
|
+
status?: number
|
|
31
|
+
xFrameOptions?: string
|
|
32
|
+
/** The CSP frame-ancestors source list; present when the directive exists. */
|
|
33
|
+
frameAncestors?: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Embeddability verdict of one probe. */
|
|
37
|
+
export type Embeddability = 'embeddable' | 'blocked' | 'unknown'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decide whether a site can render inside the sidebar iframe. The signals
|
|
41
|
+
* are exactly the ones the BROWSER enforces when it refuses an iframe load:
|
|
42
|
+
* X-Frame-Options DENY/SAMEORIGIN, or a frame-ancestors directive that does
|
|
43
|
+
* not allow `*` ('self' here means the SITE's own origin — never ours, so
|
|
44
|
+
* it also blocks the sidebar). A site we could not reach yields 'unknown'
|
|
45
|
+
* and the plain iframe stays.
|
|
46
|
+
*/
|
|
47
|
+
export function embeddabilityOf(probe: BrowserProbeResult): Embeddability {
|
|
48
|
+
if (probe.reachable !== true) return 'unknown'
|
|
49
|
+
const xfo = probe.xFrameOptions?.trim().toUpperCase()
|
|
50
|
+
if (xfo === 'DENY' || xfo === 'SAMEORIGIN') return 'blocked'
|
|
51
|
+
if (probe.frameAncestors !== undefined && !probe.frameAncestors.some(source => source === '*')) return 'blocked'
|
|
52
|
+
return 'embeddable'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A loopback hostname (localhost, IPv6 ::1, 127.0.0.0/8, 0.0.0.0). */
|
|
56
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
57
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
58
|
+
if (host === 'localhost' || host === '::1' || host === '0.0.0.0') return true
|
|
59
|
+
const parts = host.split('.')
|
|
60
|
+
return parts.length === 4
|
|
61
|
+
&& parts[0] === '127'
|
|
62
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize one address-bar input against the navigation policy.
|
|
67
|
+
* @param input - raw user text.
|
|
68
|
+
* @param selfOrigin - the GUI's own origin (window.location.origin). The GUI
|
|
69
|
+
* itself may be browsed in the sidebar (the sandbox keeps it opaque), so it
|
|
70
|
+
* is let through BEFORE the loopback check — its host is normally loopback.
|
|
71
|
+
*/
|
|
72
|
+
/** Schemes that must never reach the iframe, even without `//` (javascript:,
|
|
73
|
+
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
74
|
+
* they parse as hosts below. */
|
|
75
|
+
const FORBIDDEN_SCHEMES = new Set([
|
|
76
|
+
'javascript', 'data', 'file', 'about', 'vbscript', 'blob',
|
|
77
|
+
'mailto', 'tel', 'ftp', 'ftps', 'ws', 'wss', 'sftp', 'ssh',
|
|
78
|
+
'chrome', 'chrome-extension', 'moz-extension', 'edge', 'opera', 'resource', 'view-source',
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
export function normalizeBrowserUrl(input: string, selfOrigin: string): BrowserNavigateResult {
|
|
82
|
+
const trimmed = input.trim()
|
|
83
|
+
if (trimmed === '') return { kind: 'invalid' }
|
|
84
|
+
// Distinguish an explicit scheme from a bare host:port. "example.com:8080"
|
|
85
|
+
// would match a naive scheme regex (dots are legal in schemes), so a
|
|
86
|
+
// scheme prefix is only honored when it is http(s) or a known-forbidden
|
|
87
|
+
// scheme; anything else is treated as a host and gets https://.
|
|
88
|
+
const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)
|
|
89
|
+
let withScheme: string
|
|
90
|
+
if (schemeMatch === null) {
|
|
91
|
+
withScheme = `https://${trimmed}`
|
|
92
|
+
} else {
|
|
93
|
+
const scheme = schemeMatch[1]!.toLowerCase()
|
|
94
|
+
if (scheme === 'http' || scheme === 'https') withScheme = trimmed
|
|
95
|
+
else if (FORBIDDEN_SCHEMES.has(scheme)) return { kind: 'blocked', reason: 'scheme' }
|
|
96
|
+
else withScheme = `https://${trimmed}`
|
|
97
|
+
}
|
|
98
|
+
let url: URL
|
|
99
|
+
try {
|
|
100
|
+
url = new URL(withScheme)
|
|
101
|
+
} catch {
|
|
102
|
+
return { kind: 'invalid' }
|
|
103
|
+
}
|
|
104
|
+
// The protocol backstop: any URL that still parses to a non-http(s)
|
|
105
|
+
// scheme (e.g. ftp://, ws:// — which carry `//` and skip the list) is
|
|
106
|
+
// refused here.
|
|
107
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return { kind: 'blocked', reason: 'scheme' }
|
|
108
|
+
// The GUI's own origin is ALLOWED (the user may browse the GUI itself in
|
|
109
|
+
// the sidebar; the sandbox renders it in an opaque origin like any other
|
|
110
|
+
// site). It must be checked before the loopback gate because its host is
|
|
111
|
+
// normally loopback.
|
|
112
|
+
try {
|
|
113
|
+
if (url.origin === new URL(selfOrigin).origin) return { kind: 'ok', url: url.href }
|
|
114
|
+
} catch {
|
|
115
|
+
// Unparsable selfOrigin (never in practice): fall through to the loopback gate.
|
|
116
|
+
}
|
|
117
|
+
if (isLoopbackHostname(url.hostname)) return { kind: 'blocked', reason: 'loopback' }
|
|
118
|
+
return { kind: 'ok', url: url.href }
|
|
119
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in registration: the plugin registers its own tab pages and file
|
|
3
|
+
* previewers through the same {@link BetterSidebarService} external plugins
|
|
4
|
+
* use — eating its own dogfood. The descriptors live next to their feature
|
|
5
|
+
* modules (tabs.tsx / viewers.tsx); this module only aggregates them and
|
|
6
|
+
* owns the disposer lifecycle (cordis auto-invokes it on fiber disposal,
|
|
7
|
+
* HMR-safe).
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '../../context-types.ts'
|
|
10
|
+
import type { BetterSidebarService } from '../service.ts'
|
|
11
|
+
import { builtinTabs } from './tabs.tsx'
|
|
12
|
+
import { builtinViewers } from './viewers.tsx'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register all built-in tabs and viewers with the service. Returns a
|
|
16
|
+
* disposer that unregisters everything (cordis auto-invokes it on fiber
|
|
17
|
+
* disposal). The `ctx` is threaded into tab descriptors that need it
|
|
18
|
+
* (EditorHost reads `ctx.betterSidebar` for file-viewer matching).
|
|
19
|
+
*/
|
|
20
|
+
export function registerBuiltins(ctx: Context, service: BetterSidebarService): () => void {
|
|
21
|
+
const disposers: (() => void)[] = []
|
|
22
|
+
for (const tab of builtinTabs(ctx)) {
|
|
23
|
+
disposers.push(service.registerTab(tab))
|
|
24
|
+
}
|
|
25
|
+
for (const viewer of builtinViewers()) {
|
|
26
|
+
disposers.push(service.registerFileViewer(viewer))
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
for (const d of disposers) {
|
|
30
|
+
try { d() } catch { /* already disposed */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 7 built-in tab descriptors: the plugin registers its own pages
|
|
3
|
+
* (explorer / git / terminal / browser / subagent / editor / diff) through
|
|
4
|
+
* the same {@link BetterSidebarService} external plugins use — eating its
|
|
5
|
+
* own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
|
|
6
|
+
* and mints `terminal:<n>` ids through `createTab`; the browser mints
|
|
7
|
+
* `browser:<n>` the same way (no quota).
|
|
8
|
+
*/
|
|
9
|
+
import { IconBranchOutline16, IconCodeOutline16, IconFolderOpen16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
10
|
+
import type { Context } from '../../context-types.ts'
|
|
11
|
+
import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
|
|
12
|
+
import { t } from '../locales.ts'
|
|
13
|
+
import { openSidebarFile } from '../intercept.tsx'
|
|
14
|
+
import { ExplorerView } from '../ExplorerView.tsx'
|
|
15
|
+
import { EditorHost } from '../EditorHost.tsx'
|
|
16
|
+
import { lazyChunkComponent } from '../lazy-chunk.tsx'
|
|
17
|
+
import { GitView } from '../GitView.tsx'
|
|
18
|
+
import { DiffTab } from '../DiffTab.tsx'
|
|
19
|
+
import { SubagentView } from '../SubagentView.tsx'
|
|
20
|
+
import { BrowserView } from '../BrowserView.tsx'
|
|
21
|
+
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
|
|
22
|
+
import type { ComponentType } from 'react'
|
|
23
|
+
import type { SessionScope } from '../api.ts'
|
|
24
|
+
import type { SidebarStore } from '../state.ts'
|
|
25
|
+
import type { TabDescriptor } from '../service.ts'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazy wrapper over the terminal view: xterm (and its stylesheet) is fetched
|
|
29
|
+
* only when a terminal tab is first opened (see chunk-loader.ts). The
|
|
30
|
+
* wrapper keeps the descriptor contract `(props) => ReactNode` — Sidebar
|
|
31
|
+
* calls it as a plain function.
|
|
32
|
+
*
|
|
33
|
+
* TerminalView's props are { scope, tabId, store } — `tabId` is NOT part of
|
|
34
|
+
* TabComponentProps (it carries `tab: SidebarTab` instead), so the
|
|
35
|
+
* descriptor maps it explicitly; a bare pass-through would leave tabId
|
|
36
|
+
* undefined and TerminalView's isAgentTabId(tabId) would crash on
|
|
37
|
+
* `undefined.startsWith` (regression-pinned in tests/lazy-chunk.spec.tsx).
|
|
38
|
+
*/
|
|
39
|
+
const LazyTerminal = lazyChunkComponent<TerminalViewProps>(
|
|
40
|
+
'terminal',
|
|
41
|
+
(mod) => mod.TerminalView as ComponentType<TerminalViewProps> | undefined,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
/** The terminal view's props (mirror of TerminalView's own signature). */
|
|
45
|
+
interface TerminalViewProps {
|
|
46
|
+
scope: SessionScope
|
|
47
|
+
tabId: string
|
|
48
|
+
store: SidebarStore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
|
|
52
|
+
export const TERMINAL_LIMIT = 3
|
|
53
|
+
|
|
54
|
+
/** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */
|
|
55
|
+
function uiTerminalCount(state: SidebarState): number {
|
|
56
|
+
return allLeaves(state.splits)
|
|
57
|
+
.flatMap(leaf => leaf.tabs)
|
|
58
|
+
.filter(tab => tab.type === 'terminal' && !isAgentTabId(tab.id)).length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The 7 built-in tab descriptors. */
|
|
62
|
+
export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: 'editor',
|
|
66
|
+
title: () => t('editor'),
|
|
67
|
+
icon: (size: number) => <IconCodeOutline16 size={size} />,
|
|
68
|
+
order: -1,
|
|
69
|
+
hidden: true,
|
|
70
|
+
dedupeKey: (tab) => tab.path,
|
|
71
|
+
component: ({ ctx, store, scope, tab }) => (
|
|
72
|
+
<EditorHost ctx={ctx} store={store} scope={scope} path={tab.path ?? ''} title={tab.title} />
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'explorer',
|
|
77
|
+
title: () => t('explorer'),
|
|
78
|
+
icon: (size: number) => <IconFolderOpen16 size={size} />,
|
|
79
|
+
order: 10,
|
|
80
|
+
single: true,
|
|
81
|
+
component: ({ ctx, store, scope, expanded, onToggleDir, onReferenceFile }) => (
|
|
82
|
+
<ExplorerView
|
|
83
|
+
sessionId={scope.sessionId}
|
|
84
|
+
cwd={scope.cwd}
|
|
85
|
+
expanded={expanded ?? []}
|
|
86
|
+
onToggle={onToggleDir ?? (() => { /* no-op */ })}
|
|
87
|
+
onOpenFile={(path) => { openSidebarFile(ctx, store, scope.sessionId, path) }}
|
|
88
|
+
onReferenceFile={onReferenceFile ?? (() => { /* no-op */ })}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'git',
|
|
94
|
+
title: () => t('git'),
|
|
95
|
+
icon: (size: number) => <IconBranchOutline16 size={size} />,
|
|
96
|
+
order: 20,
|
|
97
|
+
single: true,
|
|
98
|
+
component: ({ ctx, store, scope, onOpenDiff }) => (
|
|
99
|
+
<GitView
|
|
100
|
+
scope={scope}
|
|
101
|
+
onOpenFile={(path) => { openSidebarFile(ctx, store, scope.sessionId, path) }}
|
|
102
|
+
onOpenDiff={onOpenDiff ?? (() => { /* no-op */ })}
|
|
103
|
+
/>
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'subagent',
|
|
108
|
+
title: () => t('subagent'),
|
|
109
|
+
icon: (size: number) => <IconThinkOutline16 size={size} />,
|
|
110
|
+
order: 30,
|
|
111
|
+
single: true,
|
|
112
|
+
// Declarative settings: the auto-open switches render under this row in
|
|
113
|
+
// the Side card settings page (the Jobs page's own related settings).
|
|
114
|
+
settings: {
|
|
115
|
+
toggles: [{
|
|
116
|
+
key: 'autoOpenSubagent',
|
|
117
|
+
title: () => t('settingsSubagentTitle'),
|
|
118
|
+
desc: () => t('settingsSubagentDesc'),
|
|
119
|
+
}, {
|
|
120
|
+
key: 'autoOpenJobs',
|
|
121
|
+
title: () => t('settingsJobsTitle'),
|
|
122
|
+
desc: () => t('settingsJobsDesc'),
|
|
123
|
+
}],
|
|
124
|
+
},
|
|
125
|
+
component: ({ ctx, scope, visible, onSubagentJump }) => (
|
|
126
|
+
<SubagentView
|
|
127
|
+
sessionId={scope.sessionId}
|
|
128
|
+
ctx={ctx}
|
|
129
|
+
active={visible}
|
|
130
|
+
onOpenChild={(address) => { onSubagentJump?.(address.childSessionId) }}
|
|
131
|
+
/>
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'terminal',
|
|
136
|
+
title: () => t('terminal'),
|
|
137
|
+
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
|
138
|
+
order: 40,
|
|
139
|
+
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
|
140
|
+
// Declarative settings: the model-facing terminal tools switch and the
|
|
141
|
+
// bottom-panel first-expansion auto-terminal switch render under this
|
|
142
|
+
// row in the Side card settings page (the Terminal page's own related
|
|
143
|
+
// settings; the host gates the toolset on the tools one independently).
|
|
144
|
+
settings: {
|
|
145
|
+
toggles: [{
|
|
146
|
+
key: 'agentTerminalTools',
|
|
147
|
+
title: () => t('settingsToolsTitle'),
|
|
148
|
+
desc: () => t('settingsToolsDesc'),
|
|
149
|
+
}, {
|
|
150
|
+
key: 'bottomPanelAutoTerminal',
|
|
151
|
+
title: () => t('settingsBottomTerminalTitle'),
|
|
152
|
+
desc: () => t('settingsBottomTerminalDesc'),
|
|
153
|
+
}],
|
|
154
|
+
},
|
|
155
|
+
createTab: (state) => {
|
|
156
|
+
const count = uiTerminalCount(state)
|
|
157
|
+
if (count >= TERMINAL_LIMIT) return null
|
|
158
|
+
return {
|
|
159
|
+
tab: {
|
|
160
|
+
id: `terminal:${state.nextTerminal}`,
|
|
161
|
+
type: 'terminal',
|
|
162
|
+
title: `${t('terminal')} ${state.nextTerminal}`,
|
|
163
|
+
},
|
|
164
|
+
patch: { nextTerminal: state.nextTerminal + 1 },
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
component: ({ tab, scope, store }) => <LazyTerminal scope={scope} store={store} tabId={tab.id} />,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'browser',
|
|
171
|
+
title: () => t('browser'),
|
|
172
|
+
icon: (size: number) => <IconGlobeOutline16 size={size} />,
|
|
173
|
+
order: 50,
|
|
174
|
+
// Declarative settings: the sandbox escape hatch and the link
|
|
175
|
+
// takeover render under this tab's row in the Side card settings
|
|
176
|
+
// page (the sandbox one is warned on).
|
|
177
|
+
settings: {
|
|
178
|
+
toggles: [{
|
|
179
|
+
key: 'browserNoSandbox',
|
|
180
|
+
title: () => t('settingsBrowserSandboxTitle'),
|
|
181
|
+
desc: () => t('settingsBrowserSandboxDesc'),
|
|
182
|
+
}, {
|
|
183
|
+
key: 'browserInterceptLinks',
|
|
184
|
+
title: () => t('settingsBrowserLinksTitle'),
|
|
185
|
+
desc: () => t('settingsBrowserLinksDesc'),
|
|
186
|
+
}],
|
|
187
|
+
},
|
|
188
|
+
createTab: (state) => ({
|
|
189
|
+
tab: {
|
|
190
|
+
id: `browser:${state.nextBrowser}`,
|
|
191
|
+
type: 'browser',
|
|
192
|
+
title: t('browser'),
|
|
193
|
+
},
|
|
194
|
+
patch: { nextBrowser: state.nextBrowser + 1 },
|
|
195
|
+
}),
|
|
196
|
+
component: (props) => <BrowserView {...props} />,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'diff',
|
|
200
|
+
title: () => t('git'),
|
|
201
|
+
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
|
202
|
+
order: -1,
|
|
203
|
+
hidden: true,
|
|
204
|
+
dedupeKey: (tab) => tab.id,
|
|
205
|
+
component: ({ scope, tab }) => (
|
|
206
|
+
tab.diff === undefined ? null
|
|
207
|
+
: <DiffTab sessionId={scope.sessionId} cwd={scope.cwd} diff={tab.diff} />
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 9 built-in file viewer descriptors: every preview surface is a
|
|
3
|
+
* registered viewer (image / pdf / docx / xlsx / pptx / markdown / html /
|
|
4
|
+
* code / binary-download), exactly like external plugins register theirs.
|
|
5
|
+
* The `binary-download` viewer sniffs NUL bytes via `detect` for unknown
|
|
6
|
+
* binaries and serves doc/xls/ppt by extension; `code` is the catch-all
|
|
7
|
+
* (`exts: []`, lowest priority) that claims any file no other viewer did.
|
|
8
|
+
*
|
|
9
|
+
* The heavy viewers (docx/xlsx/pptx and the CodeMirror-backed
|
|
10
|
+
* markdown/html/code) render through {@link lazyChunkComponent} wrappers —
|
|
11
|
+
* their libraries are fetched only when such a file is first opened (see
|
|
12
|
+
* chunk-loader.ts). The descriptor metadata (id/exts/priority/detect) is
|
|
13
|
+
* identical either way, so matching semantics and external-plugin
|
|
14
|
+
* overrides are unaffected; the `component` wrapper keeps the descriptor
|
|
15
|
+
* contract `(props) => ReactNode`.
|
|
16
|
+
*
|
|
17
|
+
* Every viewer carries the declarative settings-surface fields — `title`
|
|
18
|
+
* and `icon` — so the Side card settings page can render the enable/disable
|
|
19
|
+
* inventory without hardcoding (eating our own dogfood).
|
|
20
|
+
*/
|
|
21
|
+
import { IconCodeOutline16, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
22
|
+
import { lazyChunkComponent } from '../lazy-chunk.tsx'
|
|
23
|
+
import { PdfView } from '../PdfView.tsx'
|
|
24
|
+
import { BinaryDownload } from '../binary-download.tsx'
|
|
25
|
+
import {
|
|
26
|
+
IconImageOutline16,
|
|
27
|
+
IconMarkdownOutline16,
|
|
28
|
+
IconPdfOutline16,
|
|
29
|
+
IconDocxOutline16,
|
|
30
|
+
IconXlsxOutline16,
|
|
31
|
+
IconPptxOutline16,
|
|
32
|
+
IconHtmlOutline16,
|
|
33
|
+
} from '../icons.tsx'
|
|
34
|
+
import type { ComponentType } from 'react'
|
|
35
|
+
import type { FileViewerDescriptor, FileViewerProps } from '../service.ts'
|
|
36
|
+
import { t } from '../locales.ts'
|
|
37
|
+
import css from '../sidebar.module.css'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lazy wrappers over the chunk-resident viewer components. The `pick`
|
|
41
|
+
* functions are module-level (stable identity — the wrapper effect depends
|
|
42
|
+
* on them); the cast bridges the chunk exports record to the descriptor
|
|
43
|
+
* prop shape (the views read only their own subset of FileViewerProps).
|
|
44
|
+
*/
|
|
45
|
+
const LazyDocx = lazyChunkComponent<FileViewerProps>('docx', (mod) => mod.DocxView as ComponentType<FileViewerProps> | undefined)
|
|
46
|
+
const LazyXlsx = lazyChunkComponent<FileViewerProps>('xlsx', (mod) => mod.XlsxView as ComponentType<FileViewerProps> | undefined)
|
|
47
|
+
const LazyPptx = lazyChunkComponent<FileViewerProps>('pptx', (mod) => mod.PptxView as ComponentType<FileViewerProps> | undefined)
|
|
48
|
+
const LazyTextEditor = lazyChunkComponent<FileViewerProps>('editor', (mod) => mod.TextEditor as ComponentType<FileViewerProps> | undefined)
|
|
49
|
+
|
|
50
|
+
/** The 9 built-in file viewer descriptors. */
|
|
51
|
+
export function builtinViewers(): readonly FileViewerDescriptor[] {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
id: 'image',
|
|
55
|
+
title: () => t('viewerImage'),
|
|
56
|
+
icon: (size: number) => <IconImageOutline16 size={size} />,
|
|
57
|
+
exts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'],
|
|
58
|
+
fetchStrategy: 'mediaUrl',
|
|
59
|
+
component: ({ mediaUrl: url, title }) => (
|
|
60
|
+
<div className={css.editorImageWrap}>
|
|
61
|
+
<img className={css.editorImage} src={url} alt={title} />
|
|
62
|
+
</div>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'pdf',
|
|
67
|
+
title: () => t('viewerPdf'),
|
|
68
|
+
icon: (size: number) => <IconPdfOutline16 size={size} />,
|
|
69
|
+
exts: ['pdf'],
|
|
70
|
+
fetchStrategy: 'mediaUrl',
|
|
71
|
+
component: ({ scope, path, title }) => (
|
|
72
|
+
<PdfView scope={scope} path={path} title={title} />
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'docx',
|
|
77
|
+
title: () => t('viewerDocx'),
|
|
78
|
+
icon: (size: number) => <IconDocxOutline16 size={size} />,
|
|
79
|
+
exts: ['docx'],
|
|
80
|
+
fetchStrategy: 'mediaUrl',
|
|
81
|
+
component: (props) => <LazyDocx {...props} />,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'xlsx',
|
|
85
|
+
title: () => t('viewerXlsx'),
|
|
86
|
+
icon: (size: number) => <IconXlsxOutline16 size={size} />,
|
|
87
|
+
exts: ['xlsx'],
|
|
88
|
+
fetchStrategy: 'mediaUrl',
|
|
89
|
+
component: (props) => <LazyXlsx {...props} />,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'pptx',
|
|
93
|
+
title: () => t('viewerPptx'),
|
|
94
|
+
icon: (size: number) => <IconPptxOutline16 size={size} />,
|
|
95
|
+
exts: ['pptx'],
|
|
96
|
+
fetchStrategy: 'mediaUrl',
|
|
97
|
+
component: (props) => <LazyPptx {...props} />,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'markdown',
|
|
101
|
+
title: () => t('viewerMarkdown'),
|
|
102
|
+
icon: (size: number) => <IconMarkdownOutline16 size={size} />,
|
|
103
|
+
exts: ['md', 'markdown'],
|
|
104
|
+
fetchStrategy: 'fsRead',
|
|
105
|
+
component: (props) => <LazyTextEditor {...props} />,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'html',
|
|
109
|
+
title: () => t('viewerHtml'),
|
|
110
|
+
icon: (size: number) => <IconHtmlOutline16 size={size} />,
|
|
111
|
+
exts: ['html', 'htm'],
|
|
112
|
+
fetchStrategy: 'fsRead',
|
|
113
|
+
// Declarative settings: the sandbox escape hatch and the default-unsafe
|
|
114
|
+
// start state render under this viewer's row in the Side card settings
|
|
115
|
+
// page (both warned on).
|
|
116
|
+
settings: {
|
|
117
|
+
toggles: [{
|
|
118
|
+
key: 'htmlViewerNoSandbox',
|
|
119
|
+
title: () => t('settingsHtmlSandboxTitle'),
|
|
120
|
+
desc: () => t('settingsHtmlSandboxDesc'),
|
|
121
|
+
}, {
|
|
122
|
+
key: 'htmlViewerDefaultUnsafe',
|
|
123
|
+
title: () => t('settingsHtmlDefaultUnsafeTitle'),
|
|
124
|
+
desc: () => t('settingsHtmlDefaultUnsafeDesc'),
|
|
125
|
+
}],
|
|
126
|
+
},
|
|
127
|
+
component: (props) => <LazyTextEditor {...props} />,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'code',
|
|
131
|
+
title: () => t('viewerCode'),
|
|
132
|
+
icon: (size: number) => <IconCodeOutline16 size={size} />,
|
|
133
|
+
exts: [],
|
|
134
|
+
priority: -100,
|
|
135
|
+
fetchStrategy: 'fsRead',
|
|
136
|
+
component: (props) => <LazyTextEditor {...props} />,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'binary-download',
|
|
140
|
+
title: () => t('viewerBinary'),
|
|
141
|
+
icon: (size: number) => <IconDownloadOutline16 size={size} />,
|
|
142
|
+
exts: ['doc', 'xls', 'ppt'],
|
|
143
|
+
priority: -50,
|
|
144
|
+
fetchStrategy: 'binary-download',
|
|
145
|
+
// NUL probe: a file whose head bytes contain a NUL is binary — claimed
|
|
146
|
+
// before the catch-all code viewer on the head re-match.
|
|
147
|
+
detect: (_path, head) => head.includes(0),
|
|
148
|
+
component: ({ scope, path }) => <BinaryDownload scope={scope} path={path} />,
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
}
|