@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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 4f2f068da6af190caeadf8deeffce24d138c6b37
6
- README.zh.md: d0090dd49663cf397e4980b70127659cb041fea9
5
+ README.md: f9476df50bac571b567512cfa5272b5a64a1c3d6
6
+ README.zh.md: ec991222c0a38f2376acdd21ae52fe185facd0c2
package/README.md CHANGED
@@ -48,7 +48,7 @@ After installing, **restart `dsh web`**: a "SSH" entry appears in the sidebar; t
48
48
 
49
49
  ## Configuration
50
50
 
51
- The settings panel (plugin config) toggles `announceToAgent` (whether to announce the plugin to the Agent) and `enabled` (master switch).
51
+ The settings panel (plugin config) toggles `announceToAgent` (whether to announce the plugin to the Agent) and `enabled` (master switch), and sets `terminalFontFamily` (the web terminal font; empty defers to the CSS chain: `--dsh-ssh-terminal-font` → the official `--ds-font-family-code` token → the built-in monospace stack). The terminal font is fixed in the xterm constructor, so a plain stylesheet cannot override it; to render powerline / Nerd Font glyphs, enter a Nerd Font stack here (e.g. `"SauceCodePro Nerd Font", monospace`). Changes re-apply to open terminals live, no reconnect needed.
52
52
 
53
53
  ## Data
54
54
 
package/README.zh.md CHANGED
@@ -48,7 +48,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-ssh
48
48
 
49
49
  ## 配置
50
50
 
51
- 设置面板(插件配置)可开关 `announceToAgent`(是否向 Agent 宣告插件)与 `enabled`(总开关)。
51
+ 设置面板(插件配置)可开关 `announceToAgent`(是否向 Agent 宣告插件)与 `enabled`(总开关),并可设置 `terminalFontFamily`(Web 终端字体,留空则按 CSS 链解析:`--dsh-ssh-terminal-font` → 官方 `--ds-font-family-code` token → 内置 monospace 栈)。终端字体写死在 xterm 构造参数里,CSS 无法直接覆盖;要渲染 powerline / Nerd Font 图标,请在此填入对应 Nerd Font 栈(如 `"SauceCodePro Nerd Font", monospace`),修改对已打开的终端即时生效,无需重连。
52
52
 
53
53
  ## 数据
54
54
 
