@linxin666/dsh-ssh 0.2.0 → 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 +2 -2
- package/README.md +5 -5
- package/README.zh.md +3 -3
- package/lib/client.js +145 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +33 -23
- 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/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -11
- package/lib/types/index.d.ts +7 -0
- package/package.json +1 -3
- package/src/client/index.ts +38 -3
- package/src/client/mount.tsx +5 -2
- package/src/client/panel/HostsTab.tsx +10 -2
- package/src/client/panel/SshPanel.tsx +6 -4
- package/src/client/panel/TerminalTab.tsx +32 -6
- package/src/client/panel/helpers.ts +44 -0
- package/src/client/sidebar-entry-core.ts +208 -0
- package/src/client/sidebar-entry.ts +23 -138
- package/src/engine/connection-pool.ts +27 -21
- package/src/engine/pty.ts +0 -1
- package/src/index.ts +8 -0
- package/src/routes.ts +8 -3
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: f9476df50bac571b567512cfa5272b5a64a1c3d6
|
|
6
|
+
README.zh.md: ec991222c0a38f2376acdd21ae52fe185facd0c2
|
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ Built on the capability list of [badseal/ssh-skill](https://github.com/badseal/s
|
|
|
33
33
|
Install the family aggregate package `@linxin666/dsh-web-ui-all` (all plugins and skins in one) or this plugin alone:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
###
|
|
37
|
-
dsh plugin --profile web add @linxin666/dsh-ssh
|
|
36
|
+
### From npm (recommended)
|
|
37
|
+
dsh plugin --profile web add @linxin666/dsh-ssh@latest
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### From the repository (development)
|
|
40
40
|
git clone https://github.com/zhu1090093659/dsh-web-ui.git
|
|
41
41
|
cd dsh-web-ui
|
|
42
42
|
pnpm install && pnpm -r build
|
|
@@ -48,12 +48,12 @@ 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
|
|
|
55
55
|
- Host config: `~/.dsh/dsh-ssh.json` (versioned JSON, atomic write)
|
|
56
|
-
- Transfer staging: `os.tmpdir()/dsh-ssh-uploads/`
|
|
56
|
+
- Transfer staging: `os.tmpdir()/dsh-ssh-uploads/` (0700 directory, 0600 in-flight files)
|
|
57
57
|
|
|
58
58
|
## Development
|
|
59
59
|
|
package/README.zh.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
36
|
### 从 npm 安装(推荐)
|
|
37
|
-
dsh plugin --profile web add @linxin666/dsh-ssh
|
|
37
|
+
dsh plugin --profile web add @linxin666/dsh-ssh@latest
|
|
38
38
|
|
|
39
39
|
### 从仓库安装(开发调试)
|
|
40
40
|
git clone https://github.com/zhu1090093659/dsh-web-ui.git
|
|
@@ -48,12 +48,12 @@ 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
|
|
|
55
55
|
- 主机配置:`~/.dsh/dsh-ssh.json`(版本化 JSON,原子写入)
|
|
56
|
-
- 传输暂存:`os.tmpdir()/dsh-ssh-uploads
|
|
56
|
+
- 传输暂存:`os.tmpdir()/dsh-ssh-uploads/`(目录 0700,传输中的文件 0600)
|
|
57
57
|
|
|
58
58
|
## 开发
|
|
59
59
|
|
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:
|
|
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 = (
|
|
14833
|
+
connection.onExit = (_code, error) => {
|
|
14784
14834
|
if (settled) return;
|
|
14785
14835
|
settled = true;
|
|
14786
14836
|
dataSubRef.current?.dispose();
|
|
@@ -14864,7 +14914,8 @@ window.__ModuleLoader__.load({
|
|
|
14864
14914
|
className: panel_module_css_default.termWrap,
|
|
14865
14915
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14866
14916
|
ref: containerRef,
|
|
14867
|
-
className: panel_module_css_default.termContainer
|
|
14917
|
+
className: panel_module_css_default.termContainer,
|
|
14918
|
+
"data-dsh-part": "terminal"
|
|
14868
14919
|
}), status.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14869
14920
|
className: panel_module_css_default.termPlaceholder,
|
|
14870
14921
|
children: hosts.length === 0 ? tt("hosts.empty") : tt("terminal.placeholder")
|
|
@@ -15500,7 +15551,7 @@ window.__ModuleLoader__.load({
|
|
|
15500
15551
|
}
|
|
15501
15552
|
];
|
|
15502
15553
|
/** The tabbed SSH panel. */
|
|
15503
|
-
function SshPanel({ controller, api }) {
|
|
15554
|
+
function SshPanel({ controller, api, terminalFont }) {
|
|
15504
15555
|
const [activeTab, setActiveTab] = (0, react.useState)("hosts");
|
|
15505
15556
|
const [connectRequest, setConnectRequest] = (0, react.useState)(null);
|
|
15506
15557
|
const handleConnect = (alias) => {
|
|
@@ -15534,6 +15585,7 @@ window.__ModuleLoader__.load({
|
|
|
15534
15585
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15535
15586
|
className: panel_module_css_default.tabBar,
|
|
15536
15587
|
role: "tablist",
|
|
15588
|
+
"data-dsh-part": "tab-bar",
|
|
15537
15589
|
children: TABS.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15538
15590
|
type: "button",
|
|
15539
15591
|
role: "tab",
|
|
@@ -15556,7 +15608,8 @@ window.__ModuleLoader__.load({
|
|
|
15556
15608
|
activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
|
|
15557
15609
|
api,
|
|
15558
15610
|
presetAlias: connectRequest?.alias,
|
|
15559
|
-
requestId: connectRequest?.nonce
|
|
15611
|
+
requestId: connectRequest?.nonce,
|
|
15612
|
+
terminalFont
|
|
15560
15613
|
}),
|
|
15561
15614
|
activeTab === "transfer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferTab, { api }),
|
|
15562
15615
|
activeTab === "tunnels" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TunnelsTab, { api }),
|
|
@@ -15597,9 +15650,10 @@ window.__ModuleLoader__.load({
|
|
|
15597
15650
|
* to the controller's panelOpen state.
|
|
15598
15651
|
* @param controller - the panel controller driving the view.
|
|
15599
15652
|
* @param api - the SSH API client the tabs operate through.
|
|
15653
|
+
* @param terminalFont - live terminal-font setting source (issue #577).
|
|
15600
15654
|
* @returns disposer unmounting the tree and restoring the column.
|
|
15601
15655
|
*/
|
|
15602
|
-
function mountPanel(controller, api) {
|
|
15656
|
+
function mountPanel(controller, api, terminalFont) {
|
|
15603
15657
|
let root;
|
|
15604
15658
|
let container;
|
|
15605
15659
|
const ensure = () => {
|
|
@@ -15614,12 +15668,14 @@ window.__ModuleLoader__.load({
|
|
|
15614
15668
|
if (column === void 0) return;
|
|
15615
15669
|
container = document.createElement("div");
|
|
15616
15670
|
container.dataset.dshSshView = "";
|
|
15671
|
+
container.dataset.dshPlugin = "ssh";
|
|
15617
15672
|
container.className = panel_module_css_default.view;
|
|
15618
15673
|
column.appendChild(container);
|
|
15619
15674
|
root = (0, react_dom_client.createRoot)(container);
|
|
15620
15675
|
root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
|
|
15621
15676
|
controller,
|
|
15622
|
-
api
|
|
15677
|
+
api,
|
|
15678
|
+
terminalFont
|
|
15623
15679
|
}));
|
|
15624
15680
|
};
|
|
15625
15681
|
const waitObserver = new MutationObserver(() => {
|
|
@@ -15697,7 +15753,7 @@ window.__ModuleLoader__.load({
|
|
|
15697
15753
|
}
|
|
15698
15754
|
};
|
|
15699
15755
|
//#endregion
|
|
15700
|
-
//#region src/client/sidebar-entry.ts
|
|
15756
|
+
//#region src/client/sidebar-entry-core.ts
|
|
15701
15757
|
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
15702
15758
|
function sidebarRoot() {
|
|
15703
15759
|
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
|
|
@@ -15711,28 +15767,30 @@ window.__ModuleLoader__.load({
|
|
|
15711
15767
|
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
15712
15768
|
}
|
|
15713
15769
|
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
15714
|
-
function createEntry(
|
|
15770
|
+
function createEntry(options) {
|
|
15715
15771
|
const entry = document.createElement("button");
|
|
15716
15772
|
entry.type = "button";
|
|
15717
|
-
entry.
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
entry.
|
|
15723
|
-
|
|
15724
|
-
|
|
15773
|
+
entry.setAttribute(options.rowAttribute, "");
|
|
15774
|
+
if (options.plugin !== void 0) {
|
|
15775
|
+
entry.setAttribute("data-dsh-plugin", options.plugin);
|
|
15776
|
+
entry.setAttribute("data-dsh-part", "sidebar-entry");
|
|
15777
|
+
}
|
|
15778
|
+
entry.className = options.css["entry"] ?? "";
|
|
15779
|
+
entry.setAttribute("aria-label", options.label());
|
|
15780
|
+
if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
|
|
15781
|
+
entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
|
|
15782
|
+
entry.addEventListener("click", options.onToggle);
|
|
15725
15783
|
return entry;
|
|
15726
15784
|
}
|
|
15727
15785
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
15728
|
-
function placeEntry(root, entry) {
|
|
15786
|
+
function placeEntry(root, entry, options) {
|
|
15729
15787
|
const button = newSessionButton(root);
|
|
15730
15788
|
if (button === void 0) return false;
|
|
15731
15789
|
if (entry.parentElement !== root) {
|
|
15732
15790
|
const row = button.closest("[class*=\"logoRow\"]");
|
|
15733
15791
|
const base = row !== null && row.parentElement === root ? row : button;
|
|
15734
|
-
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches("
|
|
15735
|
-
const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
|
|
15792
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
|
|
15793
|
+
const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
|
|
15736
15794
|
root.insertBefore(entry, anchor);
|
|
15737
15795
|
}
|
|
15738
15796
|
return true;
|
|
@@ -15740,11 +15798,12 @@ window.__ModuleLoader__.load({
|
|
|
15740
15798
|
/**
|
|
15741
15799
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
15742
15800
|
* on later React re-renders.
|
|
15743
|
-
* @param
|
|
15801
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
15744
15802
|
* @returns disposer removing the entry and its observers.
|
|
15745
15803
|
*/
|
|
15746
|
-
function mountSidebarEntry(
|
|
15747
|
-
|
|
15804
|
+
function mountSidebarEntry$1(options) {
|
|
15805
|
+
if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
|
|
15806
|
+
const entry = createEntry(options);
|
|
15748
15807
|
let root;
|
|
15749
15808
|
let placed = false;
|
|
15750
15809
|
const tryPlace = () => {
|
|
@@ -15761,7 +15820,7 @@ window.__ModuleLoader__.load({
|
|
|
15761
15820
|
}
|
|
15762
15821
|
root ??= sidebarRoot();
|
|
15763
15822
|
if (root === void 0) return;
|
|
15764
|
-
placed = placeEntry(root, entry);
|
|
15823
|
+
placed = placeEntry(root, entry, options);
|
|
15765
15824
|
if (placed) rootObserver.observe(root, {
|
|
15766
15825
|
childList: true,
|
|
15767
15826
|
subtree: true
|
|
@@ -15780,28 +15839,69 @@ window.__ModuleLoader__.load({
|
|
|
15780
15839
|
tryPlace();
|
|
15781
15840
|
return;
|
|
15782
15841
|
}
|
|
15783
|
-
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
15842
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry, options);
|
|
15784
15843
|
});
|
|
15785
|
-
const
|
|
15786
|
-
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15844
|
+
const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
|
|
15845
|
+
const syncActive = () => {
|
|
15846
|
+
if (options.active.isOpen()) entry.dataset.active = "true";
|
|
15847
|
+
else delete entry.dataset.active;
|
|
15848
|
+
};
|
|
15849
|
+
const unsubscribe = options.active.subscribe(syncActive);
|
|
15850
|
+
syncActive();
|
|
15851
|
+
return unsubscribe;
|
|
15852
|
+
})();
|
|
15791
15853
|
tryPlace();
|
|
15792
15854
|
return () => {
|
|
15793
15855
|
waitObserver.disconnect();
|
|
15794
15856
|
rootObserver.disconnect();
|
|
15795
|
-
|
|
15857
|
+
unsubscribeActive?.();
|
|
15796
15858
|
entry.remove();
|
|
15797
15859
|
};
|
|
15798
15860
|
}
|
|
15799
15861
|
//#endregion
|
|
15862
|
+
//#region src/client/sidebar-entry.ts
|
|
15863
|
+
/** Stable data attribute identifying the injected entry row. */
|
|
15864
|
+
const ENTRY_SELECTOR = "[data-dsh-ssh-entry]";
|
|
15865
|
+
/** Inline icon (matches the shell's 16px nav-icon look): a terminal prompt glyph. */
|
|
15866
|
+
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M4.5 5.5l2.5 2.5-2.5 2.5\"/><path d=\"M8.5 10.5h3\"/></svg>";
|
|
15867
|
+
/**
|
|
15868
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
15869
|
+
* on later React re-renders.
|
|
15870
|
+
* @param controller - the panel controller the entry toggles.
|
|
15871
|
+
* @returns disposer removing the entry and its observers.
|
|
15872
|
+
*/
|
|
15873
|
+
function mountSidebarEntry(controller) {
|
|
15874
|
+
return mountSidebarEntry$1({
|
|
15875
|
+
rowAttribute: "data-dsh-ssh-entry",
|
|
15876
|
+
rowSelector: ENTRY_SELECTOR,
|
|
15877
|
+
plugin: "ssh",
|
|
15878
|
+
icon: ICON,
|
|
15879
|
+
css: panel_module_css_default,
|
|
15880
|
+
label: () => tt("entry.label"),
|
|
15881
|
+
tooltip: () => tt("entry.tooltip"),
|
|
15882
|
+
onToggle: () => {
|
|
15883
|
+
controller.toggle();
|
|
15884
|
+
},
|
|
15885
|
+
position: "after",
|
|
15886
|
+
familySelectors: ["[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]"],
|
|
15887
|
+
active: {
|
|
15888
|
+
subscribe: (listener) => controller.subscribe(listener),
|
|
15889
|
+
isOpen: () => controller.getSnapshot().panelOpen
|
|
15890
|
+
}
|
|
15891
|
+
});
|
|
15892
|
+
}
|
|
15893
|
+
//#endregion
|
|
15800
15894
|
//#region src/client/index.ts
|
|
15801
15895
|
/** Locale namespace this plugin owns. */
|
|
15802
15896
|
const NS = "dsh-ssh";
|
|
15897
|
+
/** Settings namespace the terminal-font preference lives in (issue #577). */
|
|
15898
|
+
const SETTINGS_NS = "dsh-ssh";
|
|
15803
15899
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
15804
|
-
const inject = [
|
|
15900
|
+
const inject = [
|
|
15901
|
+
"slots",
|
|
15902
|
+
"locale",
|
|
15903
|
+
"settingsScope"
|
|
15904
|
+
];
|
|
15805
15905
|
/**
|
|
15806
15906
|
* Mount the SSH panel.
|
|
15807
15907
|
* @param ctx - client root context (locale service).
|
|
@@ -15813,10 +15913,18 @@ window.__ModuleLoader__.load({
|
|
|
15813
15913
|
}), "dsh-ssh: dictionaries");
|
|
15814
15914
|
const controller = new PanelController();
|
|
15815
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
|
+
};
|
|
15816
15924
|
const disposers = [];
|
|
15817
15925
|
try {
|
|
15818
15926
|
disposers.push(mountSidebarEntry(controller));
|
|
15819
|
-
disposers.push(mountPanel(controller, api));
|
|
15927
|
+
disposers.push(mountPanel(controller, api, terminalFont));
|
|
15820
15928
|
} catch (error) {
|
|
15821
15929
|
console.warn("[dsh-ssh] mount failed:", error);
|
|
15822
15930
|
}
|