@linxin666/dsh-ssh 0.1.20 → 0.2.1

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/client.js CHANGED
@@ -323,6 +323,14 @@ window.__ModuleLoader__.load({
323
323
  "hosts.add": "新增主机",
324
324
  "hosts.import": "导入 ~/.ssh/config",
325
325
  "hosts.search": "搜索别名 / 描述 / 标签…",
326
+ "hosts.groupBy.label": "分组方式",
327
+ "hosts.groupBy.none": "不分组",
328
+ "hosts.groupBy.environment": "按环境分组",
329
+ "hosts.groupBy.tags": "按标签分组",
330
+ "hosts.group.ungrouped": "未设置环境",
331
+ "hosts.group.noTags": "无标签",
332
+ "hosts.group.count": "{count} 台",
333
+ "hosts.group.testAll": "测试全部",
326
334
  "hosts.col.alias": "别名",
327
335
  "hosts.col.host": "主机",
328
336
  "hosts.col.user": "用户",
@@ -452,6 +460,14 @@ window.__ModuleLoader__.load({
452
460
  "hosts.add": "Add host",
453
461
  "hosts.import": "Import ~/.ssh/config",
454
462
  "hosts.search": "Search alias / description / tags…",
463
+ "hosts.groupBy.label": "Group by",
464
+ "hosts.groupBy.none": "No grouping",
465
+ "hosts.groupBy.environment": "Group by environment",
466
+ "hosts.groupBy.tags": "Group by tags",
467
+ "hosts.group.ungrouped": "No environment",
468
+ "hosts.group.noTags": "No tags",
469
+ "hosts.group.count": "{count} hosts",
470
+ "hosts.group.testAll": "Test all",
455
471
  "hosts.col.alias": "Alias",
456
472
  "hosts.col.host": "Host",
457
473
  "hosts.col.user": "User",
@@ -595,7 +611,7 @@ window.__ModuleLoader__.load({
595
611
  }
596
612
  //#endregion
597
613
  //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
598
- 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_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}";
614
+ 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}";
599
615
  const tagId = "@linxin666/dsh-ssh/panel.module.css";
600
616
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
601
617
  const tag = document.createElement("style");
@@ -636,6 +652,13 @@ window.__ModuleLoader__.load({
636
652
  "formError": "mL8Uca_formError",
637
653
  "formRow": "mL8Uca_formRow",
638
654
  "ghostButton": "mL8Uca_ghostButton",
655
+ "groupBySelect": "mL8Uca_groupBySelect",
656
+ "groupChevron": "mL8Uca_groupChevron",
657
+ "groupCount": "mL8Uca_groupCount",
658
+ "groupHeader": "mL8Uca_groupHeader",
659
+ "groupName": "mL8Uca_groupName",
660
+ "groupSection": "mL8Uca_groupSection",
661
+ "groupToggle": "mL8Uca_groupToggle",
639
662
  "hiddenFile": "mL8Uca_hiddenFile",
640
663
  "hint": "mL8Uca_hint",
641
664
  "iconButton": "mL8Uca_iconButton",
@@ -1238,6 +1261,31 @@ window.__ModuleLoader__.load({
1238
1261
  * add/edit/delete/test actions, ~/.ssh/config import, and a connect action
1239
1262
  * that hands the alias to the terminal tab via onConnect.
1240
1263
  */
1264
+ /**
1265
+ * Bucket hosts into collapsible groups (#379). Grouping by tags places a
1266
+ * multi-tag host in every one of its tag groups (folder view); hosts without
1267
+ * the grouping key land in the '' bucket, which always sorts last. Groups
1268
+ * sort alphabetically; host order inside a group follows the API listing.
1269
+ */
1270
+ function groupHosts(hosts, groupBy) {
1271
+ if (groupBy === "none") return [{
1272
+ key: "",
1273
+ hosts
1274
+ }];
1275
+ const buckets = /* @__PURE__ */ new Map();
1276
+ const push = (key, host) => {
1277
+ const bucket = buckets.get(key);
1278
+ if (bucket === void 0) buckets.set(key, [host]);
1279
+ else bucket.push(host);
1280
+ };
1281
+ for (const host of hosts) if (groupBy === "environment") push(host.environment ?? "", host);
1282
+ else if (host.tags.length === 0) push("", host);
1283
+ else for (const tag of host.tags) push(tag, host);
1284
+ return [...buckets.entries()].sort(([a], [b]) => a === "" ? 1 : b === "" ? -1 : a.localeCompare(b)).map(([key, group]) => ({
1285
+ key,
1286
+ hosts: group
1287
+ }));
1288
+ }
1241
1289
  /** The hosts table plus its toolbar and dialogs. */
1242
1290
  function HostsTab({ api, onConnect }) {
1243
1291
  const [hosts, setHosts] = (0, react.useState)(null);
@@ -1248,6 +1296,9 @@ window.__ModuleLoader__.load({
1248
1296
  const [importing, setImporting] = (0, react.useState)(false);
1249
1297
  const [notice, setNotice] = (0, react.useState)(null);
1250
1298
  const [dialog, setDialog] = (0, react.useState)(null);
1299
+ const [groupBy, setGroupBy] = (0, react.useState)("none");
1300
+ const [collapsed, setCollapsed] = (0, react.useState)({});
1301
+ const [testingGroup, setTestingGroup] = (0, react.useState)(null);
1251
1302
  const seqRef = (0, react.useRef)(0);
1252
1303
  const load = (0, react.useCallback)(async (query) => {
1253
1304
  const seq = ++seqRef.current;
@@ -1302,6 +1353,14 @@ window.__ModuleLoader__.load({
1302
1353
  setError(errorMessage(cause));
1303
1354
  }
1304
1355
  };
1356
+ const testGroup = async (group) => {
1357
+ setTestingGroup(group.key);
1358
+ try {
1359
+ await Promise.all(group.hosts.map((host) => runTest(host.alias)));
1360
+ } finally {
1361
+ setTestingGroup(null);
1362
+ }
1363
+ };
1305
1364
  const importConfig = async () => {
1306
1365
  setImporting(true);
1307
1366
  try {
@@ -1318,6 +1377,106 @@ window.__ModuleLoader__.load({
1318
1377
  setImporting(false);
1319
1378
  }
1320
1379
  };
1380
+ const renderHostRow = (host) => {
1381
+ const test = testResults[host.alias];
1382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1384
+ className: panel_module_css_default.mono,
1385
+ children: host.alias
1386
+ }),
1387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1388
+ className: panel_module_css_default.mono,
1389
+ children: [
1390
+ host.host,
1391
+ ":",
1392
+ host.port
1393
+ ]
1394
+ }),
1395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
1396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1397
+ className: panel_module_css_default.badge,
1398
+ "data-kind": host.auth,
1399
+ children: host.auth === "key" ? tt("form.auth.key") : tt("form.auth.password")
1400
+ }) }),
1401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1402
+ className: panel_module_css_default.cellMuted,
1403
+ children: host.environment ?? ""
1404
+ }),
1405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1406
+ className: panel_module_css_default.cellMuted,
1407
+ children: host.tags.join(", ")
1408
+ }),
1409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1410
+ className: panel_module_css_default.cellMuted,
1411
+ children: host.description ?? ""
1412
+ }),
1413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1414
+ className: panel_module_css_default.actions,
1415
+ children: [
1416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1417
+ type: "button",
1418
+ className: panel_module_css_default.linkButton,
1419
+ disabled: testingAlias === host.alias,
1420
+ onClick: () => {
1421
+ runTest(host.alias);
1422
+ },
1423
+ children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
1424
+ }),
1425
+ testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1426
+ className: panel_module_css_default.spinner,
1427
+ "aria-hidden": "true"
1428
+ }),
1429
+ test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1430
+ className: panel_module_css_default.inlineTest,
1431
+ "data-status": test.ok ? "ok" : "fail",
1432
+ children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
1433
+ }),
1434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1435
+ type: "button",
1436
+ className: panel_module_css_default.linkButton,
1437
+ onClick: () => {
1438
+ setDialog({
1439
+ mode: "edit",
1440
+ host
1441
+ });
1442
+ },
1443
+ children: tt("hosts.edit")
1444
+ }),
1445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1446
+ type: "button",
1447
+ className: panel_module_css_default.linkButton,
1448
+ "data-danger": true,
1449
+ onClick: () => {
1450
+ deleteHost(host.alias);
1451
+ },
1452
+ children: tt("hosts.delete")
1453
+ }),
1454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1455
+ type: "button",
1456
+ className: panel_module_css_default.ghostButton,
1457
+ onClick: () => {
1458
+ onConnect(host.alias);
1459
+ },
1460
+ children: tt("hosts.connected")
1461
+ })
1462
+ ]
1463
+ }) })
1464
+ ] }, host.alias);
1465
+ };
1466
+ const renderHostTable = (rows) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1467
+ className: panel_module_css_default.table,
1468
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.alias") }),
1470
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.host") }),
1471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.user") }),
1472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.auth") }),
1473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.environment") }),
1474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.tags") }),
1475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.description") }),
1476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.actions") })
1477
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map(renderHostRow) })]
1478
+ });
1479
+ const groups = hosts === null ? [] : groupHosts(hosts, groupBy);
1321
1480
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1322
1481
  className: panel_module_css_default.fillBody,
