dsh-vscode-workbench 0.1.7 → 0.1.8

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.
@@ -1,5 +1,6 @@
1
1
  import type { FileDocument, GitCommit, GitDiff } from '../types.ts';
2
2
  import type { VscodeWorkbench } from './service.ts';
3
+ import { type DocumentPreviewKind } from './documentPreview.tsx';
3
4
  interface Tab extends FileDocument {
4
5
  dirty: boolean;
5
6
  draft: string;
@@ -13,6 +14,8 @@ interface Tab extends FileDocument {
13
14
  diffMode?: 'inline' | 'split';
14
15
  diffSourcePath?: string;
15
16
  imageUrl?: string;
17
+ documentPreview?: DocumentPreviewKind;
18
+ documentUrl?: string;
16
19
  }
17
20
  type ResizeDirection = 1 | -1;
18
21
  /** Calculate a panel width from a horizontal pointer movement. */
@@ -0,0 +1,11 @@
1
+ export type DocumentPreviewKind = 'pdf' | 'docx' | 'download';
2
+ /** Bound PDF zoom to values that remain usable in the workbench viewport. */
3
+ export declare function clampPdfScale(scale: number): number;
4
+ /** Return the in-workbench preview supported for a document path. */
5
+ export declare function documentPreviewKind(path: string): DocumentPreviewKind | undefined;
6
+ /** Render an in-workbench preview for PDF and Word documents. */
7
+ export declare function DocumentPreview({ kind, src, path }: {
8
+ kind: DocumentPreviewKind;
9
+ src: string;
10
+ path: string;
11
+ }): import("react").JSX.Element;
@@ -1,3 +1,7 @@
1
1
  import type { VscodeWorkbench } from './service.ts';
2
+ /** Reveal the terminal panel supplied by a terminal adapter without toggling it closed. */
3
+ export declare function openTerminal(workbench: VscodeWorkbench): boolean;
4
+ /** Toggle the terminal panel from the activity bar. */
5
+ export declare function toggleTerminal(workbench: VscodeWorkbench): boolean;
2
6
  /** Handle terminal shortcuts through contributions registered by a terminal adapter. */
3
7
  export declare function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean;
@@ -0,0 +1,43 @@
1
+ import type { TerminalInfo } from '../terminal.ts';
2
+ export interface TerminalConnection {
3
+ write(data: string): void;
4
+ resize(cols: number, rows: number): void;
5
+ close(): void;
6
+ subscribe(listener: (output: {
7
+ sequence: number;
8
+ data: string;
9
+ replay?: boolean;
10
+ }) => void): () => void;
11
+ }
12
+ export interface TerminalWorkspaceSnapshot {
13
+ terminals: readonly TerminalInfo[];
14
+ loading: boolean;
15
+ error?: string;
16
+ }
17
+ /** Browser client for the workbench's local terminal transport. */
18
+ export interface TerminalWorkspace {
19
+ refresh(): Promise<void>;
20
+ create(title?: string): Promise<TerminalInfo>;
21
+ close(id: string): Promise<void>;
22
+ rename(id: string, title: string): Promise<void>;
23
+ connect(id: string): TerminalConnection;
24
+ getSnapshot(): TerminalWorkspaceSnapshot;
25
+ subscribe(listener: () => void): () => void;
26
+ }
27
+ /** Identify automatic terminal-emulator replies that must not be echoed during replay. */
28
+ export declare function isTerminalProtocolReply(data: string): boolean;
29
+ /** Maintain one terminal collection for a DSH session. */
30
+ export declare class TerminalWorkspaceController implements TerminalWorkspace {
31
+ private readonly sessionId;
32
+ private snapshot;
33
+ private readonly listeners;
34
+ constructor(sessionId: string);
35
+ refresh(): Promise<void>;
36
+ create(title?: string): Promise<TerminalInfo>;
37
+ close(id: string): Promise<void>;
38
+ rename(id: string, title: string): Promise<void>;
39
+ connect(id: string): TerminalConnection;
40
+ getSnapshot(): TerminalWorkspaceSnapshot;
41
+ subscribe(listener: () => void): () => void;
42
+ private set;
43
+ }
@@ -19,6 +19,16 @@ export declare function workspaceImage(cwd: string, path: string): Promise<{
19
19
  path: string;
20
20
  type: string;
21
21
  }>;
22
+ /** Resolve a supported preview file inside the workspace. */
23
+ export declare function workspacePreviewFile(cwd: string, path: string): Promise<{
24
+ path: string;
25
+ type: string;
26
+ }>;
27
+ /** Resolve the PDF.js worker shipped with this plugin's runtime dependency. */
28
+ export declare function pdfWorkerAsset(): {
29
+ path: string;
30
+ type: string;
31
+ };
22
32
  interface ProcessResult {
23
33
  stdout: string;
24
34
  stderr: string;
@@ -0,0 +1,41 @@
1
+ import type { IPty } from 'node-pty';
2
+ import type { HostContext } from './context.ts';
3
+ export interface TerminalInfo {
4
+ id: string;
5
+ title: string;
6
+ cwd: string;
7
+ running: boolean;
8
+ exitCode?: number;
9
+ sequence: number;
10
+ }
11
+ export interface TerminalOutput {
12
+ sequence: number;
13
+ data: string;
14
+ }
15
+ interface TerminalHandle {
16
+ info: TerminalInfo;
17
+ process: IPty;
18
+ replay: string;
19
+ listeners: Set<(output: TerminalOutput) => void>;
20
+ }
21
+ /** Remove credentials while retaining ordinary interactive-shell variables. */
22
+ export declare function sanitizedTerminalEnv(env: NodeJS.ProcessEnv): Record<string, string>;
23
+ /** Select the user's interactive shell with platform-specific fallbacks. */
24
+ export declare function defaultTerminalShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
25
+ shell: string;
26
+ args: string[];
27
+ };
28
+ /** Own interactive terminals by their canonical workspace directory. */
29
+ export declare class TerminalManager {
30
+ private readonly workspaces;
31
+ private workspace;
32
+ list(cwd: string): TerminalInfo[];
33
+ create(cwd: string, title?: string): TerminalInfo;
34
+ get(cwd: string, id: string): TerminalHandle | undefined;
35
+ close(cwd: string, id: string): void;
36
+ rename(cwd: string, id: string, title: string): TerminalInfo;
37
+ dispose(): void;
38
+ }
39
+ /** Register the private local terminal API and WebSocket transport. */
40
+ export declare function installTerminal(ctx: HostContext): void;
41
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-workbench",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Explorer, Monaco editor, search and local Git overlay for DSH",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,10 +56,15 @@
56
56
  "dependencies": {
57
57
  "@vscode/codicons": "^0.0.36",
58
58
  "@vscode/ripgrep": "^1.15.14",
59
+ "@xterm/addon-fit": "^0.11.0",
60
+ "@xterm/xterm": "^5.5.0",
59
61
  "chokidar": "^4.0.3",
62
+ "docx-preview": "^0.4.0",
60
63
  "dompurify": "^3.2.6",
61
64
  "marked": "^16.3.0",
62
65
  "monaco-editor": "^0.52.2",
66
+ "node-pty": "^1.1.0",
67
+ "pdfjs-dist": "4.10.38",
63
68
  "ws": "^8.18.0"
64
69
  },
65
70
  "peerDependencies": {
@@ -2,7 +2,7 @@ export type CodiconName =
2
2
  | 'add' | 'check' | 'check-all' | 'chevron-down' | 'chevron-right' | 'close' | 'close-all' | 'cloud-upload' | 'code' | 'color-mode' | 'edit'
3
3
  | 'diff' | 'diff-sidebyside' | 'diff-single' | 'discard' | 'file' | 'file-code' | 'file-media' | 'file-pdf' | 'file-zip'
4
4
  | 'files' | 'folder' | 'folder-opened' | 'git-branch' | 'git-commit' | 'graph' | 'home' | 'list-tree' | 'new-file'
5
- | 'layout-sidebar-right' | 'layout-sidebar-right-off' | 'new-folder' | 'preview' | 'refresh' | 'remove' | 'search' | 'settings' | 'source-control' | 'sparkle' | 'trash'
5
+ | 'layout-sidebar-right' | 'layout-sidebar-right-off' | 'new-folder' | 'preview' | 'refresh' | 'remove' | 'search' | 'settings' | 'source-control' | 'sparkle' | 'split-horizontal' | 'terminal' | 'trash'
6
6
 
7
7
  /** Render one glyph from Microsoft's VS Code Codicon font. */
8
8
  export function Icon({ name, className = '' }: { name: CodiconName; className?: string }) {
@@ -0,0 +1,90 @@
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
2
+ import { FitAddon } from '@xterm/addon-fit'
3
+ import { Terminal } from '@xterm/xterm'
4
+ import { Icon } from './Icon.tsx'
5
+ import type { BottomPanelProps } from './service.ts'
6
+ import { isTerminalProtocolReply, TerminalWorkspaceController } from './terminal.ts'
7
+
8
+ function terminalTheme(colorScheme: 'dark' | 'light') { return colorScheme === 'dark' ? { background: '#131417', foreground: '#f1f3f5', cursor: '#c2c6cc', selectionBackground: '#355d94' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff' } }
9
+
10
+ function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: 'dark' | 'light' }) {
11
+ const root = useRef<HTMLDivElement>(null)
12
+ const terminalRef = useRef<Terminal>()
13
+ const replaying = useRef(false)
14
+ const replayGuard = useRef<ReturnType<typeof setTimeout>>()
15
+ const [error, setError] = useState<string>()
16
+ useEffect(() => {
17
+ if (root.current === null) return
18
+ const terminal = new Terminal({ convertEol: false, cursorBlink: true, fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Consolas,monospace', fontSize: 13, theme: terminalTheme(colorScheme) })
19
+ terminalRef.current = terminal
20
+ const fit = new FitAddon()
21
+ terminal.loadAddon(fit)
22
+ terminal.open(root.current)
23
+ const connection = workspace.connect(id)
24
+ const output = connection.subscribe(value => {
25
+ if (value.replay) { replaying.current = true; clearTimeout(replayGuard.current) }
26
+ terminal.write(value.data, () => { if (value.replay) replayGuard.current = setTimeout(() => { replaying.current = false }, 250) })
27
+ })
28
+ const input = terminal.onData(data => { if (!replaying.current || !isTerminalProtocolReply(data)) connection.write(data) })
29
+ const resize = terminal.onResize(({ cols, rows }) => connection.resize(cols, rows))
30
+ const observer = new ResizeObserver(() => { try { fit.fit() } catch { /* The panel can be hidden during layout changes. */ } })
31
+ observer.observe(root.current)
32
+ try { fit.fit(); terminal.focus() } catch (exception) { setError(exception instanceof Error ? exception.message : String(exception)) }
33
+ return () => { terminalRef.current = undefined; clearTimeout(replayGuard.current); observer.disconnect(); input.dispose(); resize.dispose(); output(); connection.close(); terminal.dispose() }
34
+ }, [workspace, id])
35
+ useEffect(() => { if (terminalRef.current !== undefined) terminalRef.current.options.theme = terminalTheme(colorScheme) }, [colorScheme])
36
+ return error === undefined ? <div ref={root} className="dvw-terminal-host" /> : <div className="dvw-terminal-error">{error}</div>
37
+ }
38
+
39
+ /** Render interactive terminal tabs in the workbench bottom panel. */
40
+ export function TerminalPanel({ sessionId, colorScheme }: BottomPanelProps) {
41
+ const workspaceRef = useRef<{ sessionId: string; value: TerminalWorkspaceController }>()
42
+ if (workspaceRef.current?.sessionId !== sessionId) workspaceRef.current = { sessionId, value: new TerminalWorkspaceController(sessionId) }
43
+ const workspace = workspaceRef.current.value
44
+ const snapshot = useSyncExternalStore(workspace.subscribe.bind(workspace), workspace.getSnapshot.bind(workspace))
45
+ const [active, setActive] = useState<string>()
46
+ const [context, setContext] = useState<{ id: string; x: number; y: number }>()
47
+ const [renaming, setRenaming] = useState<{ id: string; value: string }>()
48
+ const [split, setSplit] = useState<readonly string[]>()
49
+ const [sideWidth, setSideWidth] = useState(220)
50
+ useEffect(() => {
51
+ let cancelled = false
52
+ void workspace.refresh().then(() => { if (!cancelled && workspace.getSnapshot().terminals.length === 0) void workspace.create().then(value => { if (!cancelled) setActive(value.id) }) })
53
+ return () => { cancelled = true }
54
+ }, [workspace])
55
+ useEffect(() => { if (snapshot.terminals.length > 0 && (!active || !snapshot.terminals.some(value => value.id === active))) setActive(snapshot.terminals[0]?.id) }, [snapshot.terminals, active])
56
+ useEffect(() => { const create = () => { void workspace.create().then(value => setActive(value.id)) }; window.addEventListener('dvw-terminal-new', create); return () => window.removeEventListener('dvw-terminal-new', create) }, [workspace])
57
+ useEffect(() => {
58
+ if (context === undefined) return
59
+ const close = () => setContext(undefined)
60
+ const escape = (event: KeyboardEvent) => { if (event.key === 'Escape') close() }
61
+ window.addEventListener('pointerdown', close)
62
+ window.addEventListener('keydown', escape)
63
+ return () => { window.removeEventListener('pointerdown', close); window.removeEventListener('keydown', escape) }
64
+ }, [context])
65
+ const closeTerminal = (id: string) => { setContext(undefined); setSplit(current => current?.filter(value => value !== id)); void workspace.close(id) }
66
+ const saveRename = (id: string) => { const title = renaming?.id === id ? renaming.value.trim() : ''; setRenaming(undefined); if (title) void workspace.rename(id, title) }
67
+ const splitTerminal = (first = active) => { if (first === undefined) return; void workspace.create().then(value => { setSplit(current => [...(current?.includes(first) ? current : [first]), value.id]); setActive(value.id) }) }
68
+ const shown = (split ?? (active === undefined ? [] : [active])).filter(id => snapshot.terminals.some(value => value.id === id))
69
+ const selectTerminal = (id: string) => { setActive(id); setSplit(current => current?.includes(id) ? current : [id]) }
70
+ return <div className="dvw-terminal-root">
71
+ <div className="dvw-terminal-main">
72
+ {snapshot.error !== undefined && <div className="dvw-terminal-error">{snapshot.error}</div>}
73
+ <div className="dvw-terminal-panes" data-split={shown.length > 1 || undefined}>{shown.map(id => <XtermView key={id} workspace={workspace} id={id} colorScheme={colorScheme} />)}</div>
74
+ </div>
75
+ <div className="dvw-terminal-resizer" role="separator" aria-label="终端列表宽度" aria-orientation="vertical" onPointerDown={event => {
76
+ if (event.button !== 0) return
77
+ event.preventDefault()
78
+ const startX = event.clientX
79
+ const startWidth = sideWidth
80
+ const move = (next: PointerEvent) => setSideWidth(Math.max(160, Math.min(420, Math.round(startWidth + startX - next.clientX))))
81
+ const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
82
+ window.addEventListener('pointermove', move)
83
+ window.addEventListener('pointerup', up)
84
+ }}/>
85
+ <aside className="dvw-terminal-tabs" style={{ width: sideWidth }} aria-label="终端会话">
86
+ <div className="dvw-terminal-list">{snapshot.terminals.map(value => renaming?.id === value.id ? <div key={value.id} className="dvw-terminal-entry" data-active><div className="dvw-terminal-tab"><Icon name="terminal"/><input className="dvw-terminal-rename" value={renaming.value} autoFocus aria-label="终端名称" onChange={event => setRenaming({ id: value.id, value: event.target.value })} onKeyDown={event => { if (event.key === 'Enter') saveRename(value.id); if (event.key === 'Escape') setRenaming(undefined) }} onBlur={() => saveRename(value.id)}/></div></div> : <div key={value.id} className="dvw-terminal-entry" data-active={active === value.id} data-split-member={shown.includes(value.id) || undefined}><button className="dvw-terminal-tab" onClick={() => selectTerminal(value.id)} onContextMenu={event => { event.preventDefault(); selectTerminal(value.id); setContext({ id: value.id, x: event.clientX, y: event.clientY }) }}><Icon name="terminal"/>{value.title}</button><div className="dvw-terminal-entry-actions"><button title="拆分终端" onClick={() => splitTerminal(value.id)}><Icon name="split-horizontal"/></button><button title="终止终端" onClick={() => closeTerminal(value.id)}><Icon name="trash"/></button></div></div>)}</div>
87
+ {context !== undefined && <div className="dvw-terminal-menu" role="menu" style={{ left: context.x, top: context.y }} onPointerDown={event => event.stopPropagation()}><button role="menuitem" onClick={() => { const terminal = snapshot.terminals.find(value => value.id === context.id); setContext(undefined); if (terminal !== undefined) setRenaming({ id: terminal.id, value: terminal.title }) }}>重命名</button><button className="dvw-terminal-menu-danger" role="menuitem" onClick={() => closeTerminal(context.id)}>终止终端</button></div>}
88
+ </aside>
89
+ </div>
90
+ }
@@ -9,9 +9,11 @@ import type { MarkdownHeading } from './MarkdownPreview.tsx'
9
9
  import { MonacoDiffEditor, MonacoEditor } from './MonacoEditor.tsx'
10
10
  import { DshConversationPanel, DshConversationTheme } from './DshConversationPanel.tsx'
11
11
  import { DshMark } from './DshMark.tsx'
12
+ import { DocumentPreview, documentPreviewKind, type DocumentPreviewKind } from './documentPreview.tsx'
13
+ import { toggleTerminal } from './shortcuts.ts'
12
14
  import { resetWorkspaceDocuments, WorkspaceRequestScope } from './workspaceState.ts'
13
15
 
14
- interface Tab extends FileDocument { dirty: boolean; draft: string; loaded?: boolean; line?: number; column?: number; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string }
16
+ interface Tab extends FileDocument { dirty: boolean; draft: string; loaded?: boolean; line?: number; column?: number; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string; documentPreview?:DocumentPreviewKind; documentUrl?:string }
15
17
  type WorkbenchTheme = 'dsh' | 'dark' | 'light'
16
18
  interface TreeProps { sessionId: string; path: string; depth: number; expanded: Set<string>; selected?: string; selectedDirectory: string; statuses: Map<string, string>; onToggle(path: string): void; onSelectDirectory(path: string): void; onOpen(path: string): void; onChange(): void; onUpload(directory: string, files: FileList): void }
17
19
  interface CreatingEntry { kind: 'file' | 'directory'; directory: string; value: string; error?: string }
@@ -298,13 +300,13 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
298
300
  useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const epoch=workspaceRequests.current.begin();const cleared=resetWorkspaceDocuments<Tab>();loadingTabs.current.clear();setWorkspace(undefined);setTabs(cleared.tabs);tabsRef.current=cleared.tabs;setActive(cleared.active);setError(cleared.error);const controller=new AbortController();void api<{cwd:string;name:string}>('workspace',{sessionId},controller.signal).then(value=>{if(!workspaceRequests.current.isCurrent(epoch))return;setWorkspace(value);const saved=localStorage.getItem(`dvw:${value.cwd}`);if(saved){try{const parsed=JSON.parse(saved) as {view?:WorkbenchView;paths?:string[];active?:string;panelHeight?:number;sideWidth?:number};if(parsed.view)service.show(parsed.view);if(typeof parsed.panelHeight==='number')setPanelHeight(parsed.panelHeight);if(typeof parsed.sideWidth==='number')setSideWidth(Math.max(210,Math.min(640,Math.round(parsed.sideWidth))));const restored=restoreTabPaths(parsed.paths??[]);setTabs(restored);setActive(restored.some(tab=>tab.path===parsed.active)?parsed.active:restored.at(-1)?.path)}catch{}}}).catch(error=>{if(!controller.signal.aborted&&workspaceRequests.current.isCurrent(epoch))setError(String(error))});return()=>{controller.abort();loadingTabs.current.clear();workspaceRequests.current.invalidate(epoch)}},[snapshot.visible,sessionId])
299
301
  useEffect(()=>{if(workspace) localStorage.setItem(`dvw:${workspace.cwd}`,JSON.stringify({view:snapshot.view,paths:tabs.filter(t=>!t.path.startsWith('diff:')).map(t=>t.path),active,panelHeight,sideWidth}))},[workspace,snapshot.view,tabs,active,panelHeight,sideWidth])
300
302
  useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const protocol=location.protocol==='https:'?'wss':'ws';const ws=new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);ws.onmessage=event=>{let paths:string[]=[];try{paths=(JSON.parse(String(event.data)) as {paths?:string[]}).paths??[]}catch{}window.dispatchEvent(new CustomEvent('dvw-files-changed',{detail:paths}));setTabs(current=>current.map(tab=>tab.dirty?{...tab,external:true}:tab))};return()=>ws.close()},[snapshot.visible,sessionId])