package/lib/client.js CHANGED
@@ -609,6 +609,36 @@ window.__ModuleLoader__.load({
609
609
  if (error instanceof Error) return error.message;
610
610
  return String(error);
611
611
  }
612
+ /**
613
+ * The terminal font stack used before issue #577, kept as the last resort
614
+ * when neither the plugin setting nor a CSS custom property names a font.
615
+ */
616
+ const TERMINAL_FONT_FALLBACK = "Menlo, Consolas, \"Liberation Mono\", monospace";
617
+ /**
618
+ * Resolve the xterm `fontFamily` (issue #577). xterm's DOM renderer takes
619
+ * the font only from constructor/options, so a plain stylesheet rule cannot
620
+ * retarget it — the value must be read back into the options. The chain,
621
+ * first non-empty value wins:
622
+ * 1. `override` — the `terminalFontFamily` plugin setting;
623
+ * 2. `--dsh-ssh-terminal-font` — dedicated hook for skins and user CSS
624
+ * (e.g. a Nerd Font for powerline glyphs);
625
+ * 3. `--ds-font-family-code` — the official code-font token several skins
626
+ * already remap;
627
+ * 4. {@link TERMINAL_FONT_FALLBACK}.
628
+ */
629
+ function resolveTerminalFontFamily(override) {
630
+ const trimmed = override?.trim();
631
+ if (trimmed !== void 0 && trimmed !== "") return trimmed;
632
+ if (typeof getComputedStyle === "function" && typeof document !== "undefined") {
633
+ const target = document.body ?? document.documentElement;
634
+ const style = getComputedStyle(target);
635
+ for (const name of ["--dsh-ssh-terminal-font", "--ds-font-family-code"]) {
636
+ const value = style.getPropertyValue(name).trim();
637
+ if (value !== "") return value;
638
+ }
639
+ }
640
+ return TERMINAL_FONT_FALLBACK;
641
+ }
612
642
  //#endregion
613
643
  //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
614
644
  const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}.mL8Uca_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel{display:none}.mL8Uca_view{overflow:hidden}.mL8Uca_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.mL8Uca_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.mL8Uca_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.mL8Uca_backButton{align-items:center;gap:4px;display:inline-flex}.mL8Uca_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.mL8Uca_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.mL8Uca_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.mL8Uca_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.mL8Uca_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}.mL8Uca_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.mL8Uca_toolbar,.mL8Uca_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.mL8Uca_controls .mL8Uca_input{flex:0 260px;min-width:150px}.mL8Uca_toolbarSpacer{flex:1}.mL8Uca_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.mL8Uca_search::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_groupBySelect{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;padding:6px 8px;font-size:13px}.mL8Uca_groupSection+.mL8Uca_groupSection{border-top:1px solid var(--dsw-alias-border-l1)}.mL8Uca_groupHeader{background:var(--dsw-alias-bg-base,var(--dsw-specific-input-major));align-items:center;gap:8px;padding:4px 8px;display:flex;position:sticky;top:0}.mL8Uca_groupToggle{min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;flex:1;align-items:center;gap:6px;padding:4px 6px;font-size:12.5px;font-weight:500;display:flex}.mL8Uca_groupToggle:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_groupChevron{border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);flex:none;width:7px;height:7px;margin-bottom:2px;transition:transform .12s;transform:rotate(45deg)}.mL8Uca_groupChevron[data-collapsed]{margin-bottom:0;transform:rotate(-45deg)}.mL8Uca_groupName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mL8Uca_groupCount{color:var(--dsw-alias-label-tertiary);flex:none;font-weight:400}.mL8Uca_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.mL8Uca_table{border-collapse:collapse;width:100%;font-size:12.5px}.mL8Uca_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.mL8Uca_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.mL8Uca_table tbody tr:last-child td{border-bottom:none}.mL8Uca_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellMuted{color:var(--dsw-alias-label-tertiary)}.mL8Uca_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.mL8Uca_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.mL8Uca_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.mL8Uca_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.mL8Uca_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.mL8Uca_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.mL8Uca_primaryButton:disabled{opacity:.5;cursor:default}.mL8Uca_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.mL8Uca_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_ghostButton:disabled{opacity:.45;cursor:default}.mL8Uca_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.mL8Uca_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.mL8Uca_linkButton:hover:not(:disabled){text-decoration:underline}.mL8Uca_linkButton:disabled{opacity:.45;cursor:default}.mL8Uca_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite mL8Uca_dshSshSpin;display:inline-block}@keyframes mL8Uca_dshSshSpin{to{transform:rotate(360deg)}}.mL8Uca_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.mL8Uca_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_empty,.mL8Uca_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.mL8Uca_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.mL8Uca_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.mL8Uca_modalTitle{margin:0;font-size:15px;font-weight:700}.mL8Uca_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.mL8Uca_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.mL8Uca_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.mL8Uca_field{flex-direction:column;gap:5px;display:flex}.mL8Uca_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.mL8Uca_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.mL8Uca_input:focus{border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_input::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_input:disabled{opacity:.55}.mL8Uca_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}.mL8Uca_radioRow{align-items:center;gap:16px;display:flex}.mL8Uca_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.mL8Uca_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.mL8Uca_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.mL8Uca_termContainer{padding:8px 10px;position:absolute;inset:0}.mL8Uca_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.mL8Uca_hiddenFile{display:none}.mL8Uca_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.mL8Uca_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.mL8Uca_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.mL8Uca_browseList{max-height:240px;overflow-y:auto}.mL8Uca_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.mL8Uca_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_dirRow[data-type=dir] .mL8Uca_dirName{color:var(--dsw-alias-state-business-primary)}.mL8Uca_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.mL8Uca_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.mL8Uca_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.mL8Uca_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.mL8Uca_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.mL8Uca_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.mL8Uca_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.mL8Uca_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.mL8Uca_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.mL8Uca_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.mL8Uca_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.mL8Uca_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.mL8Uca_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.mL8Uca_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