1323
1482
  children: [
@@ -1333,6 +1492,28 @@ window.__ModuleLoader__.load({
1333
1492
  setSearch(event.target.value);
1334
1493
  }
1335
1494
  }),
1495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1496
+ className: panel_module_css_default.groupBySelect,
1497
+ "aria-label": tt("hosts.groupBy.label"),
1498
+ value: groupBy,
1499
+ onChange: (event) => {
1500
+ setGroupBy(event.target.value);
1501
+ },
1502
+ children: [
1503
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1504
+ value: "none",
1505
+ children: tt("hosts.groupBy.none")
1506
+ }),
1507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1508
+ value: "environment",
1509
+ children: tt("hosts.groupBy.environment")
1510
+ }),
1511
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1512
+ value: "tags",
1513
+ children: tt("hosts.groupBy.tags")
1514
+ })
1515
+ ]
1516
+ }),
1336
1517
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.toolbarSpacer }),
1337
1518
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1338
1519
  type: "button",
@@ -1371,105 +1552,55 @@ window.__ModuleLoader__.load({
1371
1552
  className: panel_module_css_default.empty,
1372
1553
  children: tt("hosts.empty")
1373
1554
  }),
1374
- hosts !== null && hosts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1555
+ hosts !== null && hosts.length > 0 && groupBy === "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1375
1556
  className: panel_module_css_default.tableWrap,