301
- useEffect(()=>{if(sessionId===undefined||active===undefined)return;const tab=tabsRef.current.find(row=>row.path===active);if(tab===undefined||tab.loaded!==false||loadingTabs.current.has(active))return;const epoch=workspaceRequests.current.current();loadingTabs.current.add(active);if(/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(active)){const imageUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;setTabs(rows=>rows.map(row=>row.path===active?{...row,loaded:true,imageUrl}:row));loadingTabs.current.delete(active);return}void api<FileDocument>('fs.read',{sessionId,path:active}).then(doc=>{if(!workspaceRequests.current.isCurrent(epoch))return;if(doc.binary||doc.tooLarge){setTabs(rows=>rows.filter(row=>row.path!==active));setActive(current=>current===active?undefined:current);setError(doc.binary?'二进制文件不能在文本编辑器中打开':`文件超过 5 MiB:${active}`);return}setTabs(rows=>rows.map(row=>row.path===active?{...row,...doc,draft:doc.content,loaded:true}:row))}).catch(error=>{if(workspaceRequests.current.isCurrent(epoch))setError(String(error))}).finally(()=>loadingTabs.current.delete(active))},[active,sessionId,tabs])
303
+ useEffect(()=>{if(sessionId===undefined||active===undefined)return;const tab=tabsRef.current.find(row=>row.path===active);if(tab===undefined||tab.loaded!==false||loadingTabs.current.has(active))return;const epoch=workspaceRequests.current.current();loadingTabs.current.add(active);const documentPreview=documentPreviewKind(active);if(documentPreview!==undefined){const documentUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;setTabs(rows=>rows.map(row=>row.path===active?{...row,loaded:true,documentPreview,documentUrl}:row));loadingTabs.current.delete(active);return}if(/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(active)){const imageUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;setTabs(rows=>rows.map(row=>row.path===active?{...row,loaded:true,imageUrl}:row));loadingTabs.current.delete(active);return}void api<FileDocument>('fs.read',{sessionId,path:active}).then(doc=>{if(!workspaceRequests.current.isCurrent(epoch))return;if(doc.binary||doc.tooLarge){setTabs(rows=>rows.filter(row=>row.path!==active));setActive(current=>current===active?undefined:current);setError(doc.binary?'二进制文件不能在文本编辑器中打开':`文件超过 5 MiB:${active}`);return}setTabs(rows=>rows.map(row=>row.path===active?{...row,...doc,draft:doc.content,loaded:true}:row))}).catch(error=>{if(workspaceRequests.current.isCurrent(epoch))setError(String(error))}).finally(()=>loadingTabs.current.delete(active))},[active,sessionId,tabs])
302
304
  const open=(path:string,line?:number,column?:number)=>{if(sessionId!==undefined)openForWorkspace(path,line,column)}