@@ -1300,15 +1330,19 @@ window.__ModuleLoader__.load({
1300
1330
  const [collapsed, setCollapsed] = (0, react.useState)({});
1301
1331
  const [testingGroup, setTestingGroup] = (0, react.useState)(null);
1302
1332
  const seqRef = (0, react.useRef)(0);
1333
+ const mountedRef = (0, react.useRef)(true);
1334
+ (0, react.useEffect)(() => () => {
1335
+ mountedRef.current = false;
1336
+ }, []);
1303
1337
  const load = (0, react.useCallback)(async (query) => {
1304
1338
  const seq = ++seqRef.current;
1305
1339
  try {
1306
1340
  const list = await api.listHosts(query);
1307
- if (seq !== seqRef.current) return;
1341
+ if (!mountedRef.current || seq !== seqRef.current) return;
1308
1342
  setHosts(list);
1309
1343
  setError(null);
1310
1344
  } catch (cause) {
1311
- if (seq !== seqRef.current) return;
1345
+ if (!mountedRef.current || seq !== seqRef.current) return;
1312
1346
  setError(errorMessage(cause));
1313
1347
  }
1314
1348
  }, [api]);
@@ -14677,8 +14711,13 @@ window.__ModuleLoader__.load({
14677
14711
  style.textContent = XTERM_CSS;
14678
14712
  document.head.appendChild(style);
14679
14713
  }
14714
+ /** No-op source stand-in so the hook order stays stable without the prop. */
14715
+ const NO_FONT_SOURCE = {
14716
+ get: () => void 0,
14717
+ subscribe: () => () => void 0
14718
+ };
14680
14719
  /** The xterm terminal view. */
14681
- function TerminalTab({ api, presetAlias, requestId }) {
14720
+ function TerminalTab({ api, presetAlias, requestId, terminalFont }) {
14682
14721
  const [hosts, setHosts] = (0, react.useState)([]);
14683
14722
  const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
14684
14723
  const [status, setStatus] = (0, react.useState)({ kind: "idle" });
@@ -14687,9 +14726,20 @@ window.__ModuleLoader__.load({
14687
14726
  const fitRef = (0, react.useRef)(null);
14688
14727
  const connRef = (0, react.useRef)(null);
14689
14728
  const dataSubRef = (0, react.useRef)(null);
14729
+ const fontSource = terminalFont ?? NO_FONT_SOURCE;
14730
+ const fontOverride = (0, react.useSyncExternalStore)(fontSource.subscribe, fontSource.get);
14690
14731
  (0, react.useEffect)(() => {
14691
14732
  ensureXtermCss();
14692
14733
  }, []);
14734
+ (0, react.useEffect)(() => {
14735
+ const term = termRef.current;
14736
+ if (term === null) return;
14737
+ const next = resolveTerminalFontFamily(fontOverride);
14738
+ if (term.options.fontFamily === next) return;
14739
+ term.options.fontFamily = next;
14740
+ fitRef.current?.fit();
14741
+ connRef.current?.resize(term.cols, term.rows);
14742
+ }, [fontOverride]);
14693
14743
  (0, react.useEffect)(() => {
14694
14744
  let disposed = false;
14695
14745
  (async () => {
@@ -14752,7 +14802,7 @@ window.__ModuleLoader__.load({
14752
14802
  convertEol: false,
14753
14803
  cursorBlink: true,
14754
14804
  fontSize: 13,
14755
- fontFamily: "Menlo, Consolas, \"Liberation Mono\", monospace",
14805
+ fontFamily: resolveTerminalFontFamily(fontOverride),
14756
14806
  theme: {
14757
14807
  background: "#0b0e14",
14758
14808
  foreground: "#d8dee9",
@@ -14780,7 +14830,7 @@ window.__ModuleLoader__.load({
14780
14830
  connection.onOutput = (data) => {
14781
14831
  term.write(data);
14782
14832
  };
14783
- connection.onExit = (code, error) => {
14833
+ connection.onExit = (_code, error) => {
14784
14834
  if (settled) return;
14785
14835
  settled = true;
14786
14836
  dataSubRef.current?.dispose();
@@ -15501,7 +15551,7 @@ window.__ModuleLoader__.load({
15501
15551
  }
15502
15552
  ];
15503
15553
  /** The tabbed SSH panel. */
15504
- function SshPanel({ controller, api }) {
15554
+ function SshPanel({ controller, api, terminalFont }) {
15505
15555
  const [activeTab, setActiveTab] = (0, react.useState)("hosts");
15506
15556
  const [connectRequest, setConnectRequest] = (0, react.useState)(null);
15507
15557
  const handleConnect = (alias) => {
@@ -15558,7 +15608,8 @@ window.__ModuleLoader__.load({
15558
15608
  activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
15559
15609
  api,
15560
15610
  presetAlias: connectRequest?.alias,
15561
- requestId: connectRequest?.nonce
15611
+ requestId: connectRequest?.nonce,
15612
+ terminalFont
15562
15613
  }),
15563
15614
  activeTab === "transfer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferTab, { api }),
15564
15615
  activeTab === "tunnels" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TunnelsTab, { api }),
@@ -15599,9 +15650,10 @@ window.__ModuleLoader__.load({
15599
15650
  * to the controller's panelOpen state.
15600
15651
  * @param controller - the panel controller driving the view.
15601
15652
  * @param api - the SSH API client the tabs operate through.
15653
+ * @param terminalFont - live terminal-font setting source (issue #577).
15602
15654
  * @returns disposer unmounting the tree and restoring the column.
15603
15655
  */
15604
- function mountPanel(controller, api) {
15656
+ function mountPanel(controller, api, terminalFont) {
15605
15657
  let root;
15606
15658
  let container;
15607
15659
  const ensure = () => {
@@ -15622,7 +15674,8 @@ window.__ModuleLoader__.load({
15622
15674
  root = (0, react_dom_client.createRoot)(container);
15623
15675
  root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
15624
15676
  controller,
15625
- api
15677
+ api,
15678
+ terminalFont
15626
15679
  }));
15627
15680
  };
15628
15681
  const waitObserver = new MutationObserver(() => {
@@ -15841,8 +15894,14 @@ window.__ModuleLoader__.load({
15841
15894
  //#region src/client/index.ts
15842
15895
  /** Locale namespace this plugin owns. */
15843
15896
  const NS = "dsh-ssh";
15897
+ /** Settings namespace the terminal-font preference lives in (issue #577). */
15898
+ const SETTINGS_NS = "dsh-ssh";
15844
15899
  /** Required services (fiber inject waiting — the runtime must be up first). */
15845
- const inject = ["slots", "locale"];
15900
+ const inject = [
15901
+ "slots",
15902
+ "locale",
15903
+ "settingsScope"
15904
+ ];
15846
15905
  /**
15847
15906
  * Mount the SSH panel.
15848
15907
  * @param ctx - client root context (locale service).
@@ -15854,10 +15913,18 @@ window.__ModuleLoader__.load({
15854
15913
  }), "dsh-ssh: dictionaries");
15855
15914
  const controller = new PanelController();
15856
15915
  const api = new SshApi();
15916
+ const scope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: SETTINGS_NS });
15917
+ const terminalFont = {
15918
+ get: () => {
15919
+ const snapshot = scope.getSnapshot();
15920
+ return snapshot.status === "ready" ? snapshot.value?.terminalFontFamily : void 0;
15921
+ },
15922
+ subscribe: (listener) => scope.subscribe(listener)
15923
+ };
15857
15924
  const disposers = [];
15858
15925
  try {
15859
15926
  disposers.push(mountSidebarEntry(controller));
15860
- disposers.push(mountPanel(controller, api));
15927
+ disposers.push(mountPanel(controller, api, terminalFont));
15861
15928
  } catch (error) {
15862
15929
  console.warn("[dsh-ssh] mount failed:", error);
15863
15930
  }