1376
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1377
- className: panel_module_css_default.table,
1378
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1379
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.alias") }),
1380
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.host") }),
1381
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.user") }),
1382
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.auth") }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.environment") }),
1384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.tags") }),
1385
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.description") }),
1386
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.actions") })
1387
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: hosts.map((host) => {
1388
- const test = testResults[host.alias];
1389
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1390
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1391
- className: panel_module_css_default.mono,
1392
- children: host.alias
1393
- }),
1394
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1395
- className: panel_module_css_default.mono,
1396
- children: [
1397
- host.host,
1398
- ":",
1399
- host.port
1400
- ]
1401
- }),
1402
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
1403
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1404
- className: panel_module_css_default.badge,
1405
- "data-kind": host.auth,
1406
- children: host.auth === "key" ? tt("form.auth.key") : tt("form.auth.password")
1407
- }) }),
1408
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1409
- className: panel_module_css_default.cellMuted,
1410
- children: host.environment ?? ""
1411
- }),
1412
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1413
- className: panel_module_css_default.cellMuted,
1414
- children: host.tags.join(", ")
1415
- }),
1416
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1417
- className: panel_module_css_default.cellMuted,
1418
- children: host.description ?? ""
1419
- }),
1420
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1421
- className: panel_module_css_default.actions,
1557
+ children: renderHostTable(hosts)
1558
+ }),
1559
+ hosts !== null && hosts.length > 0 && groupBy !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1560
+ className: panel_module_css_default.tableWrap,
1561
+ children: groups.map((group) => {
1562
+ const isCollapsed = collapsed[group.key] === true;
1563
+ const label = group.key === "" ? groupBy === "tags" ? tt("hosts.group.noTags") : tt("hosts.group.ungrouped") : group.key;
1564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1565
+ className: panel_module_css_default.groupSection,
1566
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1567
+ className: panel_module_css_default.groupHeader,
1568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1569
+ type: "button",
1570
+ className: panel_module_css_default.groupToggle,
1571
+ "aria-expanded": !isCollapsed,
1572
+ onClick: () => {
1573
+ setCollapsed((prev) => ({
1574
+ ...prev,
1575
+ [group.key]: !isCollapsed
1576
+ }));
1577
+ },
1422
1578
  children: [
1423
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1424
- type: "button",
1425
- className: panel_module_css_default.linkButton,
1426
- disabled: testingAlias === host.alias,
1427
- onClick: () => {
1428
- runTest(host.alias);
1429
- },
1430
- children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
1431
- }),
1432
- testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1433
- className: panel_module_css_default.spinner,
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1580
+ className: panel_module_css_default.groupChevron,
1581
+ "data-collapsed": isCollapsed || void 0,
1434
1582
  "aria-hidden": "true"
1435
1583
  }),