303
305
  useEffect(()=>{const listener=(event:Event)=>void open((event as CustomEvent<string>).detail);window.addEventListener('dvw-open-workspace-path',listener);return()=>window.removeEventListener('dvw-open-workspace-path',listener)},[sessionId,tabs])
304
306
  const save=async(tab:Tab)=>{if(sessionId===undefined||!tab.dirty)return;try{const doc=await api<FileDocument>('fs.save',{sessionId,path:tab.path,content:tab.draft,revision:tab.revision});setTabs(rows=>rows.map(row=>row.path===tab.path?{...row,...doc,draft:doc.content,dirty:false,external:false}:row))}catch(error){setError(String(error))}}
305
307
  const close=(tab:Tab)=>{if(tab.dirty&&!confirm(`${tab.path} 有未保存更改,确定关闭?`))return;setTabs(rows=>rows.filter(row=>row.path!==tab.path));if(active===tab.path)setActive(tabs.filter(row=>row.path!==tab.path).at(-1)?.path)}
306
308
  const closeAll=()=>{const dirty=tabs.filter(tab=>tab.dirty);if(dirty.length>0&&!confirm(`有 ${dirty.length} 个文件包含未保存更改,确定全部关闭?`))return;setTabs([]);setActive(undefined)}
307
- const activeTab=tabs.find(tab=>tab.path===active); const Panel=panels.find(panel=>panel.id===snapshot.bottomPanel)?.component
309
+ const activeTab=tabs.find(tab=>tab.path===active); const Panel=panels.find(panel=>panel.id===snapshot.bottomPanel)?.component; const terminalAvailable=panels.some(panel=>panel.id==='terminal')
308
310
  const selectView=(view:WorkbenchView)=>{if(snapshot.view===view&&primarySideVisible){setPrimarySideVisible(false);return}setPrimarySideVisible(true);service.show(view)}
309
311
  const selectHeading=(index:number)=>{if(activeTab===undefined)return;setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab));window.setTimeout(()=>window.dispatchEvent(new CustomEvent('dvw-markdown-heading',{detail:index})),50)}
310
312
  const suppressAttachmentDrop=(event:DragEvent)=>{if(!event.dataTransfer.types.includes('Files'))return;const target=event.target;const insideTree=target instanceof Element&&target.closest('.dvw-tree')!==null;if(!insideTree){event.preventDefault();event.stopPropagation()}}
@@ -320,11 +322,11 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
320
322
  {!chatVisible&&<button className="dvw-chat-toggle dvw-icon" title="显示 DSH 对话栏 (Ctrl+Alt+B)" onClick={()=>setChatVisible(true)}><DshMark/></button>}
321
323
  {chatVisible&&<aside className="dvw-chat"><PanelResizer className="dvw-chat-resizer" label="拖动调整 DSH 对话栏宽度" width={chatWidth} min={400} max={720} defaultWidth={420} direction={-1} onWidth={setChatWidth}/><header className="dvw-chat-head"><span><DshMark/> DSH</span><button className="dvw-icon" title="关闭 DSH 对话栏" onClick={()=>setChatVisible(false)}><Icon name="close"/></button></header><div className="dvw-chat-body"/></aside>}
