dsh-vscode-workbench 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +27 -10
- package/README.md +26 -11
- package/cordis.patch.yml +2 -0
- package/lib/client.js +1039 -417
- package/lib/client.js.map +1 -1
- package/lib/index.js +8 -247
- package/lib/index.js.map +1 -1
- package/lib/types/client/DshBrowserPage.d.ts +6 -0
- package/lib/types/client/DshPanelPage.d.ts +9 -0
- package/lib/types/client/Icon.d.ts +1 -1
- package/lib/types/client/TerminalPanel.d.ts +5 -2
- package/lib/types/client/Workbench.d.ts +7 -1
- package/lib/types/client/browser.d.ts +26 -0
- package/lib/types/client/dshPanels.d.ts +4 -0
- package/lib/types/client/shortcuts.d.ts +2 -0
- package/lib/types/client/terminal.d.ts +37 -32
- package/package.json +15 -4
- package/src/client/DshBrowserPage.tsx +61 -0
- package/src/client/DshPanelPage.tsx +63 -0
- package/src/client/Icon.tsx +1 -1
- package/src/client/TerminalPanel.tsx +63 -40
- package/src/client/Workbench.tsx +72 -11
- package/src/client/browser.ts +68 -0
- package/src/client/dshPanels.ts +16 -0
- package/src/client/index.tsx +25 -6
- package/src/client/shortcuts.ts +9 -0
- package/src/client/styles.ts +18 -7
- package/src/client/terminal.ts +125 -78
- package/src/index.ts +0 -2
- package/lib/types/terminal.d.ts +0 -46
- package/src/terminal.ts +0 -173
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin a DSH global panel over the workbench editor while its tab is active.
|
|
3
|
+
* The official page keeps its own DOM and state; switching to a file only
|
|
4
|
+
* hides it, so returning to the tab does not rebuild the page.
|
|
5
|
+
*/
|
|
6
|
+
export declare function DshPanelPage({ panelId, active }: {
|
|
7
|
+
panelId: string;
|
|
8
|
+
active: boolean;
|
|
9
|
+
}): null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CodiconName = 'add' | 'arrow-right' | 'comment-discussion' | 'check' | 'check-all' | 'chevron-down' | 'chevron-right' | 'close' | 'close-all' | 'cloud-upload' | 'code' | 'color-mode' | 'edit' | 'diff' | 'diff-sidebyside' | 'diff-single' | 'discard' | 'file' | 'file-code' | 'file-media' | 'file-pdf' | 'file-zip' | 'files' | 'folder' | 'folder-opened' | 'git-branch' | 'git-commit' | 'graph' | 'home' | 'list-tree' | 'new-file' | 'layout-sidebar-right' | 'layout-sidebar-right-off' | 'new-folder' | 'preview' | 'refresh' | 'remove' | 'search' | 'settings' | 'source-control' | 'sparkle' | 'split-horizontal' | 'terminal' | 'trash';
|
|
1
|
+
export type CodiconName = 'add' | 'arrow-right' | 'comment-discussion' | 'check' | 'check-all' | 'chevron-down' | 'chevron-right' | 'close' | 'close-all' | 'cloud-upload' | 'code' | 'color-mode' | 'edit' | 'diff' | 'diff-sidebyside' | 'diff-single' | 'discard' | 'file' | 'file-code' | 'file-media' | 'file-pdf' | 'file-zip' | 'files' | 'folder' | 'folder-opened' | 'git-branch' | 'git-commit' | 'globe' | 'graph' | 'home' | 'list-tree' | 'new-file' | 'layout-sidebar-right' | 'layout-sidebar-right-off' | 'new-folder' | 'preview' | 'refresh' | 'remove' | 'search' | 'settings' | 'source-control' | 'sparkle' | 'split-horizontal' | 'terminal' | 'trash';
|
|
2
2
|
/** Render one glyph from Microsoft's VS Code Codicon font. */
|
|
3
3
|
export declare function Icon({ name, className }: {
|
|
4
4
|
name: CodiconName;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { BottomPanelProps } from './service.ts';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { type TerminalWorkspaceController } from './terminal.ts';
|
|
3
|
+
/** Keep the incumbent terminal layout while using the official process and stream models. */
|
|
4
|
+
export declare function TerminalPanel({ workspace, colorScheme }: BottomPanelProps & {
|
|
5
|
+
workspace: TerminalWorkspaceController;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -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 { NativeBrowser } from './browser.ts';
|
|
3
4
|
import type { DshNavigation } from './dshNavigation.ts';
|
|
4
5
|
import { type DocumentPreviewKind } from './documentPreview.tsx';
|
|
5
6
|
interface Tab extends FileDocument {
|
|
@@ -80,10 +81,15 @@ export declare function graphRows(commits: GitCommit[]): GraphRow[];
|
|
|
80
81
|
export declare function graphTrackPath(from: number, to: number, startY: number, endY: number, bendY: number): string;
|
|
81
82
|
/** Leave the node horizontally and turn down into a parent lane with a lane-width arc. */
|
|
82
83
|
export declare function graphParentPath(from: number, to: number, midY: number, endY: number): string;
|
|
83
|
-
export declare function Workbench({ service, sessionId, cwd, navigation }: {
|
|
84
|
+
export declare function Workbench({ service, browser, sessionId, cwd, navigation, dshPanelId: hostDshPanelId, onOpenDshPanel, onRestoreConversation, onCloseDshPanel }: {
|
|
84
85
|
service: VscodeWorkbench;
|
|
86
|
+
browser?: NativeBrowser;
|
|
85
87
|
sessionId?: string;
|
|
86
88
|
cwd?: string;
|
|
87
89
|
navigation?: DshNavigation;
|
|
90
|
+
dshPanelId?: string;
|
|
91
|
+
onOpenDshPanel?(panelId: string): void;
|
|
92
|
+
onRestoreConversation?(): void;
|
|
93
|
+
onCloseDshPanel?(): void;
|
|
88
94
|
}): import("react").JSX.Element | null;
|
|
89
95
|
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
export interface BrowserTab {
|
|
3
|
+
id: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BrowserSnapshot {
|
|
8
|
+
available: boolean;
|
|
9
|
+
tabs: readonly BrowserTab[];
|
|
10
|
+
}
|
|
11
|
+
/** Navigate official browser tabs; their controller, renderer and persistence stay with DSH. */
|
|
12
|
+
export declare class NativeBrowser {
|
|
13
|
+
private readonly ctx;
|
|
14
|
+
private snapshot;
|
|
15
|
+
private readonly listeners;
|
|
16
|
+
private readonly lifetime;
|
|
17
|
+
private readonly disposers;
|
|
18
|
+
constructor(ctx: Context);
|
|
19
|
+
getSnapshot: () => BrowserSnapshot;
|
|
20
|
+
subscribe: (listener: () => void) => (() => void);
|
|
21
|
+
open(sessionId: string, request?: AbortSignal): Promise<string>;
|
|
22
|
+
focus(id: string): void;
|
|
23
|
+
close(sessionId: string, id: string): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
private update;
|
|
26
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Locate the central panel DSH renders when a global panel (Plugins, market…) is open. */
|
|
2
|
+
export declare function findDshGlobalPanel(documentRoot?: Document): HTMLElement | null;
|
|
3
|
+
/** Tab-style label for an embedded DSH panel; unknown ids keep their own name. */
|
|
4
|
+
export declare function dshPanelLabel(id: string): string;
|
|
@@ -5,3 +5,5 @@ export declare function openTerminal(workbench: VscodeWorkbench): boolean;
|
|
|
5
5
|
export declare function toggleTerminal(workbench: VscodeWorkbench): boolean;
|
|
6
6
|
/** Handle terminal shortcuts through contributions registered by a terminal adapter. */
|
|
7
7
|
export declare function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean;
|
|
8
|
+
/** Match the official desktop browser shortcut while the workbench owns focus. */
|
|
9
|
+
export declare function handleBrowserShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean;
|
|
@@ -1,44 +1,49 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
close(): void;
|
|
6
|
-
subscribe(listener: (output: {
|
|
7
|
-
sequence: number;
|
|
8
|
-
data: string;
|
|
9
|
-
replay?: boolean;
|
|
10
|
-
}) => void): () => void;
|
|
11
|
-
subscribeError(listener: (message: string) => void): () => void;
|
|
1
|
+
import type { ClientTerminals, TerminalView, TerminalViewState } from '@deepseek-ai/dsh-api-terminal-controller/client';
|
|
2
|
+
export interface TerminalEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
12
5
|
}
|
|
13
6
|
export interface TerminalWorkspaceSnapshot {
|
|
14
|
-
terminals: readonly
|
|
7
|
+
terminals: readonly TerminalEntry[];
|
|
15
8
|
loading: boolean;
|
|
16
9
|
error?: string;
|
|
17
10
|
}
|
|
18
|
-
/**
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
/** Own workbench membership without overwriting the official sidebar's terminal inventory. */
|
|
12
|
+
export declare class TerminalWorkspaces {
|
|
13
|
+
readonly models: ClientTerminals;
|
|
14
|
+
private readonly workspaces;
|
|
15
|
+
private saved;
|
|
16
|
+
constructor(models: ClientTerminals);
|
|
17
|
+
get(sessionId: string): TerminalWorkspaceController;
|
|
18
|
+
save(sessionId: string, ids: string[]): void;
|
|
19
|
+
private readSaved;
|
|
20
|
+
private sync;
|
|
21
|
+
private onStorage;
|
|
22
|
+
private retain;
|
|
23
|
+
dispose(): void;
|
|
27
24
|
}
|
|
28
|
-
/**
|
|
29
|
-
export declare
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
/** Layout only; the official model owns processes, stream ordering, recovery and input control. */
|
|
26
|
+
export declare class TerminalWorkspaceController {
|
|
27
|
+
private readonly owner;
|
|
28
|
+
readonly sessionId: string;
|
|
29
|
+
readonly closeFailures: ClientTerminals['closeFailures'];
|
|
33
30
|
private snapshot;
|
|
34
31
|
private readonly listeners;
|
|
35
|
-
|
|
32
|
+
private readonly views;
|
|
33
|
+
constructor(owner: TerminalWorkspaces, sessionId: string, ids: readonly string[]);
|
|
34
|
+
view(id: string): TerminalView;
|
|
36
35
|
refresh(): Promise<boolean>;
|
|
37
|
-
create(
|
|
38
|
-
close(id: string):
|
|
36
|
+
create(): Promise<TerminalEntry>;
|
|
37
|
+
close(id: string): void;
|
|
39
38
|
rename(id: string, title: string): Promise<void>;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
retryClose(id: Parameters<ClientTerminals['retryClose']>[0]): void;
|
|
40
|
+
reconcile(ids: readonly string[]): void;
|
|
41
|
+
getSnapshot: () => TerminalWorkspaceSnapshot;
|
|
42
|
+
subscribe: (listener: () => void) => (() => void);
|
|
43
|
+
dispose(): void;
|
|
44
|
+
private save;
|
|
45
|
+
private updateTitles;
|
|
46
|
+
private fail;
|
|
43
47
|
private set;
|
|
44
48
|
}
|
|
49
|
+
export declare function terminalIssue(state: TerminalViewState): string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-workbench",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Explorer, Monaco editor, search and local Git overlay for DSH",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,12 @@
|
|
|
51
51
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
52
52
|
"@deepseek-ai/dsh-client-ui-session",
|
|
53
53
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
55
|
+
"@deepseek-ai/dsh-api-terminal-controller",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
57
|
+
],
|
|
58
|
+
"external": [
|
|
59
|
+
"@deepseek-ai/dsh-api-terminal-controller/client"
|
|
55
60
|
]
|
|
56
61
|
}
|
|
57
62
|
},
|
|
@@ -66,7 +71,6 @@
|
|
|
66
71
|
"dompurify": "^3.2.6",
|
|
67
72
|
"marked": "^16.3.0",
|
|
68
73
|
"monaco-editor": "^0.52.2",
|
|
69
|
-
"node-pty": "^1.1.0",
|
|
70
74
|
"pdfjs-dist": "4.10.38",
|
|
71
75
|
"vscode-icons-js": "^11.6.1",
|
|
72
76
|
"ws": "^8.18.0",
|
|
@@ -75,11 +79,13 @@
|
|
|
75
79
|
"peerDependencies": {
|
|
76
80
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
77
81
|
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-api-terminal-controller": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
78
83
|
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
79
84
|
"@deepseek-ai/dsh-client-ui-layout": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
80
85
|
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
81
86
|
"@deepseek-ai/dsh-client-ui-session": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
82
87
|
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
83
89
|
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
84
90
|
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
85
91
|
"@deepseek-ai/dsh-llm": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
@@ -93,7 +99,9 @@
|
|
|
93
99
|
"@deepseek-ai/cordis-plugin-group": "1.1.0",
|
|
94
100
|
"@deepseek-ai/cordis-plugin-include": "1.1.0",
|
|
95
101
|
"@deepseek-ai/cordis-plugin-loader": "1.1.0",
|
|
102
|
+
"@deepseek-ai/dsh-api-gateway": "0.2.2-rc.1",
|
|
96
103
|
"@deepseek-ai/dsh-api-session-controller": "0.2.2-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-api-terminal-controller": "0.2.2-rc.1",
|
|
97
105
|
"@deepseek-ai/dsh-app-boot": "0.2.2-rc.1",
|
|
98
106
|
"@deepseek-ai/dsh-client-connection": "0.2.2-rc.1",
|
|
99
107
|
"@deepseek-ai/dsh-client-store": "0.2.2-rc.1",
|
|
@@ -101,6 +109,7 @@
|
|
|
101
109
|
"@deepseek-ai/dsh-client-ui-renderer": "0.2.2-rc.1",
|
|
102
110
|
"@deepseek-ai/dsh-client-ui-session": "0.2.2-rc.1",
|
|
103
111
|
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.2-rc.1",
|
|
112
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.2-rc.1",
|
|
104
113
|
"@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
|
|
105
114
|
"@deepseek-ai/dsh-host-webserver": "0.2.2-rc.1",
|
|
106
115
|
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
@@ -111,12 +120,14 @@
|
|
|
111
120
|
"@types/react": "^18.3.0",
|
|
112
121
|
"@types/react-dom": "^18.3.0",
|
|
113
122
|
"@types/ws": "^8.18.0",
|
|
123
|
+
"immer": "^10.2.0",
|
|
114
124
|
"jsdom": "29.1.1",
|
|
115
125
|
"react": "^18.2.0",
|
|
116
126
|
"react-dom": "^18.3.1",
|
|
117
127
|
"tsdown": "^0.22.2",
|
|
118
128
|
"typescript": "^5.9.0",
|
|
119
|
-
"vitest": "^4.0.0"
|
|
129
|
+
"vitest": "^4.0.0",
|
|
130
|
+
"zustand": "~4.5.7"
|
|
120
131
|
},
|
|
121
132
|
"scripts": {
|
|
122
133
|
"clean": "node scripts/clean.mjs lib dist",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useLayoutEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Present the original browser DOM without moving React-owned nodes or rebuilding webviews. */
|
|
4
|
+
const PALETTE: Record<string, string> = {
|
|
5
|
+
'--dsw-alias-bg-base': '--dvw-surface-1', '--dsw-alias-bg-layer-1': '--dvw-surface-2',
|
|
6
|
+
'--dsw-alias-bg-layer-2': '--dvw-surface-3', '--dsw-alias-label-primary': '--dvw-text',
|
|
7
|
+
'--dsw-alias-label-secondary': '--dvw-text-2', '--dsw-alias-label-tertiary': '--dvw-text-3',
|
|
8
|
+
'--dsw-alias-border-l2': '--dvw-border', '--dsw-alias-border-l3': '--dvw-border-soft',
|
|
9
|
+
'--dsw-alias-interactive-bg-hover': '--dvw-hover', '--dsw-alias-interactive-bg-active': '--dvw-active',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DshBrowserPage({ sessionId, tabId, active, colorScheme }: { sessionId: string; tabId: string; active: boolean; colorScheme: 'dark' | 'light' | 'white' | 'eyecare' }) {
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
let page: HTMLElement | undefined
|
|
15
|
+
let restore: (() => void) | undefined
|
|
16
|
+
let frame = 0
|
|
17
|
+
const place = () => {
|
|
18
|
+
const next = [...document.querySelectorAll<HTMLElement>('div[data-sidebar-right-tab]')].find(node => node.dataset.sidebarRightTab === tabId && node.closest('[data-sidebar-right-session]')?.getAttribute('data-sidebar-right-session') === sessionId)
|
|
19
|
+
if (next !== page) {
|
|
20
|
+
restore?.(); page = next
|
|
21
|
+
if (page !== undefined) {
|
|
22
|
+
const node = page
|
|
23
|
+
const popover = node.getAttribute('popover')
|
|
24
|
+
const properties = ['left', 'top', 'width', 'height', 'color-scheme', ...Object.keys(PALETTE)]
|
|
25
|
+
const styles = properties.map(property => [property, node.style.getPropertyValue(property), node.style.getPropertyPriority(property)])
|
|
26
|
+
node.setAttribute('popover', 'manual')
|
|
27
|
+
node.setAttribute('data-dvw-browser-page', '')
|
|
28
|
+
restore = () => {
|
|
29
|
+
if (node.matches(':popover-open')) node.hidePopover()
|
|
30
|
+
if (popover === null) node.removeAttribute('popover'); else node.setAttribute('popover', popover)
|
|
31
|
+
node.removeAttribute('data-dvw-browser-page')
|
|
32
|
+
for (const [property, value, priority] of styles) node.style.setProperty(property!, value!, priority)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (page === undefined) return
|
|
37
|
+
const rect = document.querySelector('.dvw-editor')?.getBoundingClientRect()
|
|
38
|
+
const visible = active && rect !== undefined && rect.width > 0 && rect.height > 0
|
|
39
|
+
if (!visible) { if (page.matches(':popover-open')) page.hidePopover(); return }
|
|
40
|
+
for (const [property, value] of Object.entries({ left: rect.left, top: rect.top, width: rect.width, height: rect.height })) page.style.setProperty(property, `${Math.round(value)}px`, 'important')
|
|
41
|
+
const overlay = document.querySelector('.dvw-overlay')
|
|
42
|
+
if (overlay !== null) {
|
|
43
|
+
const palette = getComputedStyle(overlay)
|
|
44
|
+
for (const [alias, token] of Object.entries(PALETTE)) page.style.setProperty(alias, palette.getPropertyValue(token))
|
|
45
|
+
}
|
|
46
|
+
page.style.setProperty('color-scheme', colorScheme === 'dark' ? 'dark' : 'light')
|
|
47
|
+
// The top layer escapes dockkit clipping while keeping the original DOM parent and lease.
|
|
48
|
+
if (!page.matches(':popover-open')) page.showPopover()
|
|
49
|
+
}
|
|
50
|
+
const schedule = () => { if (frame === 0) frame = requestAnimationFrame(() => { frame = 0; place() }) }
|
|
51
|
+
place()
|
|
52
|
+
const resize = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(schedule)
|
|
53
|
+
const editor = document.querySelector('.dvw-editor')
|
|
54
|
+
if (editor !== null) resize?.observe(editor)
|
|
55
|
+
const observer = new MutationObserver(schedule)
|
|
56
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
57
|
+
window.addEventListener('resize', schedule)
|
|
58
|
+
return () => { observer.disconnect(); resize?.disconnect(); window.removeEventListener('resize', schedule); cancelAnimationFrame(frame); restore?.() }
|
|
59
|
+
}, [sessionId, tabId, active, colorScheme])
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
import { findDshGlobalPanel } from './dshPanels.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pin a DSH global panel over the workbench editor while its tab is active.
|
|
6
|
+
* The official page keeps its own DOM and state; switching to a file only
|
|
7
|
+
* hides it, so returning to the tab does not rebuild the page.
|
|
8
|
+
*/
|
|
9
|
+
export function DshPanelPage({ panelId, active }: { panelId: string; active: boolean }) {
|
|
10
|
+
const activeRef = useRef(active)
|
|
11
|
+
activeRef.current = active
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
let panel: HTMLElement | null = null
|
|
14
|
+
let frame = 0
|
|
15
|
+
const clear = (node: HTMLElement) => {
|
|
16
|
+
node.removeAttribute('data-dvw-dsh-page')
|
|
17
|
+
node.removeAttribute('data-dvw-dsh-hidden')
|
|
18
|
+
for (const property of ['left', 'top', 'width', 'height']) node.style.removeProperty(property)
|
|
19
|
+
}
|
|
20
|
+
const place = () => {
|
|
21
|
+
const next = findDshGlobalPanel()
|
|
22
|
+
if (next !== panel) {
|
|
23
|
+
if (panel !== null) clear(panel)
|
|
24
|
+
panel = next
|
|
25
|
+
panel?.setAttribute('data-dvw-dsh-page', '')
|
|
26
|
+
}
|
|
27
|
+
if (panel === null) return
|
|
28
|
+
panel.toggleAttribute('data-dvw-dsh-hidden', !activeRef.current)
|
|
29
|
+
if (!activeRef.current) return
|
|
30
|
+
const editor = document.querySelector<HTMLElement>('.dvw-editor')
|
|
31
|
+
if (editor === null) return
|
|
32
|
+
const rect = editor.getBoundingClientRect()
|
|
33
|
+
if (rect.width <= 0 || rect.height <= 0) return
|
|
34
|
+
panel.style.setProperty('left', `${Math.round(rect.left)}px`)
|
|
35
|
+
panel.style.setProperty('top', `${Math.round(rect.top)}px`)
|
|
36
|
+
panel.style.setProperty('width', `${Math.round(rect.width)}px`)
|
|
37
|
+
panel.style.setProperty('height', `${Math.round(rect.height)}px`)
|
|
38
|
+
}
|
|
39
|
+
const schedule = () => {
|
|
40
|
+
if (frame !== 0) return
|
|
41
|
+
frame = requestAnimationFrame(() => { frame = 0; place() })
|
|
42
|
+
}
|
|
43
|
+
place()
|
|
44
|
+
const resize = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(schedule)
|
|
45
|
+
const editor = document.querySelector<HTMLElement>('.dvw-editor')
|
|
46
|
+
if (editor !== null) resize?.observe(editor)
|
|
47
|
+
window.addEventListener('resize', schedule)
|
|
48
|
+
// The page may mount after this effect, or rebuild its subtree on navigation.
|
|
49
|
+
const observer = new MutationObserver(records => {
|
|
50
|
+
if (records.every(record => record.target instanceof Element && record.target.closest('.dvw-overlay') !== null)) return
|
|
51
|
+
schedule()
|
|
52
|
+
})
|
|
53
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
54
|
+
return () => {
|
|
55
|
+
observer.disconnect()
|
|
56
|
+
resize?.disconnect()
|
|
57
|
+
cancelAnimationFrame(frame)
|
|
58
|
+
window.removeEventListener('resize', schedule)
|
|
59
|
+
if (panel !== null) clear(panel)
|
|
60
|
+
}
|
|
61
|
+
}, [panelId, active])
|
|
62
|
+
return null
|
|
63
|
+
}
|
package/src/client/Icon.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type CodiconName =
|
|
2
2
|
| 'add' | 'arrow-right' | 'comment-discussion' | '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
|
-
| 'files' | 'folder' | 'folder-opened' | 'git-branch' | 'git-commit' | 'graph' | 'home' | 'list-tree' | 'new-file'
|
|
4
|
+
| 'files' | 'folder' | 'folder-opened' | 'git-branch' | 'git-commit' | 'globe' | 'graph' | 'home' | 'list-tree' | 'new-file'
|
|
5
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. */
|
|
@@ -1,57 +1,79 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
2
2
|
import { FitAddon } from '@xterm/addon-fit'
|
|
3
3
|
import { Terminal } from '@xterm/xterm'
|
|
4
4
|
import { Icon } from './Icon.tsx'
|
|
5
5
|
import type { BottomPanelProps } from './service.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { terminalIssue, type TerminalWorkspaceController } from './terminal.ts'
|
|
7
7
|
|
|
8
8
|
// VS Code Light+ ANSI palette: the default xterm colors (e.g. yellow #e5e510) vanish on a light background.
|
|
9
9
|
const LIGHT_ANSI = { black: '#000000', red: '#cd3131', green: '#107c10', yellow: '#949800', blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555', brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14', brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc', brightCyan: '#0598bc', brightWhite: '#a5a5a5' }
|
|
10
10
|
function terminalTheme(colorScheme: 'dark' | 'light' | 'white' | 'eyecare') { return colorScheme === 'dark' ? { background: '#191a1b', foreground: '#bfbfbf', cursor: '#bfbfbf', selectionBackground: '#264f78' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff', ...LIGHT_ANSI } }
|
|
11
11
|
|
|
12
|
-
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: '
|
|
12
|
+
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: BottomPanelProps['colorScheme'] }) {
|
|
13
|
+
const model = workspace.view(id)
|
|
14
|
+
const state = useSyncExternalStore(model.state.subscribe, model.state.getSnapshot)
|
|
13
15
|
const root = useRef<HTMLDivElement>(null)
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
const screen = useRef<Terminal>()
|
|
17
|
+
const fit = useRef<FitAddon>()
|
|
18
|
+
const revision = useRef(0)
|
|
19
|
+
const current = useRef(state)
|
|
20
|
+
current.current = state
|
|
21
|
+
const measure = () => {
|
|
22
|
+
const node = root.current
|
|
23
|
+
const dimensions = fit.current?.proposeDimensions()
|
|
24
|
+
if (!node?.clientWidth || !node.clientHeight || !current.current.writable || dimensions === undefined) return
|
|
25
|
+
const cols = Math.min(dimensions.cols, current.current.environment?.maxCols ?? 500)
|
|
26
|
+
const rows = Math.min(dimensions.rows, current.current.environment?.maxRows ?? 200)
|
|
27
|
+
if (cols < 2 || rows < 1) return
|
|
28
|
+
screen.current?.resize(cols, rows)
|
|
29
|
+
model.resize(cols, rows)
|
|
30
|
+
}
|
|
31
|
+
useLayoutEffect(() => {
|
|
19
32
|
if (root.current === null) return
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const fit = new FitAddon()
|
|
24
|
-
terminal.loadAddon(fit)
|
|
33
|
+
const terminal = new Terminal({ cursorBlink: true, fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Consolas,monospace', fontSize: 13, minimumContrastRatio: 4.5, theme: { ...terminalTheme(colorScheme), background: getComputedStyle(root.current).backgroundColor }, scrollback: 1000 })
|
|
34
|
+
const addon = new FitAddon()
|
|
35
|
+
terminal.loadAddon(addon)
|
|
25
36
|
terminal.open(root.current)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
terminal.dispose()
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
const output = connection.subscribe(value => {
|
|
34
|
-
if (value.replay) { replaying.current = true; clearTimeout(replayGuard.current) }
|
|
35
|
-
terminal.write(value.data, () => { if (value.replay) replayGuard.current = setTimeout(() => { replaying.current = false }, 250) })
|
|
36
|
-
})
|
|
37
|
-
const connectionError = connection.subscribeError(setError)
|
|
38
|
-
const input = terminal.onData(data => { if (!replaying.current || !isTerminalProtocolReply(data)) connection.write(data) })
|
|
39
|
-
const resize = terminal.onResize(({ cols, rows }) => connection.resize(cols, rows))
|
|
40
|
-
const observer = new ResizeObserver(() => { try { fit.fit() } catch { /* The panel can be hidden during layout changes. */ } })
|
|
37
|
+
terminal.textarea?.setAttribute('aria-label', '终端输入')
|
|
38
|
+
screen.current = terminal; fit.current = addon; revision.current = 0
|
|
39
|
+
const input = terminal.onData(data => model.write(data))
|
|
40
|
+
const observer = new ResizeObserver(measure)
|
|
41
41
|
observer.observe(root.current)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
return () => { observer.disconnect(); input.dispose(); terminal.dispose(); screen.current = undefined; fit.current = undefined }
|
|
43
|
+
}, [model])
|
|
44
|
+
useEffect(() => model.mount(), [model])
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
const terminal = screen.current
|
|
47
|
+
const render = state.render
|
|
48
|
+
if (terminal === undefined || render === undefined || render.revision <= revision.current) return
|
|
49
|
+
revision.current = render.revision
|
|
50
|
+
if (render.frame.type === 'snapshot') { terminal.reset(); terminal.resize(render.frame.info.cols, render.frame.info.rows) }
|
|
51
|
+
terminal.write(render.frame.type === 'snapshot' ? render.frame.screen : render.frame.data, () => model.acknowledge(render.revision))
|
|
52
|
+
}, [state.render, model])
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
if (screen.current === undefined) return
|
|
55
|
+
screen.current.options.disableStdin = !state.writable
|
|
56
|
+
if (state.writable) { measure(); screen.current.focus() }
|
|
57
|
+
else if (state.info !== undefined) screen.current.resize(state.info.cols, state.info.rows)
|
|
58
|
+
}, [state.writable, state.info?.cols, state.info?.rows, model])
|
|
59
|
+
useEffect(() => { if (screen.current !== undefined) screen.current.options.theme = { ...terminalTheme(colorScheme), ...(root.current === null ? {} : { background: getComputedStyle(root.current).backgroundColor }) } }, [colorScheme])
|
|
60
|
+
const error = terminalIssue(state)
|
|
61
|
+
const retry = state.phase === 'failed' || state.phase === 'disconnected'
|
|
62
|
+
const readOnly = state.phase === 'connected' && state.info?.state === 'running' && !state.writable
|
|
63
|
+
const ended = state.info?.state === 'exited'
|
|
64
|
+
return <section className="dvw-terminal-pane">
|
|
65
|
+
{(error || retry || readOnly || ended || state.info === undefined) && <div className="dvw-terminal-notice" role="status">
|
|
66
|
+
<span>{error ?? (ended ? `进程已退出(${state.info?.exitCode ?? '—'})` : readOnly ? '其他页面正在控制此终端' : retry ? '终端连接已断开' : '正在启动终端…')}</span>
|
|
67
|
+
{(retry || readOnly) && <button onClick={() => state.info === undefined ? void model.refresh() : model.connect()}>{readOnly ? '接管输入' : '重新连接'}</button>}
|
|
68
|
+
</div>}
|
|
69
|
+
<div ref={root} className="dvw-terminal-host"/>
|
|
70
|
+
</section>
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
/**
|
|
50
|
-
export function TerminalPanel({
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const workspace = workspaceRef.current.value
|
|
54
|
-
const snapshot = useSyncExternalStore(workspace.subscribe.bind(workspace), workspace.getSnapshot.bind(workspace))
|
|
73
|
+
/** Keep the incumbent terminal layout while using the official process and stream models. */
|
|
74
|
+
export function TerminalPanel({ workspace, colorScheme }: BottomPanelProps & { workspace: TerminalWorkspaceController }) {
|
|
75
|
+
const snapshot = useSyncExternalStore(workspace.subscribe, workspace.getSnapshot)
|
|
76
|
+
const closeFailures = useSyncExternalStore(workspace.closeFailures.subscribe, workspace.closeFailures.getSnapshot)
|
|
55
77
|
const [active, setActive] = useState<string>()
|
|
56
78
|
const [context, setContext] = useState<{ id: string; x: number; y: number }>()
|
|
57
79
|
const [renaming, setRenaming] = useState<{ id: string; value: string }>()
|
|
@@ -72,7 +94,7 @@ export function TerminalPanel({ sessionId, colorScheme }: BottomPanelProps) {
|
|
|
72
94
|
window.addEventListener('keydown', escape)
|
|
73
95
|
return () => { window.removeEventListener('pointerdown', close); window.removeEventListener('keydown', escape) }
|
|
74
96
|
}, [context])
|
|
75
|
-
const closeTerminal = (id: string) => { setContext(undefined); setSplit(current => current?.filter(value => value !== id));
|
|
97
|
+
const closeTerminal = (id: string) => { setContext(undefined); setSplit(current => current?.filter(value => value !== id)); workspace.close(id) }
|
|
76
98
|
const saveRename = (id: string) => { const title = renaming?.id === id ? renaming.value.trim() : ''; setRenaming(undefined); if (title) void workspace.rename(id, title) }
|
|
77
99
|
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) }).catch(() => undefined) }
|
|
78
100
|
const shown = (split ?? (active === undefined ? [] : [active])).filter(id => snapshot.terminals.some(value => value.id === id))
|
|
@@ -80,6 +102,7 @@ export function TerminalPanel({ sessionId, colorScheme }: BottomPanelProps) {
|
|
|
80
102
|
return <div className="dvw-terminal-root">
|
|
81
103
|
<div className="dvw-terminal-main">
|
|
82
104
|
{snapshot.error !== undefined && <div className="dvw-terminal-error">{snapshot.error}</div>}
|
|
105
|
+
{closeFailures.map(failure => <div className="dvw-terminal-notice" role="alert" key={failure.id}><span>终端“{failure.title}”未能关闭:{failure.message}</span><button onClick={() => workspace.retryClose(failure.id)}>重试关闭</button></div>)}
|
|
83
106
|
<div className="dvw-terminal-panes" data-split={shown.length > 1 || undefined}>{shown.map(id => <XtermView key={id} workspace={workspace} id={id} colorScheme={colorScheme} />)}</div>
|
|
84
107
|
</div>
|
|
85
108
|
<div className="dvw-terminal-resizer" role="separator" aria-label="终端列表宽度" aria-orientation="vertical" onPointerDown={event => {
|