1436
- test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1437
- className: panel_module_css_default.inlineTest,
1438
- "data-status": test.ok ? "ok" : "fail",
1439
- children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
1584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1585
+ className: panel_module_css_default.groupName,
1586
+ children: label
1440
1587
  }),
1441
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1442
- type: "button",
1443
- className: panel_module_css_default.linkButton,
1444
- onClick: () => {
1445
- setDialog({
1446
- mode: "edit",
1447
- host
1448
- });
1449
- },
1450
- children: tt("hosts.edit")
1451
- }),
1452
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1453
- type: "button",
1454
- className: panel_module_css_default.linkButton,
1455
- "data-danger": true,
1456
- onClick: () => {
1457
- deleteHost(host.alias);
1458
- },
1459
- children: tt("hosts.delete")
1460
- }),
1461
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1462
- type: "button",
1463
- className: panel_module_css_default.ghostButton,
1464
- onClick: () => {
1465
- onConnect(host.alias);
1466
- },
1467
- children: tt("hosts.connected")
1588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1589
+ className: panel_module_css_default.groupCount,
1590
+ children: tt("hosts.group.count", { count: group.hosts.length })
1468
1591
  })
1469
1592
  ]
1470
- }) })
1471
- ] }, host.alias);
1472
- }) })]
1593
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1594
+ type: "button",
1595
+ className: panel_module_css_default.linkButton,
1596
+ disabled: testingGroup === group.key,
1597
+ onClick: () => {
1598
+ testGroup(group);
1599
+ },
1600
+ children: testingGroup === group.key ? tt("hosts.testing") : tt("hosts.group.testAll")
1601
+ })]
1602
+ }), !isCollapsed && renderHostTable(group.hosts)]
1603
+ }, group.key);
1473
1604
  })
1474
1605
  }),