322
324
  {settingsOpen&&<div className="dvw-settings-backdrop" role="presentation" onMouseDown={()=>setSettingsOpen(false)}><section className="dvw-settings" role="dialog" aria-modal="true" aria-label="工作台设置" onMouseDown={event=>event.stopPropagation()}><header><span><Icon name="settings"/> 工作台设置</span><button className="dvw-icon" title="关闭设置" onClick={()=>setSettingsOpen(false)}><Icon name="close"/></button></header><div className="dvw-settings-body"><section><h3>外观</h3><label>颜色主题</label><div className="dvw-settings-themes">{([['dsh','跟随 DSH'],['dark','深色'],['light','浅色']] as const).map(([value,label])=><button key={value} data-active={theme===value} onClick={()=>setTheme(value)}><span className={`dvw-theme-swatch ${value}`}/><span>{label}</span>{theme===value&&<Icon name="check"/>}</button>)}</div></section><section><h3>布局</h3><label className="dvw-setting-check"><input type="checkbox" checked={primarySideVisible} onChange={event=>setPrimarySideVisible(event.target.checked)}/><span>显示左侧视图</span></label><label className="dvw-setting-check"><input type="checkbox" checked={chatVisible} onChange={event=>setChatVisible(event.target.checked)}/><span>显示 DSH 对话栏</span></label><label className="dvw-setting-check"><input type="checkbox" checked={statusVisible} onChange={event=>setStatusVisible(event.target.checked)}/><span>显示底部状态栏</span></label><label className="dvw-setting-range"><span>左侧视图宽度</span><input type="range" min="210" max="640" step="1" value={Math.round(sideWidth)} onChange={event=>setSideWidth(Math.round(Number(event.target.value)))}/><output>{sideWidth}px</output></label><label className="dvw-setting-range"><span>DSH 对话栏宽度</span><input type="range" min="400" max="720" step="1" value={Math.round(chatWidth)} onChange={event=>setChatWidth(Math.max(400,Math.round(Number(event.target.value))))}/><output>{chatWidth}px</output></label></section></div></section></div>}
323
- <nav className="dvw-activity"><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='explorer'} title="资源管理器" onClick={()=>selectView('explorer')}><Icon name="files"/></button><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='search'} title="搜索" onClick={()=>selectView('search')}><Icon name="search"/></button><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='git'} title="源代码管理" onClick={()=>selectView('git')}><Icon name="source-control"/></button><button className="dvw-act dsh" title="返回 DSH" onClick={()=>service.hide()}><Icon name="home"/></button><button className="dvw-act dvw-settings-button" data-active={settingsOpen} title="管理工作台设置" onClick={()=>setSettingsOpen(true)}><Icon name="settings"/></button></nav>
325
+ <nav className="dvw-activity"><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='explorer'} title="资源管理器" onClick={()=>selectView('explorer')}><Icon name="files"/></button><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='search'} title="搜索" onClick={()=>selectView('search')}><Icon name="search"/></button><button className="dvw-act" data-active={primarySideVisible&&snapshot.view==='git'} title="源代码管理" onClick={()=>selectView('git')}><Icon name="source-control"/></button><button className="dvw-act" data-active={snapshot.bottomPanel==='terminal'} disabled={!terminalAvailable} title={terminalAvailable?(snapshot.bottomPanel==='terminal'?'隐藏终端':'显示终端'):'终端适配器未安装'} onClick={()=>toggleTerminal(service)}><Icon name="terminal"/></button><button className="dvw-act dsh" title="返回 DSH" onClick={()=>service.hide()}><Icon name="home"/></button><button className="dvw-act dvw-settings-button" data-active={settingsOpen} title="管理工作台设置" onClick={()=>setSettingsOpen(true)}><Icon name="settings"/></button></nav>
324
326
  <aside className="dvw-side" data-side-width={sideWidth}>{snapshot.view==='explorer'&&<Explorer sessionId={sessionId} cwd={workspace?.cwd} selected={active} onOpen={path=>void open(path)}/>} {snapshot.view==='search'&&<Search sessionId={sessionId} onOpen={(path,line,column)=>void open(path,line,column)}/>} {snapshot.view==='git'&&<Git sessionId={sessionId} onDiff={(path,diff,title,diffSourcePath)=>{const key=`diff:${path}`;setTabs(rows=>[...rows.filter(row=>row.path!==key),{path:key,title,content:diff.diff,draft:diff.diff,revision:'',size:diff.diff.length,binary:false,tooLarge:false,dirty:false,diff,diffMode:'inline',diffSourcePath}]);setActive(key)}}/>}</aside>
325
327
  <PanelResizer className="dvw-side-resizer" label="拖动调整源码管理侧边栏宽度" width={sideWidth} min={210} max={640} defaultWidth={210} direction={1} onWidth={setSideWidth}/>
326
- <main className="dvw-main"><div className="dvw-tabs"><div className="dvw-tab-list">{tabs.map(tab=><button className="dvw-tab" data-active={tab.path===active} key={tab.path} title={tab.diffSourcePath??tab.path} onClick={()=>setActive(tab.path)}><span className="dvw-tab-name">{tab.dirty?'● ':''}{tabLabel(tab)}</span><span onClick={event=>{event.stopPropagation();close(tab)}}><Icon name="close"/></span></button>)}</div>{tabs.length>0&&<div className="dvw-tab-actions"><button className="dvw-icon dvw-close-all" title="全部关闭" onClick={closeAll}><Icon name="close-all"/></button></div>}</div><div className="dvw-editor" data-outline={markdownPreview&&outlineVisible&&headings.length>0}>{error&&<div className="dvw-error"><button className="dvw-icon" onClick={()=>setError(undefined)}><Icon name="close"/></button>{error}</div>}{markdownPreview&&outlineVisible&&headings.length>0&&<MarkdownOutline headings={headings} onClose={()=>setOutlineVisible(false)} onHeading={selectHeading}/>} {markdown&&activeTab&&<div className="dvw-editor-toolbar"><button className="dvw-icon dvw-outline-toggle" data-active={outlineVisible} title={outlineVisible?'关闭 Markdown 大纲':'显示 Markdown 大纲'} onClick={()=>setOutlineVisible(value=>!value)}><Icon name="list-tree"/></button><button className="dvw-mode-button" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="code"/> 源码</button><button className="dvw-mode-button" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{activeTab?.diff&&<div className="dvw-editor-toolbar dvw-diff-toolbar">{diffMarkdown&&<div className="dvw-diff-preview-switch"><button className="dvw-mode-button" data-diff-view="diff" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="diff"/> 差异</button><button className="dvw-mode-button" data-diff-view="preview" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{!activeTab.preview&&<><button className="dvw-mode-button dvw-diff-mode-button" data-mode="inline" data-active={activeTab.diffMode!=='split'} title="内联视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'inline'}:tab))}><Icon name="diff-single"/> 内联</button><button className="dvw-mode-button dvw-diff-mode-button" data-mode="split" data-active={activeTab.diffMode==='split'} title="并排视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'split'}:tab))}><Icon name="diff-sidebyside"/> 并排</button></>}</div>}<div className="dvw-editor-body" data-toolbar={markdown||activeTab?.diff!==undefined}>{activeTab===undefined?<div className="dvw-empty"><div><strong>DSH Workbench</strong><br/>从资源管理器或搜索结果打开文件</div></div>:activeTab.diff?diffMarkdown&&activeTab.preview?<MarkdownPreview source={activeTab.diff.modified} sessionId={sessionId} path={activeTab.diffSourcePath??''}/>:<MonacoDiffEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} original={activeTab.diff.original} modified={activeTab.diff.modified} mode={activeTab.diffMode??'inline'} theme={colorScheme}/>:activeTab.imageUrl?<div className="dvw-image-preview"><img src={activeTab.imageUrl} alt={activeTab.path}/><span>{activeTab.path}</span></div>:markdown&&activeTab.preview?<MarkdownPreview source={activeTab.draft} sessionId={sessionId} path={activeTab.path}/>:<MonacoEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} value={activeTab.draft} theme={colorScheme} line={activeTab.line} column={activeTab.column} onChange={draft=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,draft,dirty:draft!==tab.content}:tab))} onSave={()=>void save(activeTab)}/>}</div></div></main>
327
- {snapshot.bottomPanel&&<section className="dvw-bottom" style={{height:panelHeight}}><div style={{height:4,cursor:'ns-resize'}} onPointerDown={event=>{const startY=event.clientY,startHeight=panelHeight;const move=(e:PointerEvent)=>setPanelHeight(Math.max(120,Math.min(window.innerHeight-160,startHeight+startY-e.clientY)));const up=()=>{window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up)};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up)}}/><div className="dvw-bottom-head"><button className="dvw-bottom-title">{panels.find(p=>p.id===snapshot.bottomPanel)?.title??snapshot.bottomPanel}</button><button className="dvw-icon" style={{marginLeft:'auto'}} onClick={()=>service.toggleBottomPanel(snapshot.bottomPanel!)}><Icon name="close"/></button></div><div className="dvw-bottom-body">{Panel&&cwd&&<Panel sessionId={sessionId} cwd={cwd} visible colorScheme={colorScheme}/>}</div></section>}
328
+ <main className="dvw-main"><div className="dvw-tabs"><div className="dvw-tab-list">{tabs.map(tab=><button className="dvw-tab" data-active={tab.path===active} key={tab.path} title={tab.diffSourcePath??tab.path} onClick={()=>setActive(tab.path)}><span className="dvw-tab-name">{tab.dirty?'● ':''}{tabLabel(tab)}</span><span onClick={event=>{event.stopPropagation();close(tab)}}><Icon name="close"/></span></button>)}</div>{tabs.length>0&&<div className="dvw-tab-actions"><button className="dvw-icon dvw-close-all" title="全部关闭" onClick={closeAll}><Icon name="close-all"/></button></div>}</div><div className="dvw-editor" data-outline={markdownPreview&&outlineVisible&&headings.length>0}>{error&&<div className="dvw-error"><button className="dvw-icon" onClick={()=>setError(undefined)}><Icon name="close"/></button>{error}</div>}{markdownPreview&&outlineVisible&&headings.length>0&&<MarkdownOutline headings={headings} onClose={()=>setOutlineVisible(false)} onHeading={selectHeading}/>} {markdown&&activeTab&&<div className="dvw-editor-toolbar"><button className="dvw-icon dvw-outline-toggle" data-active={outlineVisible} title={outlineVisible?'关闭 Markdown 大纲':'显示 Markdown 大纲'} onClick={()=>setOutlineVisible(value=>!value)}><Icon name="list-tree"/></button><button className="dvw-mode-button" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="code"/> 源码</button><button className="dvw-mode-button" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{activeTab?.diff&&<div className="dvw-editor-toolbar dvw-diff-toolbar">{diffMarkdown&&<div className="dvw-diff-preview-switch"><button className="dvw-mode-button" data-diff-view="diff" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="diff"/> 差异</button><button className="dvw-mode-button" data-diff-view="preview" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{!activeTab.preview&&<><button className="dvw-mode-button dvw-diff-mode-button" data-mode="inline" data-active={activeTab.diffMode!=='split'} title="内联视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'inline'}:tab))}><Icon name="diff-single"/> 内联</button><button className="dvw-mode-button dvw-diff-mode-button" data-mode="split" data-active={activeTab.diffMode==='split'} title="并排视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'split'}:tab))}><Icon name="diff-sidebyside"/> 并排</button></>}</div>}<div className="dvw-editor-body" data-toolbar={markdown||activeTab?.diff!==undefined}>{activeTab===undefined?<div className="dvw-empty"><div><strong>DSH Workbench</strong><br/>从资源管理器或搜索结果打开文件</div></div>:activeTab.diff?diffMarkdown&&activeTab.preview?<MarkdownPreview source={activeTab.diff.modified} sessionId={sessionId} path={activeTab.diffSourcePath??''}/>:<MonacoDiffEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} original={activeTab.diff.original} modified={activeTab.diff.modified} mode={activeTab.diffMode??'inline'} theme={colorScheme}/>:activeTab.imageUrl?<div className="dvw-image-preview"><img src={activeTab.imageUrl} alt={activeTab.path}/><span>{activeTab.path}</span></div>:activeTab.documentPreview&&activeTab.documentUrl?<DocumentPreview kind={activeTab.documentPreview} src={activeTab.documentUrl} path={activeTab.path}/>:markdown&&activeTab.preview?<MarkdownPreview source={activeTab.draft} sessionId={sessionId} path={activeTab.path}/>:<MonacoEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} value={activeTab.draft} theme={colorScheme} line={activeTab.line} column={activeTab.column} onChange={draft=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,draft,dirty:draft!==tab.content}:tab))} onSave={()=>void save(activeTab)}/>}</div></div></main>
329
+ {snapshot.bottomPanel&&<section className="dvw-bottom" style={{height:panelHeight}}><div style={{height:4,cursor:'ns-resize'}} onPointerDown={event=>{const startY=event.clientY,startHeight=panelHeight;const move=(e:PointerEvent)=>setPanelHeight(Math.max(120,Math.min(window.innerHeight-160,startHeight+startY-e.clientY)));const up=()=>{window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up)};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up)}}/><div className="dvw-bottom-head"><button className="dvw-bottom-title">{panels.find(p=>p.id===snapshot.bottomPanel)?.title??snapshot.bottomPanel}</button>{snapshot.bottomPanel==='terminal'&&<button className="dvw-icon" title="新建终端" onClick={()=>window.dispatchEvent(new Event('dvw-terminal-new'))}><Icon name="add"/></button>}<button className="dvw-icon" style={{marginLeft:'auto'}} onClick={()=>service.toggleBottomPanel(snapshot.bottomPanel!)}><Icon name="close"/></button></div><div className="dvw-bottom-body">{Panel&&cwd&&<Panel sessionId={sessionId} cwd={cwd} visible colorScheme={colorScheme}/>}</div></section>}
328
330
  <footer className="dvw-status"><span><Icon name="git-branch"/> 工作区</span><span>{workspace?.cwd??cwd??''}</span>{activeTab?.external&&<span>磁盘文件已变化</span>}<button className="dvw-icon" style={{color:'white',marginLeft:'auto'}} onClick={()=>service.hide()}>返回 DSH</button></footer>
