@linxin666/dsh-ssh 0.2.1 → 0.2.3
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.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +78 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +7 -1
- package/lib/types/client/index.d.ts +13 -1
- package/lib/types/client/mount.d.ts +3 -1
- package/lib/types/client/panel/SshPanel.d.ts +4 -1
- package/lib/types/client/panel/TerminalTab.d.ts +7 -1
- package/lib/types/client/panel/helpers.d.ts +29 -0
- package/lib/types/index.d.ts +7 -0
- package/package.json +3 -3
- package/src/client/index.ts +38 -3
- package/src/client/mount.tsx +4 -2
- package/src/client/panel/HostsTab.tsx +10 -2
- package/src/client/panel/SshPanel.tsx +5 -3
- package/src/client/panel/TerminalTab.tsx +31 -5
- package/src/client/panel/helpers.ts +44 -0
- package/src/engine/pty.ts +0 -1
- package/src/index.ts +8 -0
package/lib/index.js
CHANGED
|
@@ -1614,6 +1614,11 @@ async function execCommand(engine, alias, command, timeoutMs) {
|
|
|
1614
1614
|
//#endregion
|
|
1615
1615
|
//#region src/engine/pty.ts
|
|
1616
1616
|
/**
|
|
1617
|
+
* PTY shell sessions for the web terminal: a standalone (non-pooled)
|
|
1618
|
+
* ssh2 connection with a long-lived shell channel, resize, and pausable
|
|
1619
|
+
* output delivery.
|
|
1620
|
+
*/
|
|
1621
|
+
/**
|
|
1617
1622
|
* Open a PTY shell session for the web terminal (standalone connection).
|
|
1618
1623
|
* The shell is a long-lived exclusive stream: it uses its own connection so
|
|
1619
1624
|
* closing it can never tear down a pooled exec/tunnel sharing the alias.
|
|
@@ -3412,7 +3417,8 @@ const inject = [
|
|
|
3412
3417
|
const SSH_SETTINGS_NAMESPACE = settingsNamespace("dsh-ssh");
|
|
3413
3418
|
const Config = import_lib.default.object({
|
|
3414
3419
|
announceToAgent: import_lib.default.boolean().default(true),
|
|
3415
|
-
enabled: import_lib.default.boolean().default(true)
|
|
3420
|
+
enabled: import_lib.default.boolean().default(true),
|
|
3421
|
+
terminalFontFamily: import_lib.default.string().default("")
|
|
3416
3422
|
});
|
|
3417
3423
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
3418
3424
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -10,8 +10,20 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
13
|
+
import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
14
|
import { type SshKey } from './locales.ts';
|
|
15
|
+
declare module '@deepseek-ai/cordis' {
|
|
16
|
+
interface Context {
|
|
17
|
+
/**
|
|
18
|
+
* Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
|
|
19
|
+
* absent when that group plugin is not installed, so callers fall back to
|
|
20
|
+
* the official settings scope.
|
|
21
|
+
*/
|
|
22
|
+
webUiSettings?: {
|
|
23
|
+
bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
15
27
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
16
28
|
interface LocaleNamespaceMap {
|
|
17
29
|
/** dsh-ssh surface copy. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SshApi } from './api.ts';
|
|
2
2
|
import type { PanelController } from './panel/controller.ts';
|
|
3
|
+
import type { TerminalFontSource } from './panel/helpers.ts';
|
|
3
4
|
/** The injected panel container (kept in the DOM, hidden when inactive). */
|
|
4
5
|
export declare const PANEL_VIEW_SELECTOR = "[data-dsh-ssh-view]";
|
|
5
6
|
/**
|
|
@@ -7,6 +8,7 @@ export declare const PANEL_VIEW_SELECTOR = "[data-dsh-ssh-view]";
|
|
|
7
8
|
* to the controller's panelOpen state.
|
|
8
9
|
* @param controller - the panel controller driving the view.
|
|
9
10
|
* @param api - the SSH API client the tabs operate through.
|
|
11
|
+
* @param terminalFont - live terminal-font setting source (issue #577).
|
|
10
12
|
* @returns disposer unmounting the tree and restoring the column.
|
|
11
13
|
*/
|
|
12
|
-
export declare function mountPanel(controller: PanelController, api: SshApi): () => void;
|
|
14
|
+
export declare function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource): () => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SshApi } from '../api.ts';
|
|
2
2
|
import type { PanelController } from './controller.ts';
|
|
3
|
+
import { type TerminalFontSource } from './helpers.ts';
|
|
3
4
|
/** The panel's tab identifiers. */
|
|
4
5
|
export type SshTab = 'hosts' | 'terminal' | 'transfer' | 'tunnels' | 'cluster';
|
|
5
6
|
/** Panel shell props. */
|
|
@@ -8,6 +9,8 @@ export interface SshPanelProps {
|
|
|
8
9
|
controller: PanelController;
|
|
9
10
|
/** The SSH API client every tab operates through. */
|
|
10
11
|
api: SshApi;
|
|
12
|
+
/** Live terminal-font setting source handed to the terminal tab (issue #577). */
|
|
13
|
+
terminalFont?: TerminalFontSource;
|
|
11
14
|
}
|
|
12
15
|
/** The tabbed SSH panel. */
|
|
13
|
-
export declare function SshPanel({ controller, api }: SshPanelProps): import("react").JSX.Element;
|
|
16
|
+
export declare function SshPanel({ controller, api, terminalFont }: SshPanelProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SshApi } from '../api.ts';
|
|
2
|
+
import { type TerminalFontSource } from './helpers.ts';
|
|
2
3
|
/** Terminal tab props. */
|
|
3
4
|
export interface TerminalTabProps {
|
|
4
5
|
api: SshApi;
|
|
@@ -6,6 +7,11 @@ export interface TerminalTabProps {
|
|
|
6
7
|
presetAlias?: string;
|
|
7
8
|
/** Monotonic id of the connect request (re-applies presetAlias). */
|
|
8
9
|
requestId?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Live terminal-font setting source (issue #577). Absent in tests and
|
|
12
|
+
* legacy mounts: the font then comes from the CSS custom-property chain.
|
|
13
|
+
*/
|
|
14
|
+
terminalFont?: TerminalFontSource;
|
|
9
15
|
}
|
|
10
16
|
/** The xterm terminal view. */
|
|
11
|
-
export declare function TerminalTab({ api, presetAlias, requestId }: TerminalTabProps): import("react").JSX.Element;
|
|
17
|
+
export declare function TerminalTab({ api, presetAlias, requestId, terminalFont }: TerminalTabProps): import("react").JSX.Element;
|
|
@@ -12,3 +12,32 @@ export declare function dictionary(): Record<string, string>;
|
|
|
12
12
|
export declare function tt(key: SshKey, values?: TranslateValues): string;
|
|
13
13
|
/** Human-readable error text from an unknown thrown value. */
|
|
14
14
|
export declare function errorMessage(error: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* The terminal font stack used before issue #577, kept as the last resort
|
|
17
|
+
* when neither the plugin setting nor a CSS custom property names a font.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TERMINAL_FONT_FALLBACK = "Menlo, Consolas, \"Liberation Mono\", monospace";
|
|
20
|
+
/**
|
|
21
|
+
* Live source of the user's configured terminal font family: the client
|
|
22
|
+
* entry binds the dsh-ssh settings namespace into one of these and hands it
|
|
23
|
+
* to the panel, so a settings change re-applies without a remount.
|
|
24
|
+
*/
|
|
25
|
+
export interface TerminalFontSource {
|
|
26
|
+
/** Current configured family, or undefined when unset / not yet loaded. */
|
|
27
|
+
get(): string | undefined;
|
|
28
|
+
/** Subscribe to changes; the returned function unsubscribes. */
|
|
29
|
+
subscribe(listener: () => void): () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the xterm `fontFamily` (issue #577). xterm's DOM renderer takes
|
|
33
|
+
* the font only from constructor/options, so a plain stylesheet rule cannot
|
|
34
|
+
* retarget it — the value must be read back into the options. The chain,
|
|
35
|
+
* first non-empty value wins:
|
|
36
|
+
* 1. `override` — the `terminalFontFamily` plugin setting;
|
|
37
|
+
* 2. `--dsh-ssh-terminal-font` — dedicated hook for skins and user CSS
|
|
38
|
+
* (e.g. a Nerd Font for powerline glyphs);
|
|
39
|
+
* 3. `--ds-font-family-code` — the official code-font token several skins
|
|
40
|
+
* already remap;
|
|
41
|
+
* 4. {@link TERMINAL_FONT_FALLBACK}.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveTerminalFontFamily(override?: string): string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface Config {
|
|
|
28
28
|
announceToAgent?: boolean;
|
|
29
29
|
/** Master switch for the plugin (routes, tools, prompt section). */
|
|
30
30
|
enabled?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* xterm `fontFamily` for the web terminal (issue #577). Empty (default)
|
|
33
|
+
* defers to the CSS chain: `--dsh-ssh-terminal-font`, then the official
|
|
34
|
+
* `--ds-font-family-code` token, then the built-in monospace stack. Set a
|
|
35
|
+
* Nerd Font stack here to render powerline/Nerd glyphs.
|
|
36
|
+
*/
|
|
37
|
+
terminalFontFamily?: string;
|
|
31
38
|
}
|
|
32
39
|
export declare const Config: z<Config>;
|
|
33
40
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -53,12 +53,13 @@
|
|
|
53
53
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
54
54
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
55
55
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.7",
|
|
57
56
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
58
57
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
59
58
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
59
|
+
"@deepseek-ai/dsh-scope": "^0.1.0-rc.7",
|
|
60
60
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
61
61
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
|
|
62
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
62
63
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
63
64
|
"@types/node": "^22.20.0",
|
|
64
65
|
"@types/react": "~18.3.1",
|
|
@@ -66,7 +67,6 @@
|
|
|
66
67
|
"@types/ssh2": "^1.15.5",
|
|
67
68
|
"@types/ws": "^8.5.13",
|
|
68
69
|
"jsdom": "^25.0.0",
|
|
69
|
-
"lightningcss": "1.32.0",
|
|
70
70
|
"react": "^18.3.1",
|
|
71
71
|
"react-dom": "^18.3.1",
|
|
72
72
|
"schemastery": "^3.18.0",
|
package/src/client/index.ts
CHANGED
|
@@ -10,20 +10,43 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
13
|
+
import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
14
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
16
|
+
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
16
18
|
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
17
19
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
18
20
|
import { SshApi } from './api.ts'
|
|
19
21
|
import { en, zh, type SshKey } from './locales.ts'
|
|
20
22
|
import { mountPanel } from './mount.tsx'
|
|
21
23
|
import { PanelController } from './panel/controller.ts'
|
|
24
|
+
import type { TerminalFontSource } from './panel/helpers.ts'
|
|
22
25
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
23
26
|
|
|
24
27
|
/** Locale namespace this plugin owns. */
|
|
25
28
|
const NS = 'dsh-ssh'
|
|
26
29
|
|
|
30
|
+
/** Settings namespace the terminal-font preference lives in (issue #577). */
|
|
31
|
+
const SETTINGS_NS = 'dsh-ssh'
|
|
32
|
+
|
|
33
|
+
/** The dsh-ssh settings surface the browser half reads. */
|
|
34
|
+
interface SshClientSettings {
|
|
35
|
+
/** User-configured xterm fontFamily; empty/undefined means the CSS chain. */
|
|
36
|
+
terminalFontFamily?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module '@deepseek-ai/cordis' {
|
|
40
|
+
interface Context {
|
|
41
|
+
/**
|
|
42
|
+
* Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
|
|
43
|
+
* absent when that group plugin is not installed, so callers fall back to
|
|
44
|
+
* the official settings scope.
|
|
45
|
+
*/
|
|
46
|
+
webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
28
51
|
interface LocaleNamespaceMap {
|
|
29
52
|
/** dsh-ssh surface copy. */
|
|
@@ -32,7 +55,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
35
|
-
export const inject = ['slots', 'locale']
|
|
58
|
+
export const inject = ['slots', 'locale', 'settingsScope']
|
|
36
59
|
|
|
37
60
|
/** Type-only surface (export discipline: no value exports beyond the plugin contract). */
|
|
38
61
|
export type { PanelControllerSnapshot } from './panel/controller.ts'
|
|
@@ -54,10 +77,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
54
77
|
|
|
55
78
|
const controller = new PanelController()
|
|
56
79
|
const api = new SshApi()
|
|
80
|
+
// Live terminal-font preference (issue #577): the settings namespace is
|
|
81
|
+
// edited by the host-registered GUI section; the panel re-applies changes
|
|
82
|
+
// to open terminals without a reconnect.
|
|
83
|
+
const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
|
|
84
|
+
const scope = binder.bind<SshClientSettings>({ namespace: SETTINGS_NS })
|
|
85
|
+
const terminalFont: TerminalFontSource = {
|
|
86
|
+
get: () => {
|
|
87
|
+
const snapshot = scope.getSnapshot()
|
|
88
|
+
return snapshot.status === 'ready' ? snapshot.value?.terminalFontFamily : undefined
|
|
89
|
+
},
|
|
90
|
+
subscribe: (listener) => scope.subscribe(listener),
|
|
91
|
+
}
|
|
57
92
|
const disposers: Array<() => void> = []
|
|
58
93
|
try {
|
|
59
94
|
disposers.push(mountSidebarEntry(controller))
|
|
60
|
-
disposers.push(mountPanel(controller, api))
|
|
95
|
+
disposers.push(mountPanel(controller, api, terminalFont))
|
|
61
96
|
} catch (error) {
|
|
62
97
|
// DOM failures degrade the panel, never the GUI.
|
|
63
98
|
console.warn('[dsh-ssh] mount failed:', error)
|
package/src/client/mount.tsx
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { createRoot, type Root } from 'react-dom/client'
|
|
15
15
|
import type { SshApi } from './api.ts'
|
|
16
16
|
import type { PanelController } from './panel/controller.ts'
|
|
17
|
+
import type { TerminalFontSource } from './panel/helpers.ts'
|
|
17
18
|
import { SshPanel } from './panel/SshPanel.tsx'
|
|
18
19
|
import css from './panel/panel.module.css'
|
|
19
20
|
|
|
@@ -38,9 +39,10 @@ function conversationColumn(): HTMLElement | undefined {
|
|
|
38
39
|
* to the controller's panelOpen state.
|
|
39
40
|
* @param controller - the panel controller driving the view.
|
|
40
41
|
* @param api - the SSH API client the tabs operate through.
|
|
42
|
+
* @param terminalFont - live terminal-font setting source (issue #577).
|
|
41
43
|
* @returns disposer unmounting the tree and restoring the column.
|
|
42
44
|
*/
|
|
43
|
-
export function mountPanel(controller: PanelController, api: SshApi): () => void {
|
|
45
|
+
export function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource): () => void {
|
|
44
46
|
let root: Root | undefined
|
|
45
47
|
let container: HTMLDivElement | undefined
|
|
46
48
|
|
|
@@ -61,7 +63,7 @@ export function mountPanel(controller: PanelController, api: SshApi): () => void
|
|
|
61
63
|
container.className = css.view
|
|
62
64
|
column.appendChild(container)
|
|
63
65
|
root = createRoot(container)
|
|
64
|
-
root.render(<SshPanel controller={controller} api={api} />)
|
|
66
|
+
root.render(<SshPanel controller={controller} api={api} terminalFont={terminalFont} />)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
// The frame mounts after boot settlement; watch for the column's arrival.
|
|
@@ -72,16 +72,24 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
|
|
|
72
72
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
|
73
73
|
const [testingGroup, setTestingGroup] = useState<string | null>(null)
|
|
74
74
|
const seqRef = useRef(0)
|
|
75
|
+
// Unmount guard for the async load below: the seq check only orders
|
|
76
|
+
// overlapping loads, it does not stop a late resolution/rejection landing
|
|
77
|
+
// after the tab unmounted — a setState there races the test-environment
|
|
78
|
+
// teardown (window is not defined; observed as a main-CI flake). The
|
|
79
|
+
// sibling tabs (terminal / transfer / tunnels) already guard with a
|
|
80
|
+
// disposed flag.
|
|
81
|
+
const mountedRef = useRef(true)
|
|
82
|
+
useEffect(() => () => { mountedRef.current = false }, [])
|
|
75
83
|
|
|
76
84
|
const load = useCallback(async (query?: string): Promise<void> => {
|
|
77
85
|
const seq = ++seqRef.current
|
|
78
86
|
try {
|
|
79
87
|
const list = await api.listHosts(query)
|
|
80
|
-
if (seq !== seqRef.current) return
|
|
88
|
+
if (!mountedRef.current || seq !== seqRef.current) return
|
|
81
89
|
setHosts(list)
|
|
82
90
|
setError(null)
|
|
83
91
|
} catch (cause) {
|
|
84
|
-
if (seq !== seqRef.current) return
|
|
92
|
+
if (!mountedRef.current || seq !== seqRef.current) return
|
|
85
93
|
setError(errorMessage(cause))
|
|
86
94
|
}
|
|
87
95
|
}, [api])
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { useState } from 'react'
|
|
9
9
|
import type { SshApi } from '../api.ts'
|
|
10
10
|
import type { PanelController } from './controller.ts'
|
|
11
|
-
import { tt } from './helpers.ts'
|
|
11
|
+
import { tt, type TerminalFontSource } from './helpers.ts'
|
|
12
12
|
import { ClusterTab } from './ClusterTab.tsx'
|
|
13
13
|
import { HostsTab } from './HostsTab.tsx'
|
|
14
14
|
import { TerminalTab } from './TerminalTab.tsx'
|
|
@@ -25,6 +25,8 @@ export interface SshPanelProps {
|
|
|
25
25
|
controller: PanelController
|
|
26
26
|
/** The SSH API client every tab operates through. */
|
|
27
27
|
api: SshApi
|
|
28
|
+
/** Live terminal-font setting source handed to the terminal tab (issue #577). */
|
|
29
|
+
terminalFont?: TerminalFontSource
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** The tab bar definition (labels resolved at render time). */
|
|
@@ -43,7 +45,7 @@ interface ConnectRequest {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** The tabbed SSH panel. */
|
|
46
|
-
export function SshPanel({ controller, api }: SshPanelProps) {
|
|
48
|
+
export function SshPanel({ controller, api, terminalFont }: SshPanelProps) {
|
|
47
49
|
const [activeTab, setActiveTab] = useState<SshTab>('hosts')
|
|
48
50
|
const [connectRequest, setConnectRequest] = useState<ConnectRequest | null>(null)
|
|
49
51
|
|
|
@@ -75,7 +77,7 @@ export function SshPanel({ controller, api }: SshPanelProps) {
|
|
|
75
77
|
</div>
|
|
76
78
|
<div className={css.panelContent}>
|
|
77
79
|
{activeTab === 'hosts' && <HostsTab api={api} onConnect={handleConnect} />}
|
|
78
|
-
{activeTab === 'terminal' && <TerminalTab api={api} presetAlias={connectRequest?.alias} requestId={connectRequest?.nonce} />}
|
|
80
|
+
{activeTab === 'terminal' && <TerminalTab api={api} presetAlias={connectRequest?.alias} requestId={connectRequest?.nonce} terminalFont={terminalFont} />}
|
|
79
81
|
{activeTab === 'transfer' && <TransferTab api={api} />}
|
|
80
82
|
{activeTab === 'tunnels' && <TunnelsTab api={api} />}
|
|
81
83
|
{activeTab === 'cluster' && <ClusterTab api={api} />}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* output stays visible and input is disabled. xterm's stylesheet is injected
|
|
6
6
|
* once per page load (module-level guard).
|
|
7
7
|
*/
|
|
8
|
-
import { useEffect, useRef, useState } from 'react'
|
|
8
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
9
9
|
import { Terminal, type IDisposable } from '@xterm/xterm'
|
|
10
10
|
import { FitAddon } from '@xterm/addon-fit'
|
|
11
11
|
import type { SshApi, TerminalConnection } from '../api.ts'
|
|
12
12
|
import type { SshHostSummary } from '../../protocol.ts'
|
|
13
13
|
import { XTERM_CSS } from './xterm.css.ts'
|
|
14
|
-
import { errorMessage, tt } from './helpers.ts'
|
|
14
|
+
import { errorMessage, resolveTerminalFontFamily, tt, type TerminalFontSource } from './helpers.ts'
|
|
15
15
|
import css from './panel.module.css'
|
|
16
16
|
|
|
17
17
|
/** Terminal tab props. */
|
|
@@ -21,6 +21,11 @@ export interface TerminalTabProps {
|
|
|
21
21
|
presetAlias?: string
|
|
22
22
|
/** Monotonic id of the connect request (re-applies presetAlias). */
|
|
23
23
|
requestId?: number
|
|
24
|
+
/**
|
|
25
|
+
* Live terminal-font setting source (issue #577). Absent in tests and
|
|
26
|
+
* legacy mounts: the font then comes from the CSS custom-property chain.
|
|
27
|
+
*/
|
|
28
|
+
terminalFont?: TerminalFontSource
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/** The terminal session lifecycle state shown in the status banner. */
|
|
@@ -44,8 +49,14 @@ function ensureXtermCss(): void {
|
|
|
44
49
|
document.head.appendChild(style)
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** No-op source stand-in so the hook order stays stable without the prop. */
|
|
53
|
+
const NO_FONT_SOURCE: TerminalFontSource = {
|
|
54
|
+
get: () => undefined,
|
|
55
|
+
subscribe: () => () => undefined,
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
/** The xterm terminal view. */
|
|
48
|
-
export function TerminalTab({ api, presetAlias, requestId }: TerminalTabProps) {
|
|
59
|
+
export function TerminalTab({ api, presetAlias, requestId, terminalFont }: TerminalTabProps) {
|
|
49
60
|
const [hosts, setHosts] = useState<SshHostSummary[]>([])
|
|
50
61
|
const [alias, setAlias] = useState(presetAlias ?? '')
|
|
51
62
|
const [status, setStatus] = useState<TerminalStatus>({ kind: 'idle' })
|
|
@@ -54,9 +65,24 @@ export function TerminalTab({ api, presetAlias, requestId }: TerminalTabProps) {
|
|
|
54
65
|
const fitRef = useRef<FitAddon | null>(null)
|
|
55
66
|
const connRef = useRef<TerminalConnection | null>(null)
|
|
56
67
|
const dataSubRef = useRef<IDisposable | null>(null)
|
|
68
|
+
const fontSource = terminalFont ?? NO_FONT_SOURCE
|
|
69
|
+
const fontOverride = useSyncExternalStore(fontSource.subscribe, fontSource.get)
|
|
57
70
|
|
|
58
71
|
useEffect(() => { ensureXtermCss() }, [])
|
|
59
72
|
|
|
73
|
+
// Live re-apply a terminal-font change (issue #577): xterm re-measures
|
|
74
|
+
// and repaints on the options write; a refit keeps cols/rows aligned with
|
|
75
|
+
// the new metrics and the remote PTY learns the new size.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const term = termRef.current
|
|
78
|
+
if (term === null) return
|
|
79
|
+
const next = resolveTerminalFontFamily(fontOverride)
|
|
80
|
+
if (term.options.fontFamily === next) return
|
|
81
|
+
term.options.fontFamily = next
|
|
82
|
+
fitRef.current?.fit()
|
|
83
|
+
connRef.current?.resize(term.cols, term.rows)
|
|
84
|
+
}, [fontOverride])
|
|
85
|
+
|
|
60
86
|
// Fetch the host list on tab activation.
|
|
61
87
|
useEffect(() => {
|
|
62
88
|
let disposed = false
|
|
@@ -122,7 +148,7 @@ export function TerminalTab({ api, presetAlias, requestId }: TerminalTabProps) {
|
|
|
122
148
|
convertEol: false,
|
|
123
149
|
cursorBlink: true,
|
|
124
150
|
fontSize: 13,
|
|
125
|
-
fontFamily:
|
|
151
|
+
fontFamily: resolveTerminalFontFamily(fontOverride),
|
|
126
152
|
theme: { background: '#0b0e14', foreground: '#d8dee9', cursor: '#a3b8d0' },
|
|
127
153
|
})
|
|
128
154
|
const fit = new FitAddon()
|
|
@@ -137,7 +163,7 @@ export function TerminalTab({ api, presetAlias, requestId }: TerminalTabProps) {
|
|
|
137
163
|
dataSubRef.current = term.onData(data => { connection.send(data) })
|
|
138
164
|
connection.onReady = () => { setStatus({ kind: 'connected', alias: target }) }
|
|
139
165
|
connection.onOutput = data => { term.write(data) }
|
|
140
|
-
connection.onExit = (
|
|
166
|
+
connection.onExit = (_code, error) => {
|
|
141
167
|
if (settled) return
|
|
142
168
|
settled = true
|
|
143
169
|
dataSubRef.current?.dispose()
|
|
@@ -24,3 +24,47 @@ export function errorMessage(error: unknown): string {
|
|
|
24
24
|
if (error instanceof Error) return error.message
|
|
25
25
|
return String(error)
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The terminal font stack used before issue #577, kept as the last resort
|
|
30
|
+
* when neither the plugin setting nor a CSS custom property names a font.
|
|
31
|
+
*/
|
|
32
|
+
export const TERMINAL_FONT_FALLBACK = 'Menlo, Consolas, "Liberation Mono", monospace'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Live source of the user's configured terminal font family: the client
|
|
36
|
+
* entry binds the dsh-ssh settings namespace into one of these and hands it
|
|
37
|
+
* to the panel, so a settings change re-applies without a remount.
|
|
38
|
+
*/
|
|
39
|
+
export interface TerminalFontSource {
|
|
40
|
+
/** Current configured family, or undefined when unset / not yet loaded. */
|
|
41
|
+
get(): string | undefined
|
|
42
|
+
/** Subscribe to changes; the returned function unsubscribes. */
|
|
43
|
+
subscribe(listener: () => void): () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the xterm `fontFamily` (issue #577). xterm's DOM renderer takes
|
|
48
|
+
* the font only from constructor/options, so a plain stylesheet rule cannot
|
|
49
|
+
* retarget it — the value must be read back into the options. The chain,
|
|
50
|
+
* first non-empty value wins:
|
|
51
|
+
* 1. `override` — the `terminalFontFamily` plugin setting;
|
|
52
|
+
* 2. `--dsh-ssh-terminal-font` — dedicated hook for skins and user CSS
|
|
53
|
+
* (e.g. a Nerd Font for powerline glyphs);
|
|
54
|
+
* 3. `--ds-font-family-code` — the official code-font token several skins
|
|
55
|
+
* already remap;
|
|
56
|
+
* 4. {@link TERMINAL_FONT_FALLBACK}.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveTerminalFontFamily(override?: string): string {
|
|
59
|
+
const trimmed = override?.trim()
|
|
60
|
+
if (trimmed !== undefined && trimmed !== '') return trimmed
|
|
61
|
+
if (typeof getComputedStyle === 'function' && typeof document !== 'undefined') {
|
|
62
|
+
const target = document.body ?? document.documentElement
|
|
63
|
+
const style = getComputedStyle(target)
|
|
64
|
+
for (const name of ['--dsh-ssh-terminal-font', '--ds-font-family-code']) {
|
|
65
|
+
const value = style.getPropertyValue(name).trim()
|
|
66
|
+
if (value !== '') return value
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return TERMINAL_FONT_FALLBACK
|
|
70
|
+
}
|
package/src/engine/pty.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -42,11 +42,19 @@ export interface Config {
|
|
|
42
42
|
announceToAgent?: boolean
|
|
43
43
|
/** Master switch for the plugin (routes, tools, prompt section). */
|
|
44
44
|
enabled?: boolean
|
|
45
|
+
/**
|
|
46
|
+
* xterm `fontFamily` for the web terminal (issue #577). Empty (default)
|
|
47
|
+
* defers to the CSS chain: `--dsh-ssh-terminal-font`, then the official
|
|
48
|
+
* `--ds-font-family-code` token, then the built-in monospace stack. Set a
|
|
49
|
+
* Nerd Font stack here to render powerline/Nerd glyphs.
|
|
50
|
+
*/
|
|
51
|
+
terminalFontFamily?: string
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export const Config: z<Config> = z.object({
|
|
48
55
|
announceToAgent: z.boolean().default(true),
|
|
49
56
|
enabled: z.boolean().default(true),
|
|
57
|
+
terminalFontFamily: z.string().default(''),
|
|
50
58
|
})
|
|
51
59
|
|
|
52
60
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|