1475
1606
  dialog !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFormDialog, {
@@ -14733,7 +14864,8 @@ window.__ModuleLoader__.load({
14733
14864
  className: panel_module_css_default.termWrap,
14734
14865
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14735
14866
  ref: containerRef,
14736
- className: panel_module_css_default.termContainer
14867
+ className: panel_module_css_default.termContainer,
14868
+ "data-dsh-part": "terminal"
14737
14869
  }), status.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14738
14870
  className: panel_module_css_default.termPlaceholder,
14739
14871
  children: hosts.length === 0 ? tt("hosts.empty") : tt("terminal.placeholder")
@@ -15403,6 +15535,7 @@ window.__ModuleLoader__.load({
15403
15535
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15404
15536
  className: panel_module_css_default.tabBar,
15405
15537
  role: "tablist",
15538
+ "data-dsh-part": "tab-bar",
15406
15539
  children: TABS.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15407
15540
  type: "button",
15408
15541
  role: "tab",
@@ -15483,6 +15616,7 @@ window.__ModuleLoader__.load({
15483
15616
  if (column === void 0) return;
15484
15617
  container = document.createElement("div");
15485
15618
  container.dataset.dshSshView = "";
15619
+ container.dataset.dshPlugin = "ssh";
15486
15620
  container.className = panel_module_css_default.view;
15487
15621
  column.appendChild(container);
15488
15622
  root = (0, react_dom_client.createRoot)(container);
@@ -15566,7 +15700,7 @@ window.__ModuleLoader__.load({
15566
15700
  }
15567
15701
  };
15568
15702
  //#endregion
15569
- //#region src/client/sidebar-entry.ts
15703
+ //#region src/client/sidebar-entry-core.ts
15570
15704
  /** Find the sidebar shell root element, or undefined while not yet mounted. */
15571
15705
  function sidebarRoot() {
15572
15706
  const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
@@ -15580,28 +15714,30 @@ window.__ModuleLoader__.load({
15580
15714
  for (const child of root.children) if (child.tagName === "BUTTON") return child;
15581
15715
  }
15582
15716
  /** Build the entry row (a detached button; insert once the shell is up). */
15583
- function createEntry(controller) {
15717
+ function createEntry(options) {
15584
15718
  const entry = document.createElement("button");
15585
15719
  entry.type = "button";
15586
- entry.dataset.dshSshEntry = "";
15587
- entry.className = panel_module_css_default.entry;
15588
- entry.setAttribute("aria-label", tt("entry.label"));
15589
- entry.setAttribute("title", tt("entry.tooltip"));
15590
- entry.innerHTML = "<span class=\"" + panel_module_css_default.entryIcon + "\"><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></span><span class=\"" + panel_module_css_default.entryLabel + "\">" + tt("entry.label") + "</span>";
15591
- entry.addEventListener("click", () => {
15592
- controller.toggle();
15593
- });
15720
+ entry.setAttribute(options.rowAttribute, "");
15721
+ if (options.plugin !== void 0) {
15722
+ entry.setAttribute("data-dsh-plugin", options.plugin);
15723
+ entry.setAttribute("data-dsh-part", "sidebar-entry");
15724
+ }
15725
+ entry.className = options.css["entry"] ?? "";
15726
+ entry.setAttribute("aria-label", options.label());
15727
+ if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
15728
+ entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
15729
+ entry.addEventListener("click", options.onToggle);
15594
15730
  return entry;
15595
15731
  }
15596
15732
  /** Re-insert the entry after the New Session row (before the browser region). */
15597
- function placeEntry(root, entry) {
15733
+ function placeEntry(root, entry, options) {
15598
15734
  const button = newSessionButton(root);
15599
15735
  if (button === void 0) return false;
15600
15736
  if (entry.parentElement !== root) {
15601
15737
  const row = button.closest("[class*=\"logoRow\"]");
15602
15738
  const base = row !== null && row.parentElement === root ? row : button;
15603
- const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches("[data-dsh-taskboard-entry], [data-dsh-ssh-entry]"));
15604
- const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
15739
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
15740
+ const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
15605
15741
  root.insertBefore(entry, anchor);
15606
15742
  }
15607
15743
  return true;
@@ -15609,11 +15745,12 @@ window.__ModuleLoader__.load({
15609
15745
  /**
15610
15746
  * Mount the sidebar entry, waiting for the shell to render and self-healing
15611
15747
  * on later React re-renders.
15612
- * @param controller - the panel controller the entry toggles.
15748
+ * @param options - the row's attribute/icon/copy/action/ordering configuration.
15613
15749
  * @returns disposer removing the entry and its observers.
15614
15750
  */
15615
- function mountSidebarEntry(controller) {
15616
- const entry = createEntry(controller);
15751
+ function mountSidebarEntry$1(options) {
15752
+ if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
15753
+ const entry = createEntry(options);
15617
15754
  let root;
15618
15755
  let placed = false;
15619
15756
  const tryPlace = () => {
@@ -15630,7 +15767,7 @@ window.__ModuleLoader__.load({
15630
15767
  }
15631
15768
  root ??= sidebarRoot();
15632
15769
  if (root === void 0) return;
15633
- placed = placeEntry(root, entry);
15770
+ placed = placeEntry(root, entry, options);
15634
15771
  if (placed) rootObserver.observe(root, {
15635
15772
  childList: true,
15636
15773
  subtree: true
@@ -15649,23 +15786,58 @@ window.__ModuleLoader__.load({
15649
15786
  tryPlace();
15650
15787
  return;
15651
15788
  }
15652
- if (!root.contains(entry)) placed = placeEntry(root, entry);
15789
+ if (!root.contains(entry)) placed = placeEntry(root, entry, options);
15653
15790
  });
15654
- const syncActive = () => {
15655
- if (controller.getSnapshot().panelOpen) entry.dataset.active = "true";
15656
- else delete entry.dataset.active;
15657
- };
15658
- const unsubscribe = controller.subscribe(syncActive);
15659
- syncActive();
15791
+ const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
15792
+ const syncActive = () => {
15793
+ if (options.active.isOpen()) entry.dataset.active = "true";
15794
+ else delete entry.dataset.active;
15795
+ };
15796
+ const unsubscribe = options.active.subscribe(syncActive);
15797
+ syncActive();
15798
+ return unsubscribe;
15799
+ })();
15660
15800
  tryPlace();
15661
15801
  return () => {
15662
15802
  waitObserver.disconnect();
15663
15803
  rootObserver.disconnect();
15664
- unsubscribe();
15804
+ unsubscribeActive?.();
15665
15805
  entry.remove();
15666
15806
  };
15667
15807
  }
15668
15808
  //#endregion
15809
+ //#region src/client/sidebar-entry.ts
15810
+ /** Stable data attribute identifying the injected entry row. */
15811
+ const ENTRY_SELECTOR = "[data-dsh-ssh-entry]";
15812
+ /** Inline icon (matches the shell's 16px nav-icon look): a terminal prompt glyph. */
15813
+ 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>";
15814
+ /**
15815
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
15816
+ * on later React re-renders.
15817
+ * @param controller - the panel controller the entry toggles.
15818
+ * @returns disposer removing the entry and its observers.
15819
+ */
15820
+ function mountSidebarEntry(controller) {
15821
+ return mountSidebarEntry$1({
15822
+ rowAttribute: "data-dsh-ssh-entry",
15823
+ rowSelector: ENTRY_SELECTOR,
15824
+ plugin: "ssh",
15825
+ icon: ICON,
15826
+ css: panel_module_css_default,
15827
+ label: () => tt("entry.label"),
15828
+ tooltip: () => tt("entry.tooltip"),
15829
+ onToggle: () => {
15830
+ controller.toggle();
15831
+ },
15832
+ position: "after",
15833
+ familySelectors: ["[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]"],
15834
+ active: {
15835
+ subscribe: (listener) => controller.subscribe(listener),
15836
+ isOpen: () => controller.getSnapshot().panelOpen
15837
+ }
15838
+ });
15839
+ }
15840
+ //#endregion
15669
15841
  //#region src/client/index.ts
15670
15842
  /** Locale namespace this plugin owns. */
15671
15843
  const NS = "dsh-ssh";