329
331
  </div>
330
332
  }
@@ -0,0 +1,122 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ export type DocumentPreviewKind = 'pdf' | 'docx' | 'download'
4
+
5
+ const MIN_PDF_SCALE = 0.5
6
+ const MAX_PDF_SCALE = 3
7
+
8
+ /** Bound PDF zoom to values that remain usable in the workbench viewport. */
9
+ export function clampPdfScale(scale: number): number {
10
+ return Math.max(MIN_PDF_SCALE, Math.min(MAX_PDF_SCALE, Math.round(scale * 100) / 100))
11
+ }
12
+
13
+ /** Return the in-workbench preview supported for a document path. */
14
+ export function documentPreviewKind(path: string): DocumentPreviewKind | undefined {
15
+ const extension = path.split('.').at(-1)?.toLowerCase()
16
+ if (extension === 'pdf') return 'pdf'
17
+ if (extension === 'docx') return 'docx'
18
+ if (extension === 'doc' || extension === 'odt') return 'download'
19
+ return undefined
20
+ }
21
+
22
+ function DocxPreview({ src }: { src: string }) {
23
+ const root = useRef<HTMLDivElement>(null)
24
+ const [error, setError] = useState<string>()
25
+ useEffect(() => {
26
+ const controller = new AbortController()
27
+ const target = root.current
28
+ if (target === null) return
29
+ setError(undefined)
30
+ void (async () => {
31
+ try {
32
+ const response = await fetch(src, { signal: controller.signal })
33
+ if (!response.ok) throw new Error(`无法读取文档 (${response.status})`)
34
+ const { renderAsync } = await import('docx-preview')
35
+ if (controller.signal.aborted) return
36
+ target.replaceChildren()
37
+ await renderAsync(await response.blob(), target, undefined, { inWrapper: false, ignoreWidth: false, ignoreHeight: false })
38
+ } catch (cause) {
39
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause))
40
+ }
41
+ })()
42
+ return () => controller.abort()
43
+ }, [src])
44
+ return error === undefined ? <div ref={root} className="dvw-docx-preview"/> : <div className="dvw-document-error">无法预览 Word 文档:{error}</div>
45
+ }
46
+
47
+ type PdfDocument = {
48
+ numPages: number
49
+ getPage(page: number): Promise<{ getViewport(options: { scale: number }): { width: number; height: number }; render(options: { canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }): { promise: Promise<void>; cancel(): void } }>
50
+ destroy(): void
51
+ }
52
+
53
+ function PdfPreview({ src, path }: { src: string; path: string }) {
54
+ const canvas = useRef<HTMLCanvasElement>(null)
55
+ const documentRef = useRef<PdfDocument>()
56
+ const [page, setPage] = useState(1)
57
+ const [pages, setPages] = useState(0)
58
+ const [scale, setScale] = useState(1)
59
+ const [error, setError] = useState<string>()
60
+
61
+ useEffect(() => {
62
+ let cancelled = false
63
+ let document: PdfDocument | undefined
64
+ setPage(1); setPages(0); setError(undefined)
65
+ void (async () => {
66
+ try {
67
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs')
68
+ pdfjs.GlobalWorkerOptions.workerSrc = '/dsh-vscode/pdfjs/worker.mjs'
69
+ document = await pdfjs.getDocument(src).promise as PdfDocument
70
+ if (cancelled) { document.destroy(); return }
71
+ documentRef.current = document
72
+ setPages(document.numPages)
73
+ } catch (cause) {
74
+ if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause))
75
+ }
76
+ })()
77
+ return () => { cancelled = true; documentRef.current = undefined; document?.destroy() }
78
+ }, [src])
79
+
80
+ useEffect(() => {
81
+ const document = documentRef.current
82
+ const target = canvas.current
83
+ if (document === undefined || target === null) return
84
+ let cancelled = false
85
+ let task: { promise: Promise<void>; cancel(): void } | undefined
86
+ void document.getPage(page).then(value => {
87
+ if (cancelled) return
88
+ const viewport = value.getViewport({ scale })
89
+ const ratio = window.devicePixelRatio || 1
90
+ target.width = Math.ceil(viewport.width * ratio)
91
+ target.height = Math.ceil(viewport.height * ratio)
92
+ target.style.width = `${viewport.width}px`
93
+ target.style.height = `${viewport.height}px`
94
+ const context = target.getContext('2d')
95
+ if (context === null) throw new Error('无法创建 PDF 画布')
96
+ context.setTransform(ratio, 0, 0, ratio, 0, 0)
97
+ task = value.render({ canvasContext: context, viewport })
98
+ return task.promise
99
+ }).catch(cause => { if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause)) })
100
+ return () => { cancelled = true; task?.cancel() }
101
+ }, [page, scale, pages])
102
+
103
+ return <section className="dvw-pdf-preview" aria-label={`${path} PDF 预览`}>
104
+ <header className="dvw-pdf-toolbar">
105
+ <button className="dvw-icon" title="上一页" disabled={page <= 1} onClick={() => setPage(value => Math.max(1, value - 1))}><span aria-hidden="true">‹</span></button>
106
+ <span>{pages === 0 ? '正在加载' : `${page} / ${pages}`}</span>
107
+ <button className="dvw-icon" title="下一页" disabled={pages === 0 || page >= pages} onClick={() => setPage(value => Math.min(pages, value + 1))}><span aria-hidden="true">›</span></button>
108
+ <span className="dvw-pdf-separator"/>
109
+ <button className="dvw-icon" title="缩小" disabled={scale <= MIN_PDF_SCALE} onClick={() => setScale(value => clampPdfScale(value - 0.1))}>−</button>
110
+ <button className="dvw-pdf-zoom" title="重置缩放" onClick={() => setScale(1)}>{Math.round(scale * 100)}%</button>
111
+ <button className="dvw-icon" title="放大" disabled={scale >= MAX_PDF_SCALE} onClick={() => setScale(value => clampPdfScale(value + 0.1))}>+</button>
112
+ </header>
113
+ {error === undefined ? <div className="dvw-pdf-page"><canvas ref={canvas}/></div> : <div className="dvw-document-error">无法预览 PDF:{error}</div>}
114
+ </section>
115
+ }
116
+
117
+ /** Render an in-workbench preview for PDF and Word documents. */
118
+ export function DocumentPreview({ kind, src, path }: { kind: DocumentPreviewKind; src: string; path: string }) {
119
+ if (kind === 'pdf') return <PdfPreview src={src} path={path}/>
120
+ if (kind === 'docx') return <DocxPreview src={src}/>
121
+ return <div className="dvw-document-download"><strong>此格式无法在浏览器中可靠渲染</strong><a href={src} target="_blank" rel="noreferrer">打开 {path}</a></div>
122
+ }
@@ -6,6 +6,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
6
6
  import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
