@linxin666/dsh-ssh 0.2.1 → 0.2.2

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/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;
@@ -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.1",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -53,7 +53,6 @@
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",
@@ -66,7 +65,6 @@
66
65
  "@types/ssh2": "^1.15.5",
67
66
  "@types/ws": "^8.5.13",
68
67
  "jsdom": "^25.0.0",
69
- "lightningcss": "1.32.0",
70
68
  "react": "^18.3.1",
71
69
  "react-dom": "^18.3.1",
72
70
  "schemastery": "^3.18.0",
@@ -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)
@@ -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: 'Menlo, Consolas, "Liberation Mono", monospace',
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 = (code, error) => {
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
@@ -4,7 +4,6 @@
4
4
  * output delivery.
5
5
  */
6
6
 
7
- import { Client } from 'ssh2'
8
7
  import { connectChain, type PoolEngine } from './connection-pool.ts'
9
8
 
10
9
  /** A live PTY shell session. */
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). */