7
7
  import type { VscodeWorkbench } from './service.ts'
8
8
  import { WorkbenchController } from './service.ts'
9
+ import { TerminalPanel } from './TerminalPanel.tsx'
9
10
  import { Launcher, Workbench } from './Workbench.tsx'
10
11
  import { installStyles } from './styles.ts'
11
12
  import { handleTerminalShortcut } from './shortcuts.ts'
@@ -32,6 +33,8 @@ export function apply(ctx: ClientContext): void {
32
33
  const service = new WorkbenchController()
33
34
  ctx.effect(() => installStyles(), 'dsh-vscode-workbench: styles')
34
35
  ctx.effect(() => { const dispose = ctx.reflect.provide('vscodeWorkbench', service); return () => { void dispose() } }, 'dsh-vscode-workbench: service')
36
+ ctx.effect(() => service.registerBottomPanel({ id: 'terminal', title: '终端', component: TerminalPanel }), 'dsh-vscode-workbench: terminal panel')
37
+ ctx.effect(() => service.registerCommand({ id: 'terminal.new', run() { window.dispatchEvent(new Event('dvw-terminal-new')) } }), 'dsh-vscode-workbench: terminal command')
35
38
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dsh-vscode-workbench-launcher', inject: () => ({ service }) }, Footer))
36
39
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-vscode-workbench-overlay', inject: () => ({ service }) }, Overlay))
37
40
  ctx.effect(() => {
@@ -1,5 +1,21 @@
1
1
  import type { VscodeWorkbench } from './service.ts'
2
2
 
3
+ /** Reveal the terminal panel supplied by a terminal adapter without toggling it closed. */
4
+ export function openTerminal(workbench: VscodeWorkbench): boolean {
5
+ if (!workbench.getPanels().some(panel => panel.id === 'terminal')) return false
6
+ workbench.show()
7
+ if (workbench.getSnapshot().bottomPanel !== 'terminal') workbench.toggleBottomPanel('terminal')
8
+ return true
9
+ }
10
+
11
+ /** Toggle the terminal panel from the activity bar. */
12
+ export function toggleTerminal(workbench: VscodeWorkbench): boolean {
13
+ if (!workbench.getPanels().some(panel => panel.id === 'terminal')) return false
14
+ workbench.show()
15
+ workbench.toggleBottomPanel('terminal')
16
+ return true
17
+ }
18
+
3
19
  /** Handle terminal shortcuts through contributions registered by a terminal adapter. */
4
20
  export function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean {
5
21
  if (event.isComposing || !event.ctrlKey || event.altKey || event.code !== 'Backquote') return false
@@ -8,7 +24,7 @@ export function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWo
8
24
  event.stopImmediatePropagation()
9
25
  workbench.show()
10
26
  if (event.shiftKey) {
11
- if (workbench.getSnapshot().bottomPanel !== 'terminal') workbench.toggleBottomPanel('terminal')
27
+ openTerminal(workbench)
12
28
  queueMicrotask(() => workbench.runCommand('terminal.new'))
13
29
  } else {
14
30
  workbench.toggleBottomPanel('terminal')
@@ -2,7 +2,7 @@ const CSS = String.raw`
2
2
  @font-face{font-family:codicon;font-display:block;src:url('/dsh-vscode/codicons/codicon.ttf') format('truetype')}
3
3
  .codicon{display:inline-block;font:normal normal normal 16px/1 codicon;text-align:center;text-decoration:none;text-rendering:auto;-webkit-font-smoothing:antialiased;user-select:none}
4
4
  .codicon-add:before{content:"\ea60"}.codicon-git-branch:before,.codicon-source-control:before{content:"\ea68"}.codicon-search:before{content:"\ea6d"}.codicon-edit:before{content:"\ea73"}.codicon-close:before{content:"\ea76"}.codicon-close-all:before{content:"\eac1"}.codicon-cloud-upload:before{content:"\eac3"}.codicon-file:before{content:"\ea7b"}.codicon-new-file:before{content:"\ea7f"}.codicon-new-folder:before{content:"\ea80"}.codicon-folder:before{content:"\ea83"}.codicon-trash:before{content:"\ea81"}.codicon-code:before{content:"\eac4"}.codicon-check:before{content:"\eab2"}.codicon-chevron-down:before{content:"\eab4"}.codicon-chevron-right:before{content:"\eab6"}.codicon-color-mode:before{content:"\eac6"}.codicon-diff:before,.codicon-diff-sidebyside:before{content:"\eae1"}.codicon-diff-single:before{content:"\ec22"}.codicon-discard:before{content:"\eae2"}.codicon-file-code:before{content:"\eae9"}.codicon-file-media:before{content:"\eaea"}.codicon-file-pdf:before{content:"\eaeb"}.codicon-file-zip:before{content:"\eaef"}.codicon-files:before{content:"\eaf0"}.codicon-folder-opened:before{content:"\eaf7"}.codicon-git-commit:before{content:"\eafc"}.codicon-graph:before{content:"\eb03"}.codicon-home:before{content:"\eb06"}.codicon-list-tree:before{content:"\eb86"}.codicon-preview:before{content:"\eb2f"}.codicon-refresh:before{content:"\eb37"}.codicon-remove:before{content:"\eb3b"}.codicon-check-all:before{content:"\ebb1"}
5
- .codicon-layout-sidebar-right:before{content:"\ebf4"}.codicon-layout-sidebar-right-off:before{content:"\ec00"}.codicon-settings:before{content:"\eb51"}.codicon-sparkle:before{content:"\ec10"}
5
+ .codicon-layout-sidebar-right:before{content:"\ebf4"}.codicon-layout-sidebar-right-off:before{content:"\ec00"}.codicon-settings:before{content:"\eb51"}.codicon-sparkle:before{content:"\ec10"}.codicon-split-horizontal:before{content:"\eb56"}.codicon-terminal:before{content:"\ea85"}
6
6
  .dvw-launch{display:flex;align-items:center;gap:10px;width:100%;height:36px;padding:0 9px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#aaa);cursor:pointer;font:inherit}.dvw-launch:hover{background:var(--dsw-alias-interactive-bg-hover,#292929);color:var(--dsw-alias-label-primary,#fff)}.dvw-launcher-mount{position:fixed;z-index:20;right:16px;bottom:16px}.dvw-launcher-mount .dvw-launch{width:auto;height:32px;padding:0 12px;justify-content:center;border:1px solid var(--dsw-alias-border-l3,#e5e7eb);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 2px rgba(0,0,0,.06);color:var(--dsw-alias-label-primary,#333)}body[data-ds-dark-theme] .dvw-launcher-mount .dvw-launch{color:var(--dsw-alias-label-primary,#ddd)}@media(max-width:640px){.dvw-launcher-mount{right:8px;bottom:8px}.dvw-launcher-mount .dvw-launch{height:30px;padding:0 10px;font-size:12px}}
7
7
  .dvw-overlay{--dvw-side-width:210px;--dvw-base:var(--dsw-alias-bg-base,#181818);--dvw-surface-1:var(--dsw-alias-bg-layer-1,#1e1e1e);--dvw-surface-2:var(--dsw-alias-bg-layer-2,#252526);--dvw-surface-3:var(--dsw-alias-bg-layer-3,#2d2d30);--dvw-text:var(--dsw-alias-label-primary,#ccc);--dvw-text-2:var(--dsw-alias-label-secondary,#aaa);--dvw-text-3:var(--dsw-alias-label-tertiary,#858585);--dvw-border:var(--dsw-alias-border-l2,#333);--dvw-border-soft:var(--dsw-alias-border-l3,#292929);--dvw-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));--dvw-active:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.14));--dvw-accent:var(--dsw-alias-state-business-primary,#3b82f6);--dvw-error:var(--dsw-alias-state-error-primary,#f48771);--dvw-success:var(--dsw-alias-state-success-primary,#73c991);--dvw-warn:var(--dsw-alias-state-warn-primary,#d7ba7d);position:absolute;inset:0;z-index:100;display:grid;grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto 24px;background:var(--dvw-base);color:var(--dvw-text);pointer-events:auto;font:13px/1.4 var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);overflow:hidden;color-scheme:light}
8
8
  body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-scheme:dark}
@@ -15,6 +15,7 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
15
15
  .dvw-git-head{padding:7px 10px;border-bottom:1px solid var(--dvw-border);display:flex;align-items:center;gap:8px}.dvw-branch{min-width:0;border:0;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;cursor:pointer}.dvw-commit{display:grid;grid-template-columns:1fr auto;gap:6px;padding:9px}.dvw-commit .dvw-input{grid-column:1/3}.dvw-commit-button{grid-column:1/3;height:31px;border:0;border-radius:6px;background:color-mix(in srgb,var(--dvw-accent) 24%,var(--dvw-surface-2));color:var(--dvw-text);display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer}.dvw-commit-button:hover{background:color-mix(in srgb,var(--dvw-accent) 34%,var(--dvw-surface-2))}.dvw-commit-button:disabled{opacity:.45;cursor:default}.dvw-group-head{height:30px;display:flex;align-items:center;gap:5px;padding:0 6px 0 9px;border-radius:5px;font-weight:600;position:sticky;top:0;background:var(--dvw-surface-1);z-index:1}.dvw-group-head:hover{background:var(--dvw-hover)}.dvw-group-head .dvw-count{margin-left:auto;min-width:20px;height:20px;border-radius:10px;display:grid;place-items:center;background:var(--dvw-surface-3);color:var(--dvw-text-2);font-size:11px}.dvw-change{display:flex;align-items:center;min-height:28px;padding:3px 5px 3px 19px;gap:5px;cursor:pointer}.dvw-change:hover{background:var(--dvw-hover)}.dvw-change .dvw-icon{opacity:0}.dvw-change:hover .dvw-icon,.dvw-change:focus-within .dvw-icon{opacity:1}.dvw-change-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvw-badge{font-size:11px;color:var(--dvw-warn)}.dvw-graph{border-top:1px solid var(--dvw-border-soft);margin-top:8px;padding-top:3px}.dvw-graph-list{padding-bottom:8px}.dvw-graph-row{width:100%;height:38px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;padding:0 7px;text-align:left;cursor:pointer;overflow:hidden}.dvw-graph-row:hover{background:var(--dvw-hover)}.dvw-graph-svg{flex:none;align-self:stretch}.dvw-graph-copy{display:flex;min-width:0;flex:1;flex-direction:column;justify-content:center}.dvw-graph-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-graph-meta{font-size:11px;color:var(--dvw-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-ref{max-width:82px;margin-left:4px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--dvw-accent) 50%,transparent);border-radius:9px;color:var(--dvw-accent);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-error{padding:8px;color:var(--dvw-error);white-space:pre-wrap}.dvw-diff{margin:0;height:100%;overflow:auto;padding:8px 0;background:var(--dvw-surface-1);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre}.dvw-diff-line{display:block;min-height:18px;padding:0 14px}.dvw-diff-line.add{background:color-mix(in srgb,var(--dvw-success) 15%,transparent);color:var(--dvw-success)}.dvw-diff-line.remove{background:color-mix(in srgb,var(--dvw-error) 13%,transparent);color:var(--dvw-error)}.dvw-diff-line.hunk{background:color-mix(in srgb,var(--dvw-accent) 12%,transparent);color:var(--dvw-accent)}.dvw-diff-line.meta{color:var(--dvw-text-3)}.dvw-diff-empty{padding:24px;color:var(--dvw-text-3)}
16
16
  .dvw-tabs{overflow:hidden}.dvw-tab-list{display:flex;min-width:0;flex:1;overflow-x:auto}.dvw-tab-actions{flex:none;width:42px;display:grid;place-items:center;padding:0 6px;background:var(--dvw-base)}.dvw-close-all{border:1px solid var(--dvw-border);background:var(--dvw-base)}.dvw-monaco-diff{position:absolute;inset:0}.dvw-diff-toolbar{justify-content:flex-end}.dvw-group-head{gap:2px;padding-left:2px}.dvw-group-toggle{min-width:0;height:30px;flex:1;border:0;border-radius:5px;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;padding:0 7px;cursor:pointer;text-align:left}.dvw-group-toggle:hover{background:var(--dvw-hover)}.dvw-graph-entry[data-expanded=true]{background:color-mix(in srgb,var(--dvw-accent) 5%,transparent)}.dvw-commit-files{padding:1px 0 5px 26px}.dvw-commit-file{width:100%;height:27px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:6px;padding:0 7px;text-align:left;cursor:pointer}.dvw-commit-file:hover{background:var(--dvw-hover)}.dvw-commit-file-name{white-space:nowrap}.dvw-commit-file-dir{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dvw-text-3);font-size:11px}
17
17
  .dvw-outline{flex:none;max-height:38%;display:flex;flex-direction:column;border-bottom:1px solid var(--dvw-border)}.dvw-outline-head{height:30px;display:flex;align-items:center;justify-content:space-between;padding:0 5px 0 10px;font-weight:600}.dvw-outline-head>span{display:flex;align-items:center;gap:6px}.dvw-outline-list{overflow:auto;padding-bottom:5px}.dvw-outline-item{display:block;width:100%;height:25px;border:0;background:transparent;color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;cursor:pointer}.dvw-outline-item:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-image-preview{position:absolute;inset:0;overflow:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;background:repeating-conic-gradient(var(--dvw-surface-2) 0 25%,var(--dvw-surface-1) 0 50%) 0/20px 20px}.dvw-image-preview img{max-width:100%;max-height:calc(100% - 32px);object-fit:contain;box-shadow:0 4px 24px rgba(0,0,0,.22)}.dvw-image-preview span{max-width:100%;padding:3px 8px;border-radius:5px;background:var(--dvw-surface-1);color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
18
+ .dvw-pdf-preview{position:absolute;inset:0;display:flex;min-height:0;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-pdf-toolbar{height:30px;flex:none;display:flex;align-items:center;justify-content:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--dvw-border);background:var(--dvw-surface-1);font-variant-numeric:tabular-nums}.dvw-pdf-toolbar .dvw-icon{font-size:18px}.dvw-pdf-zoom{min-width:52px;height:24px;border:0;border-radius:4px;background:var(--dvw-active);color:var(--dvw-text);font:inherit;cursor:pointer}.dvw-pdf-zoom:hover{background:var(--dvw-hover)}.dvw-pdf-separator{width:1px;height:18px;margin:0 4px;background:var(--dvw-border)}.dvw-pdf-page{min-height:0;flex:1;overflow:auto;display:flex;justify-content:center;padding:8px;background:var(--dvw-base)}.dvw-pdf-page canvas{flex:none;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.28)}.dvw-docx-preview{position:absolute;inset:0;overflow:auto;padding:28px;background:var(--dvw-base);color:#222}.dvw-docx-preview .docx-wrapper{margin:auto;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.2)}.dvw-document-error,.dvw-document-download{height:100%;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--dvw-text-2);text-align:center}.dvw-document-download a{color:var(--dvw-accent);text-decoration:none}.dvw-document-download a:hover{text-decoration:underline}
18
19
  .dvw-markdown{height:100%;overflow:auto;padding:28px clamp(24px,6vw,80px) 60px;color:var(--dvw-text);font-size:15px;line-height:1.65}.dvw-markdown>*:first-child{margin-top:0}.dvw-markdown h1,.dvw-markdown h2{padding-bottom:.3em;border-bottom:1px solid var(--dvw-border)}.dvw-markdown h1{font-size:2em}.dvw-markdown h2{font-size:1.5em}.dvw-markdown h3{font-size:1.25em}.dvw-markdown a{color:var(--dvw-accent)}.dvw-markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--dvw-surface-2);border-radius:4px;padding:.15em .35em}.dvw-markdown pre{overflow:auto;padding:14px;border:1px solid var(--dvw-border-soft);border-radius:7px;background:var(--dvw-base)}.dvw-markdown pre code{padding:0;background:transparent}.dvw-markdown blockquote{margin-left:0;padding-left:14px;border-left:3px solid var(--dvw-border);color:var(--dvw-text-2)}.dvw-markdown table{border-collapse:collapse;display:block;overflow:auto}.dvw-markdown th,.dvw-markdown td{padding:6px 12px;border:1px solid var(--dvw-border)}.dvw-markdown tr:nth-child(2n){background:var(--dvw-hover)}.dvw-markdown img{max-width:100%}.dvw-markdown hr{border:0;border-top:1px solid var(--dvw-border)}
19
20
  .dvw-sidebar-launcher-mount{border-bottom:1px solid var(--dsw-alias-border-l3,#e5e7eb)}.dvw-outline-toggle{margin-right:auto}.dvw-outline-toggle[data-active=true]{background:var(--dvw-active);color:var(--dvw-text)}.dvw-editor-outline{position:absolute;z-index:2;top:36px;bottom:0;left:0;width:240px;max-height:none;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);border-bottom:0}.dvw-editor[data-outline=true] .dvw-editor-body{left:240px}.dvw-diff-preview-switch{margin-right:auto;display:flex;gap:4px}.dvw-commit-files{position:relative;padding-top:1px;padding-right:0;padding-bottom:5px}.dvw-graph-continuation{position:absolute;top:0;bottom:0;left:7px;height:100%;pointer-events:none}.dvw-commit-file-list{position:relative;min-width:0}.dvw-overlay button:focus-visible,.dvw-overlay input:focus-visible,.dvw-overlay select:focus-visible{outline:1px solid var(--dvw-accent);outline-offset:-1px}@media(max-width:800px){.dvw-overlay{grid-template-columns:44px var(--dvw-side-width) minmax(0,1fr)}.dvw-editor-outline{width:210px}.dvw-editor[data-outline=true] .dvw-editor-body{left:210px}}
20
21
  .dvw-overlay[data-dvw-chat=true]{grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr) var(--dvw-chat-width)}
@@ -44,6 +45,13 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
44
45
  [data-dvw-conversation-panel]{position:fixed!important;top:36px!important;right:0!important;bottom:var(--dvw-status-height,24px)!important;left:auto!important;width:var(--dvw-chat-width)!important;height:auto!important;min-width:0!important;z-index:21!important;border-left:1px solid var(--dsw-alias-border-l2)!important;box-shadow:none!important}
45
46
  .dvw-confirm-backdrop{position:fixed;z-index:50;inset:0;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.38)}.dvw-confirm{width:min(380px,calc(100vw - 48px));border:1px solid var(--dvw-border);border-radius:7px;background:var(--dvw-surface-1);box-shadow:0 12px 36px rgba(0,0,0,.34);padding:18px}.dvw-confirm h2{margin:0 0 12px;font-size:15px}.dvw-confirm p{margin:0;color:var(--dvw-text)}.dvw-confirm-note{margin-top:8px!important;color:var(--dvw-text-3)!important;font-size:12px}.dvw-confirm footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.dvw-confirm footer button{height:28px;border:1px solid var(--dvw-border);border-radius:4px;background:var(--dvw-surface-2);color:var(--dvw-text);padding:0 12px;font:inherit;cursor:pointer}.dvw-confirm footer button:hover{background:var(--dvw-hover)}.dvw-confirm footer .dvw-danger-button{border-color:color-mix(in srgb,var(--dvw-error) 65%,var(--dvw-border));background:var(--dvw-error);color:#fff}.dvw-confirm footer .dvw-danger-button:hover{background:color-mix(in srgb,var(--dvw-error) 82%,#000)}
46
47
  .dvw-tree[data-dvw-native-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row[data-dvw-native-drop-target=true]{background:var(--dvw-active)}
48
+ .dvw-act:disabled{opacity:.4;cursor:not-allowed}
49
+ .dvw-terminal-root{height:100%;display:flex;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-terminal-tabs{height:28px;display:flex;align-items:center;padding:0 8px;gap:3px;border-bottom:1px solid var(--dvw-border-soft)}.dvw-terminal-tab{height:24px;max-width:180px;border:0;background:transparent;color:var(--dvw-text-2);padding:0 9px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dvw-terminal-tab[data-active=true]{color:var(--dvw-text);background:var(--dvw-active)}.dvw-terminal-actions{margin-left:auto;display:flex}.dvw-terminal-button{width:28px;height:24px;border:0;background:transparent;color:var(--dvw-text-2);cursor:pointer;font:18px/1 inherit}.dvw-terminal-button:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-terminal-button:disabled{opacity:.4;cursor:default}.dvw-terminal-host{position:relative;flex:1;min-height:0;padding:5px 8px;background:var(--dvw-base)}.dvw-terminal-host .xterm{height:100%;position:relative;user-select:none}.dvw-terminal-host .xterm.focus,.dvw-terminal-host .xterm:focus{outline:none}.dvw-terminal-host .xterm-viewport{position:absolute;inset:0;overflow-y:scroll;background-color:transparent}.dvw-terminal-host .xterm-screen{position:relative}.dvw-terminal-host .xterm-helpers{position:absolute;top:0;z-index:5}.dvw-terminal-host .xterm-helper-textarea{position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.dvw-terminal-host .composition-view{background:var(--dvw-base);color:var(--dvw-text);display:none;position:absolute;white-space:nowrap;z-index:1}.dvw-terminal-host .composition-view.active{display:block}.dvw-terminal-host .xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;left:-9999em;line-height:normal}.dvw-terminal-host .xterm-cursor-pointer,.dvw-terminal-host .xterm .xterm-cursor-pointer{cursor:pointer}.dvw-terminal-host .xterm-accessibility,.dvw-terminal-host .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.dvw-terminal-host .xterm-accessibility-tree{position:absolute;left:0;top:0;width:100%}.dvw-terminal-host .xterm-accessibility-tree>div{position:absolute;transform-origin:top left}.dvw-terminal-host canvas{position:absolute;left:0;top:0}.dvw-terminal-error{padding:8px;color:var(--dvw-error)}
50
+ .dvw-terminal-tabs{height:34px;padding:0 10px;gap:2px;background:var(--dvw-surface-1)}.dvw-terminal-tab{height:28px;display:flex;align-items:center;gap:6px;border-radius:3px;padding:0 10px;font:inherit}.dvw-terminal-tab .codicon{font-size:14px}.dvw-terminal-tab[data-active=true]{box-shadow:inset 0 1px 0 var(--dvw-accent)}.dvw-terminal-button{height:28px;font:inherit;display:grid;place-items:center;border-radius:3px}.dvw-terminal-host{padding:8px 10px}
51
+ .dvw-terminal-root{flex-direction:row}.dvw-terminal-main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}.dvw-terminal-main>.dvw-terminal-host{flex:1;min-height:0}.dvw-terminal-tabs{width:220px;height:auto;flex:none;display:flex;flex-direction:column;align-items:stretch;padding:0;border-left:1px solid var(--dvw-border);border-bottom:0;background:var(--dvw-surface-1)}.dvw-terminal-actions{height:35px;flex:none;margin-left:0;justify-content:flex-end;align-items:center;padding:0 6px;border-bottom:1px solid var(--dvw-border-soft)}.dvw-terminal-list{min-height:0;overflow:auto;padding:4px}.dvw-terminal-tab{width:100%;max-width:none;justify-content:flex-start;height:30px;border-radius:3px;color:var(--dvw-text-2)}.dvw-terminal-tab[data-active=true]{background:var(--dvw-active);color:var(--dvw-text);box-shadow:inset 2px 0 0 var(--dvw-accent)}
52
+ .dvw-terminal-rename{width:100%;min-width:0;height:22px;border:1px solid var(--dvw-accent);border-radius:2px;background:var(--dvw-base);color:var(--dvw-text);padding:0 4px;font:inherit;outline:0}.dvw-terminal-menu{position:fixed;z-index:60;min-width:140px;padding:4px;border:1px solid var(--dvw-border);border-radius:5px;background:var(--dvw-surface-3);box-shadow:0 6px 20px rgba(0,0,0,.3)}.dvw-terminal-menu button{display:block;width:100%;height:28px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text);padding:0 10px;text-align:left;font:inherit;cursor:pointer}.dvw-terminal-menu button:hover{background:var(--dvw-hover)}.dvw-terminal-menu .dvw-terminal-menu-danger{color:var(--dvw-error)}
53
+ .dvw-terminal-panes{flex:1;min-height:0;display:flex}.dvw-terminal-panes>.dvw-terminal-host{flex:1;min-width:0}.dvw-terminal-panes[data-split=true]>.dvw-terminal-host+.dvw-terminal-host{border-left:1px solid var(--dvw-border)}.dvw-terminal-resizer{width:4px;flex:none;cursor:ew-resize;position:relative;z-index:1}.dvw-terminal-resizer:hover{background:var(--dvw-accent)}.dvw-terminal-entry{position:relative;display:flex;align-items:center;min-width:0}.dvw-terminal-entry .dvw-terminal-tab{padding-right:58px}.dvw-terminal-entry-actions{position:absolute;right:4px;top:1px;height:28px;display:none;align-items:center;background:var(--dvw-active)}.dvw-terminal-entry:hover .dvw-terminal-entry-actions,.dvw-terminal-entry:focus-within .dvw-terminal-entry-actions{display:flex}.dvw-terminal-entry-actions button{width:25px;height:28px;display:grid;place-items:center;border:0;border-radius:3px;background:transparent;color:var(--dvw-text-2);cursor:pointer}.dvw-terminal-entry-actions button:hover{background:var(--dvw-hover);color:var(--dvw-text)}
54
+ .dvw-terminal-list,.dvw-terminal-host .xterm-viewport{scrollbar-width:none}.dvw-terminal-list::-webkit-scrollbar,.dvw-terminal-host .xterm-viewport::-webkit-scrollbar{display:none}
47
55
  .dvw-settings-backdrop{position:absolute;inset:0;z-index:30;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.34);backdrop-filter:blur(2px)}
48
56
  .dvw-settings{width:min(720px,calc(100vw - 48px));max-height:min(720px,calc(100vh - 48px));display:flex;flex-direction:column;border:1px solid var(--dvw-border);border-radius:10px;background:var(--dvw-surface-1);box-shadow:0 16px 50px rgba(0,0,0,.32);overflow:hidden}
49
57
  .dvw-settings>header{height:44px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 10px 0 14px;border-bottom:1px solid var(--dvw-border);font-size:14px;font-weight:600}.dvw-settings>header>span{display:flex;align-items:center;gap:8px}