@linxin666/dsh-ssh 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/client.js +303 -459
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +707 -466
  4. package/lib/types/client/api.d.ts +27 -4
  5. package/lib/types/client/native-panel.d.ts +70 -0
  6. package/lib/types/client/panel/SshPanel.d.ts +1 -3
  7. package/lib/types/client/panel/TerminalTab.d.ts +6 -1
  8. package/lib/types/client/panel/controller.d.ts +71 -4
  9. package/lib/types/engine/terminal-sessions.d.ts +67 -0
  10. package/lib/types/mount-once.d.ts +38 -7
  11. package/lib/types/protocol.d.ts +12 -1
  12. package/lib/types/routes.d.ts +2 -0
  13. package/package.json +19 -16
  14. package/src/client/api.ts +52 -22
  15. package/src/client/index.ts +30 -8
  16. package/src/client/native-panel.tsx +141 -0
  17. package/src/client/panel/SshPanel.tsx +25 -28
  18. package/src/client/panel/TerminalTab.tsx +54 -14
  19. package/src/client/panel/controller.ts +126 -7
  20. package/src/client/panel/panel.module.css +8 -128
  21. package/src/engine/terminal-sessions.ts +280 -0
  22. package/src/index.ts +4 -1
  23. package/src/mount-once.ts +107 -13
  24. package/src/protocol.ts +6 -1
  25. package/src/routes.ts +26 -107
  26. package/lib/types/client/body-mutations.d.ts +0 -45
  27. package/lib/types/client/mount.d.ts +0 -27
  28. package/lib/types/client/panel-mount-core.d.ts +0 -56
  29. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  30. package/lib/types/client/sidebar-entry.d.ts +0 -26
  31. package/src/client/body-mutations.ts +0 -133
  32. package/src/client/mount.tsx +0 -47
  33. package/src/client/panel-mount-core.ts +0 -155
  34. package/src/client/sidebar-entry-core.ts +0 -237
  35. package/src/client/sidebar-entry.ts +0 -51
package/lib/client.js CHANGED
@@ -9,7 +9,6 @@ window.__ModuleLoader__.load({
9
9
  //#endregion
10
10
  let react = require("react");
11
11
  let react_jsx_runtime = require("react/jsx-runtime");
12
- let react_dom_client = require("react-dom/client");
13
12
  //#region src/protocol.ts
14
13
  const SSH_API = {
15
14
  hosts: "/api/dsh-ssh/hosts",
@@ -644,39 +643,77 @@ window.__ModuleLoader__.load({
644
643
  bytes: received
645
644
  };
646
645
  }
647
- /** Open a WebSocket terminal session. */
646
+ /**
647
+ * Open a new host terminal session for the alias.
648
+ * @param alias - the configured host alias.
649
+ * @param cols - initial PTY width.
650
+ * @param rows - initial PTY height.
651
+ * @returns the live connection; its ready callback carries the session id
652
+ * {@link attachTerminal} uses after a view detach.
653
+ */
648
654
  openTerminal(alias, cols, rows) {
649
- const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + query({
655
+ return this.terminalSocket(query({
650
656
  alias,
651
657
  cols,
652
658
  rows
653
- });
659
+ }));
660
+ }
661
+ /**
662
+ * Reattach to a host-side session that outlived its view (a panel switch, a
663
+ * page navigation). The host replays its scrollback before the ready frame.
664
+ * @param sessionId - the id the previous connection reported on ready.
665
+ * @param cols - the view's current width.
666
+ * @param rows - the view's current height.
667
+ */
668
+ attachTerminal(sessionId, cols, rows) {
669
+ return this.terminalSocket(query({
670
+ session: sessionId,
671
+ cols,
672
+ rows
673
+ }));
674
+ }
675
+ /** One terminal socket over either an alias (open) or a session id (attach). */
676
+ terminalSocket(search) {
677
+ const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + search;
654
678
  const socket = new WebSocket(url);
679
+ let leaving = false;
680
+ const sendFrame = (frame) => {
681
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(frame));
682
+ };
655
683
  const connection = {
656
684
  onReady: void 0,
657
685
  onOutput: void 0,
658
686
  onExit: void 0,
659
687
  onAuthPrompt: void 0,
660
688
  send: (data) => {
661
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
689
+ sendFrame({
662
690
  type: "input",
663
691
  data
664
- }));
692
+ });
665
693
  },
666
694
  resize: (cols, rows) => {
667
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
695
+ sendFrame({
668
696
  type: "resize",
669
697
  cols,
670
698
  rows
671
- }));
699
+ });
672
700
  },
673
701
  sendAuthResponse: (responses) => {
674
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
702
+ sendFrame({
675
703
  type: "auth_response",
676
704
  responses
677
- }));
705
+ });
706
+ },
707
+ detach: () => {
708
+ leaving = true;
709
+ sendFrame({ type: "detach" });
710
+ try {
711
+ socket.close();
712
+ } catch {}
678
713
  },
679
714
  close: () => {
715
+ leaving = true;
716
+ sendFrame({ type: "close" });
680
717
  try {
681
718
  socket.close();
682
719
  } catch {}
@@ -689,23 +726,127 @@ window.__ModuleLoader__.load({
689
726
  } catch {
690
727
  return;
691
728
  }
692
- if (frame.type === "ready") connection.onReady?.();
729
+ if (frame.type === "ready") connection.onReady?.(frame.sessionId, frame.alias);
693
730
  else if (frame.type === "output") connection.onOutput?.(frame.data);
694
731
  else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
695
732
  else if (frame.type === "auth_prompt") connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts);
696
733
  };
697
734
  socket.onclose = () => {
698
- connection.onExit?.(null, "connection closed");
735
+ if (!leaving) connection.onExit?.(null, "connection closed");
699
736
  };
700
737
  socket.onerror = () => {
701
- connection.onExit?.(null, "connection error");
738
+ if (!leaving) connection.onExit?.(null, "connection error");
702
739
  };
703
740
  return connection;
704
741
  }
705
742
  };
743
+ /** The panel state owner the sidebar row and the view both read. */
744
+ var PanelController = class {
745
+ deps;
746
+ panelOpen = false;
747
+ activeTab = "hosts";
748
+ connectRequest = null;
749
+ terminalSessionId;
750
+ listeners = /* @__PURE__ */ new Set();
751
+ /** Cached so getSnapshot is referentially stable between changes. */
752
+ snapshot = {
753
+ panelOpen: false,
754
+ activeTab: "hosts",
755
+ connectRequest: null,
756
+ terminalSessionId: void 0
757
+ };
758
+ constructor(deps = {}) {
759
+ this.deps = deps;
760
+ }
761
+ getSnapshot() {
762
+ return this.snapshot;
763
+ }
764
+ subscribe(fn) {
765
+ this.listeners.add(fn);
766
+ return () => {
767
+ this.listeners.delete(fn);
768
+ };
769
+ }
770
+ /**
771
+ * Show the panel. The layout owns which panel the column renders, so the
772
+ * state flip and the selection travel together; the selection is requested
773
+ * after the snapshot flips, so a subscriber never observes "open" while the
774
+ * shell still shows the conversation.
775
+ */
776
+ open() {
777
+ if (this.panelOpen) return;
778
+ this.panelOpen = true;
779
+ this.commit();
780
+ this.selectPanel("ssh");
781
+ }
782
+ /** Return the column to the conversation, asking the layout explicitly. */
783
+ close() {
784
+ if (!this.panelOpen) return;
785
+ this.panelOpen = false;
786
+ this.commit();
787
+ this.selectPanel(null);
788
+ }
789
+ toggle() {
790
+ if (this.panelOpen) this.close();
791
+ else this.open();
792
+ }
793
+ /**
794
+ * Reflect a selection made outside this controller (the user clicked another
795
+ * sidebar row, or the layout dropped the panel id).
796
+ * @param panelId - the layout's current panel id, or null for the conversation.
797
+ */
798
+ syncPanelSelection(panelId) {
799
+ const open = panelId === "ssh";
800
+ if (open === this.panelOpen) return;
801
+ this.panelOpen = open;
802
+ this.commit();
803
+ }
804
+ /** Switch tabs, keeping every other piece of view state. */
805
+ setActiveTab(tab) {
806
+ if (this.activeTab === tab) return;
807
+ this.activeTab = tab;
808
+ this.commit();
809
+ }
810
+ /** Hand the terminal tab a host to connect to (hosts-tab "connect" action). */
811
+ requestConnect(alias) {
812
+ this.connectRequest = {
813
+ alias,
814
+ nonce: Date.now()
815
+ };
816
+ this.activeTab = "terminal";
817
+ this.commit();
818
+ }
819
+ /** Record the host-side session so a later page mount can reattach. */
820
+ setTerminalSession(sessionId) {
821
+ if (this.terminalSessionId === sessionId) return;
822
+ this.terminalSessionId = sessionId;
823
+ this.commit();
824
+ }
825
+ /** Forget the session (it exited, or the user disconnected). */
826
+ clearTerminalSession() {
827
+ if (this.terminalSessionId === void 0) return;
828
+ this.terminalSessionId = void 0;
829
+ this.commit();
830
+ }
831
+ commit() {
832
+ this.snapshot = {
833
+ panelOpen: this.panelOpen,
834
+ activeTab: this.activeTab,
835
+ connectRequest: this.connectRequest,
836
+ terminalSessionId: this.terminalSessionId
837
+ };
838
+ for (const fn of [...this.listeners]) fn();
839
+ }
840
+ /** Ask the layout to select a panel; a shell without the face is a no-op. */
841
+ selectPanel(panelId) {
842
+ try {
843
+ this.deps.panel?.select(panelId);
844
+ } catch {}
845
+ }
846
+ };
706
847
  //#endregion
707
848
  //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
708
- 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{box-sizing:border-box;min-height:36px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;text-align:left;white-space:nowrap;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;margin:0 2px;padding:7px 8px;font-size:14px;line-height:22px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;width:16px;height:16px;display:inline-flex}.mL8Uca_entryIcon svg{width:16px;height:16px;display:block}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry,[data-sidebar-collapsed] .mL8Uca_entry{border-radius:12px;justify-content:center;width:36px;min-height:36px;margin:0 auto 12px;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryIcon,[data-sidebar-collapsed] .mL8Uca_entryIcon,[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryIcon svg,[data-sidebar-collapsed] .mL8Uca_entryIcon svg{width:18px;height:18px}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel,[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-kind=agent]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-kind=proxy]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);margin-left:6px}.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_importSkips{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:4px;margin:6px 0 0;padding:8px 12px;list-style:none;display:flex}.mL8Uca_importSkipRow{justify-content:space-between;align-items:baseline;gap:12px;font-size:12px;display:flex}.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{position:absolute;inset:8px 10px}.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}";
849
+ const css = ".mL8Uca_view{height:100%;min-height:0;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-kind=agent]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-kind=proxy]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);margin-left:6px}.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_importSkips{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:4px;margin:6px 0 0;padding:8px 12px;list-style:none;display:flex}.mL8Uca_importSkipRow{justify-content:space-between;align-items:baseline;gap:12px;font-size:12px;display:flex}.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{position:absolute;inset:8px 10px}.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}";
709
850
  const tagId = "@linxin666/dsh-ssh/packages/dsh-ssh/src/client/panel/panel.module.css";
710
851
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
711
852
  const tag = document.createElement("style");
@@ -736,9 +877,6 @@ window.__ModuleLoader__.load({
736
877
  "dirType": "mL8Uca_dirType",
737
878
  "dshSshSpin": "mL8Uca_dshSshSpin",
738
879
  "empty": "mL8Uca_empty",
739
- "entry": "mL8Uca_entry",
740
- "entryIcon": "mL8Uca_entryIcon",
741
- "entryLabel": "mL8Uca_entryLabel",
742
880
  "field": "mL8Uca_field",
743
881
  "fieldLabel": "mL8Uca_fieldLabel",
744
882
  "fillBody": "mL8Uca_fillBody",
@@ -14893,7 +15031,7 @@ window.__ModuleLoader__.load({
14893
15031
  subscribe: () => () => void 0
14894
15032
  };
14895
15033
  /** The xterm terminal view. */
14896
- function TerminalTab({ api, presetAlias, requestId, terminalFont }) {
15034
+ function TerminalTab({ api, controller, sessionId, presetAlias, requestId, terminalFont }) {
14897
15035
  const [hosts, setHosts] = (0, react.useState)([]);
14898
15036
  const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
14899
15037
  const [status, setStatus] = (0, react.useState)({ kind: "idle" });
@@ -14938,7 +15076,12 @@ window.__ModuleLoader__.load({
14938
15076
  (0, react.useEffect)(() => {
14939
15077
  if (presetAlias !== void 0) setAlias(presetAlias);
14940
15078
  }, [presetAlias, requestId]);
14941
- const teardown = () => {
15079
+ /**
15080
+ * Tear the view down.
15081
+ * @param leave - `detach` leaves the host session alive for a reattach
15082
+ * (panel switch, unmount); `close` ends it (the disconnect control).
15083
+ */
15084
+ const teardown = (leave) => {
14942
15085
  setAuthPrompt(void 0);
14943
15086
  setAuthInputs([]);
14944
15087
  const connection = connRef.current;
@@ -14948,7 +15091,8 @@ window.__ModuleLoader__.load({
14948
15091
  connection.onOutput = void 0;
14949
15092
  connection.onExit = void 0;
14950
15093
  connection.onAuthPrompt = void 0;
14951
- connection.close();
15094
+ if (leave === "detach") connection.detach();
15095
+ else connection.close();
14952
15096
  }
14953
15097
  dataSubRef.current?.dispose();
14954
15098
  dataSubRef.current = null;
@@ -14957,7 +15101,7 @@ window.__ModuleLoader__.load({
14957
15101
  fitRef.current = null;
14958
15102
  };
14959
15103
  (0, react.useEffect)(() => () => {
14960
- teardown();
15104
+ teardown("detach");
14961
15105
  }, []);
14962
15106
  (0, react.useEffect)(() => {
14963
15107
  let lastCols = -1;
@@ -14988,12 +15132,15 @@ window.__ModuleLoader__.load({
14988
15132
  window.removeEventListener("resize", sync);
14989
15133
  };
14990
15134
  }, []);
14991
- const connect = () => {
14992
- const target = alias;
15135
+ /**
15136
+ * Build the xterm view and bind one connection to it.
15137
+ * @param open - opens the transport once the terminal has a size.
15138
+ */
15139
+ const startSession = (open) => {
14993
15140
  const container = containerRef.current;
14994
- if (target === "" || container === null) return;
15141
+ if (container === null) return;
14995
15142
  if (status.kind === "connecting" || status.kind === "connected") return;
14996
- teardown();
15143
+ teardown("detach");
14997
15144
  setStatus({ kind: "connecting" });
14998
15145
  const term = new import_xterm.Terminal({
14999
15146
  convertEol: false,
@@ -15010,11 +15157,12 @@ window.__ModuleLoader__.load({
15010
15157
  term.loadAddon(fit);
15011
15158
  term.open(container);
15012
15159
  fit.fit();
15013
- const connection = api.openTerminal(target, term.cols, term.rows);
15160
+ const connection = open(term.cols, term.rows);
15014
15161
  termRef.current = term;
15015
15162
  fitRef.current = fit;
15016
15163
  connRef.current = connection;
15017
15164
  let settled = false;
15165
+ let connectedAlias = "";
15018
15166
  dataSubRef.current = term.onData((data) => {
15019
15167
  connection.send(data);
15020
15168
  });
@@ -15026,12 +15174,14 @@ window.__ModuleLoader__.load({
15026
15174
  });
15027
15175
  setAuthInputs(prompts.map(() => ""));
15028
15176
  };
15029
- connection.onReady = () => {
15177
+ connection.onReady = (id, readyAlias) => {
15178
+ connectedAlias = readyAlias;
15179
+ controller.setTerminalSession(id);
15030
15180
  setAuthPrompt(void 0);
15031
15181
  setAuthInputs([]);
15032
15182
  setStatus({
15033
15183
  kind: "connected",
15034
- alias: target
15184
+ alias: readyAlias
15035
15185
  });
15036
15186
  };
15037
15187
  connection.onOutput = (data) => {
@@ -15046,15 +15196,29 @@ window.__ModuleLoader__.load({
15046
15196
  dataSubRef.current = null;
15047
15197
  term.options.disableStdin = true;
15048
15198
  connRef.current = null;
15199
+ controller.clearTerminalSession();
15049
15200
  setStatus({
15050
15201
  kind: "exited",
15051
- alias: target,
15202
+ alias: connectedAlias !== "" ? connectedAlias : alias,
15052
15203
  detail: error
15053
15204
  });
15054
15205
  };
15055
15206
  };
15207
+ const connect = () => {
15208
+ const target = alias;
15209
+ if (target === "") return;
15210
+ startSession((cols, rows) => api.openTerminal(target, cols, rows));
15211
+ };
15212
+ const reattachedRef = (0, react.useRef)(void 0);
15213
+ (0, react.useEffect)(() => {
15214
+ if (sessionId === void 0 || connRef.current !== null) return;
15215
+ if (reattachedRef.current === sessionId) return;
15216
+ reattachedRef.current = sessionId;
15217
+ startSession((cols, rows) => api.attachTerminal(sessionId, cols, rows));
15218
+ }, [sessionId]);
15056
15219
  const disconnect = () => {
15057
- teardown();
15220
+ teardown("close");
15221
+ controller.clearTerminalSession();
15058
15222
  setStatus({ kind: "idle" });
15059
15223
  };
15060
15224
  const submitAuth = (e) => {
@@ -15830,10 +15994,14 @@ window.__ModuleLoader__.load({
15830
15994
  //#region src/client/panel/SshPanel.tsx
15831
15995
  /**
15832
15996
  * The SSH operations panel shell: a header with a close control, a five-tab
15833
- * bar, and the active tab's content. Tab state lives here (browser session
15834
- * state); inactive tabs unmount, so each tab fetches its own data on
15835
- * activation. The hosts tab's connect action switches here to the terminal
15836
- * tab with the chosen alias preselected.
15997
+ * bar, and the active tab's content.
15998
+ *
15999
+ * The active tab, the pending connect request and the live terminal session id
16000
+ * live in the controller, not in component state: the layout mounts this page
16001
+ * only while the panel is selected, so local state would reset the tab on
16002
+ * every panel switch and lose the id the terminal tab needs to reattach its
16003
+ * host-side session. Inactive tabs unmount, so each tab fetches its own data
16004
+ * on activation.
15837
16005
  */
15838
16006
  /** The tab bar definition (labels resolved at render time). */
15839
16007
  const TABS = [
@@ -15860,16 +16028,7 @@ window.__ModuleLoader__.load({
15860
16028
  ];
15861
16029
  /** The tabbed SSH panel. */
15862
16030
  function SshPanel({ controller, api, terminalFont }) {
15863
- const panelOpen = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => controller.subscribe(listener), [controller]), (0, react.useCallback)(() => controller.getSnapshot().panelOpen, [controller]));
15864
- const [activeTab, setActiveTab] = (0, react.useState)("hosts");
15865
- const [connectRequest, setConnectRequest] = (0, react.useState)(null);
15866
- const handleConnect = (alias) => {
15867
- setActiveTab("terminal");
15868
- setConnectRequest((prev) => ({
15869
- alias,
15870
- nonce: (prev?.nonce ?? 0) + 1
15871
- }));
15872
- };
16031
+ const { panelOpen, activeTab, connectRequest, terminalSessionId } = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => controller.subscribe(listener), [controller]), (0, react.useCallback)(() => controller.getSnapshot(), [controller]));
15873
16032
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15874
16033
  className: panel_module_css_default.panel,
15875
16034
  "data-dsh-plugin": "ssh",
@@ -15905,7 +16064,7 @@ window.__ModuleLoader__.load({
15905
16064
  "data-dsh-part": "tab",
15906
16065
  className: panel_module_css_default.tab,
15907
16066
  onClick: () => {
15908
- setActiveTab(tab.id);
16067
+ controller.setActiveTab(tab.id);
15909
16068
  },
15910
16069
  children: tab.label()
15911
16070
  }, tab.id))
@@ -15915,12 +16074,16 @@ window.__ModuleLoader__.load({
15915
16074
  children: [
15916
16075
  activeTab === "hosts" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostsTab, {
15917
16076
  api,
15918
- onConnect: handleConnect
16077
+ onConnect: (alias) => {
16078
+ controller.requestConnect(alias);
16079
+ }
15919
16080
  }),
15920
16081
  activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
15921
16082
  api,
16083
+ controller,
15922
16084
  presetAlias: connectRequest?.alias,
15923
16085
  requestId: connectRequest?.nonce,
16086
+ sessionId: terminalSessionId,
15924
16087
  terminalFont
15925
16088
  }),
15926
16089
  activeTab === "transfer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferTab, { api }),
@@ -15935,261 +16098,101 @@ window.__ModuleLoader__.load({
15935
16098
  });
15936
16099
  }
15937
16100
  //#endregion
15938
- //#region src/client/body-mutations.ts
15939
- /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
15940
- const HUB_KEY = Symbol.for("dsh-web.body-mutation-hub");
15941
- const INVALIDATION_ONLY = Symbol.for("dsh-web.body-mutation-invalidation");
15942
- function needsRecords(subscribers) {
15943
- for (const listener of subscribers) if (!listener[INVALIDATION_ONLY]) return true;
15944
- return false;
15945
- }
16101
+ //#region src/client/native-panel.tsx
16102
+ /** Row order among the shell's global panel rows (Plugins 0, Schedule 10, board 20, skill center 30). */
16103
+ const PANEL_ORDER = 40;
15946
16104
  /**
15947
- * Subscribe to a coalesced DOM re-check without retaining mutation records.
15948
- * The marked wrapper also works with an older hub, which delivers records
15949
- * that it simply ignores until a page reload picks up the updated hub.
15950
- */
15951
- function subscribeBodyInvalidations(subscriber) {
15952
- const listener = () => {
15953
- subscriber();
15954
- };
15955
- listener[INVALIDATION_ONLY] = true;
15956
- return subscribeBodyMutations(listener);
15957
- }
15958
- /**
15959
- * Subscribe to body-level childList mutations.
15960
- * @param subscriber - called at most once per animation frame with the records
15961
- * collected since the previous flush; must be safe to run repeatedly.
15962
- * @returns the disposer removing this subscriber (and the observer when it was
15963
- * the last one).
15964
- */
15965
- function subscribeBodyMutations(subscriber) {
15966
- if (typeof globalThis === "undefined" || typeof document === "undefined") return () => {};
15967
- if (typeof MutationObserver !== "function") return () => {};
15968
- const registry = globalThis;
15969
- let hub = registry[HUB_KEY];
15970
- if (hub === void 0) {
15971
- const subscribers = /* @__PURE__ */ new Set();
15972
- const created = {
15973
- observer: void 0,
15974
- subscribers,
15975
- pending: [],
15976
- scheduled: false
15977
- };
15978
- const flush = () => {
15979
- created.frame = void 0;
15980
- created.scheduled = false;
15981
- const batch = created.pending;
15982
- created.pending = [];
15983
- for (const listener of [...subscribers]) {
15984
- if (!subscribers.has(listener)) continue;
15985
- try {
15986
- listener(batch);
15987
- } catch {}
15988
- }
15989
- };
15990
- const schedule = () => {
15991
- if (created.scheduled) return;
15992
- created.scheduled = true;
15993
- if (typeof requestAnimationFrame === "function") created.frame = requestAnimationFrame(flush);
15994
- else flush();
15995
- };
15996
- created.observer = new MutationObserver((records) => {
15997
- if (needsRecords(subscribers)) for (const record of records) created.pending.push(record);
15998
- schedule();
15999
- });
16000
- created.observer.observe(document.body ?? document.documentElement, {
16001
- childList: true,
16002
- subtree: true
16003
- });
16004
- registry[HUB_KEY] = created;
16005
- hub = created;
16006
- }
16007
- const active = hub;
16008
- active.subscribers.add(subscriber);
16009
- let subscribed = true;
16010
- return () => {
16011
- if (!subscribed) return;
16012
- subscribed = false;
16013
- active.subscribers.delete(subscriber);
16014
- if (!needsRecords(active.subscribers)) active.pending = [];
16015
- if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
16016
- active.observer.disconnect();
16017
- if (active.frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(active.frame);
16018
- active.frame = void 0;
16019
- active.pending = [];
16020
- active.scheduled = false;
16021
- delete registry[HUB_KEY];
16022
- }
16023
- };
16024
- }
16025
- //#endregion
16026
- //#region src/client/panel-mount-core.ts
16027
- /**
16028
- * Center-column panel takeover lifecycle.
16105
+ * The sidebar row glyph the shell asks for at its own size and active state.
16106
+ * The shell owns the button, label, tooltip and rail geometry; this component
16107
+ * draws only the glyph, like every other panel row.
16029
16108
  *
16030
- * The `conversation` slot is single-occupant (ui-conversation) and external
16031
- * plugins cannot declare slots, so a family panel takes over the center
16032
- * column at the DOM level: a container is appended inside the center column
16033
- * (`[class*="centerCol"]`, the 0.1.0-rc.6+ AppFrame layout; previously
16034
- * `[data-pane="conversation"]` on older shells — the mount selector keeps
16035
- * both, ssh #243 / task-board #107) as an extra trailing child React never
16036
- * manages, and a stylesheet rule hides the conversation content while the
16037
- * panel is active. Toggling is a data attribute on <html> — no React
16038
- * involvement, so the conversation subtree underneath stays mounted and
16039
- * stateful.
16040
- *
16041
- * Consuming plugins keep a thin wrapper that supplies the panel tree,
16042
- * container attribute names, and stylesheet class; those names are pinned by
16043
- * each package's CSS, skins, and the semantic-attributes contract. The
16044
- * sidebar row toggling the panel shares its core the same way
16045
- * (shared/client/sidebar-entry-core.ts, synced copy).
16109
+ * The glyph carries `data-dsh-panel-entry` because it is the only DOM this
16110
+ * panel's own code owns inside that shell-owned row: the L2 contract (skins)
16111
+ * resolves which row belongs to which plugin through it, since the shell
16112
+ * stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
16113
+ * @param props - the shell's icon share: square edge and selection state.
16114
+ * @returns the decorative terminal glyph.
16046
16115
  */
16047
- const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
16048
- /** Cross-plugin activation event; detail is the activating panel name. */
16049
- const ACTIVATE_EVENT = "dsh-panel-activate";
16050
- const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
16051
- /** Find the center column, or undefined while the frame is not mounted. */
16052
- function conversationColumn() {
16053
- return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
16116
+ function SshPanelIcon({ size }) {
16117
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
16118
+ "data-dsh-panel-entry": "ssh",
16119
+ viewBox: "0 0 16 16",
16120
+ width: size,
16121
+ height: size,
16122
+ fill: "none",
16123
+ stroke: "currentColor",
16124
+ strokeWidth: "1.5",
16125
+ strokeLinecap: "round",
16126
+ strokeLinejoin: "round",
16127
+ "aria-hidden": "true",
16128
+ children: [
16129
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
16130
+ x: "1.75",
16131
+ y: "2.25",
16132
+ width: "12.5",
16133
+ height: "11.5",
16134
+ rx: "1.75"
16135
+ }),
16136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.25 5.25l2.75 2.75-2.75 2.75" }),
16137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.5 10.75h3.25" })
16138
+ ]
16139
+ });
16054
16140
  }
16055
16141
  /**
16056
- * Mount a family panel into the center column and bind its visibility to the
16057
- * owning controller's open state.
16058
- * @returns disposer unmounting the tree and restoring the column.
16142
+ * The main-slot page. The layout mounts it only while this panel is selected;
16143
+ * the wrapper carries the pinned `data-dsh-ssh-view` semantic anchor (L2
16144
+ * contract, skins) the takeover container used to own.
16145
+ * @param props - the framework main-slot share plus this entry's injected face.
16146
+ * @returns the SSH panel page.
16059
16147
  */
16060
- function mountCenterPanel(options) {
16061
- let root;
16062
- let container;
16063
- let unsubscribeLocale;
16064
- try {
16065
- unsubscribeLocale = options.locale?.subscribe(() => {
16066
- if (root !== void 0) options.render(root);
16067
- });
16068
- } catch {}
16069
- const ensure = () => {
16070
- if (container !== void 0 && !container.isConnected) {
16071
- root?.unmount();
16072
- root = void 0;
16073
- container.remove();
16074
- container = void 0;
16075
- }
16076
- if (container === void 0) {
16077
- const column = conversationColumn();
16078
- if (column === void 0) return;
16079
- container = document.createElement("div");
16080
- container.dataset[options.viewDatasetKey] = "";
16081
- container.dataset.dshPlugin = options.pluginName;
16082
- container.className = options.viewClassName;
16083
- column.appendChild(container);
16084
- }
16085
- if (root !== void 0 || !options.isOpen()) return;
16086
- root = (0, react_dom_client.createRoot)(container);
16087
- options.render(root);
16088
- };
16089
- const unsubscribeBody = subscribeBodyInvalidations(() => {
16090
- ensure();
16148
+ function SshPanelPage({ controller, api, terminalFont }) {
16149
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16150
+ className: panel_module_css_default.view,
16151
+ "data-dsh-ssh-view": "",
16152
+ "data-dsh-plugin": "ssh",
16153
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
16154
+ controller,
16155
+ api,
16156
+ terminalFont
16157
+ })
16091
16158
  });
16092
- const applyActive = () => {
16093
- if (options.isOpen()) {
16094
- ensure();
16095
- document.documentElement.removeAttribute(options.siblingActiveAttribute);
16096
- document.documentElement.setAttribute(options.activeAttribute, "");
16097
- document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: options.panelName }));
16098
- } else document.documentElement.removeAttribute(options.activeAttribute);
16099
- };
16100
- const onOtherActivate = (event) => {
16101
- if (event.detail === options.siblingPanelName && options.isOpen()) options.close();
16102
- };
16103
- const onClickSidebarRow = (event) => {
16104
- if (!options.isOpen()) return;
16105
- const target = event.target;
16106
- if (target === null) return;
16107
- if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) options.close();
16108
- };
16109
- document.addEventListener("click", onClickSidebarRow, true);
16110
- document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
16111
- const unsubscribe = options.subscribe(applyActive);
16112
- applyActive();
16113
- ensure();
16114
- return () => {
16115
- document.removeEventListener("click", onClickSidebarRow, true);
16116
- document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
16117
- unsubscribeBody();
16118
- unsubscribe();
16119
- unsubscribeLocale?.();
16120
- document.documentElement.removeAttribute(options.activeAttribute);
16121
- root?.unmount();
16122
- root = void 0;
16123
- container?.remove();
16124
- container = void 0;
16125
- };
16126
16159
  }
16127
- //#endregion
16128
- //#region src/client/mount.tsx
16129
16160
  /**
16130
- * Mount the panel React tree into the center column and bind its visibility
16131
- * to the controller's panelOpen state.
16132
- * @param controller - the panel controller driving the view.
16133
- * @param api - the SSH API client the tabs operate through.
16161
+ * Register the SSH panel's sidebar row and center-column page.
16162
+ *
16163
+ * Both seats are declared by shell plugins this package does not depend on at
16164
+ * runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
16165
+ * runs only after the owning entry declares the seat, and a shell that never
16166
+ * declares it leaves the panel simply absent instead of failing boot.
16167
+ * @param ctx - client root context (services: slots).
16168
+ * @param controller - the controller the page and the row drive.
16169
+ * @param api - the SSH API client the page operates through.
16134
16170
  * @param terminalFont - live terminal-font setting source (issue #577).
16135
- * @param locale - locale-change source; when given, re-renders an open panel
16136
- * on a Language switch.
16137
- * @returns disposer unmounting the tree and restoring the column.
16171
+ * @returns disposer releasing both registrations.
16138
16172
  */
16139
- function mountPanel(controller, api, terminalFont, locale) {
16140
- return mountCenterPanel({
16141
- render: (root) => root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
16173
+ function registerSshPanel(ctx, controller, api, terminalFont) {
16174
+ const slots = ctx.slots;
16175
+ const disposers = [];
16176
+ disposers.push(slots.inject("sidebar.panellist", () => slots.register({
16177
+ name: "sidebar.panellist",
16178
+ id: "ssh",
16179
+ order: PANEL_ORDER,
16180
+ label: () => tt("entry.label")
16181
+ }, SshPanelIcon)));
16182
+ disposers.push(slots.inject("main", () => slots.register({
16183
+ name: "main",
16184
+ key: "ssh",
16185
+ inject: () => ({
16142
16186
  controller,
16143
16187
  api,
16144
16188
  terminalFont
16145
- })),
16146
- viewDatasetKey: "dshSshView",
16147
- pluginName: "ssh",
16148
- viewClassName: panel_module_css_default.view,
16149
- activeAttribute: "data-dsh-ssh-active",
16150
- siblingActiveAttribute: "data-dsh-taskboard-active",
16151
- panelName: "ssh",
16152
- siblingPanelName: "taskboard",
16153
- isOpen: () => controller.getSnapshot().panelOpen,
16154
- close: () => controller.close(),
16155
- subscribe: (listener) => controller.subscribe(listener),
16156
- locale
16157
- });
16189
+ })
16190
+ }, SshPanelPage)));
16191
+ return () => {
16192
+ for (const dispose of disposers.splice(0)) dispose();
16193
+ };
16158
16194
  }
16159
16195
  //#endregion
16160
- //#region src/client/panel/controller.ts
16161
- /** The panel state owner the sidebar entry toggles and the view renders from. */
16162
- var PanelController = class {
16163
- panelOpen = false;
16164
- listeners = /* @__PURE__ */ new Set();
16165
- getSnapshot() {
16166
- return { panelOpen: this.panelOpen };
16167
- }
16168
- subscribe(fn) {
16169
- this.listeners.add(fn);
16170
- return () => {
16171
- this.listeners.delete(fn);
16172
- };
16173
- }
16174
- open() {
16175
- if (this.panelOpen) return;
16176
- this.panelOpen = true;
16177
- this.notify();
16178
- }
16179
- close() {
16180
- if (!this.panelOpen) return;
16181
- this.panelOpen = false;
16182
- this.notify();
16183
- }
16184
- toggle() {
16185
- if (this.panelOpen) this.close();
16186
- else this.open();
16187
- }
16188
- notify() {
16189
- for (const fn of [...this.listeners]) fn();
16190
- }
16191
- };
16192
- //#endregion
16193
16196
  //#region src/client/settings-binding.ts
16194
16197
  /**
16195
16198
  * The snapshot a reader reports before the Host has answered with this
@@ -16292,175 +16295,6 @@ window.__ModuleLoader__.load({
16292
16295
  return new SharedFormsReader(ctx.configForms, field);
16293
16296
  }
16294
16297
  //#endregion
16295
- //#region src/client/sidebar-entry-core.ts
16296
- /**
16297
- * Shared sidebar entry injection core.
16298
- *
16299
- * dsh's sidebar shell exposes no slot an external plugin can register into,
16300
- * so the entry row is injected between the shell's New Session button and the
16301
- * workspace browser. The injection self-heals: a MutationObserver watches the
16302
- * sidebar root and re-inserts the row whenever a React re-render displaces it
16303
- * (re-insertion happens in the same frame, before paint, so no flicker).
16304
- *
16305
- * The row is plain DOM (no React tree) so it can never disturb the shell's
16306
- * reconciliation; the view it toggles is a separate root owned by the caller.
16307
- *
16308
- * Packages receive this file as a generated copy via scripts/sync-shared.mjs;
16309
- * edit the shared source and re-run the sync instead of editing a copy.
16310
- */
16311
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
16312
- function sidebarRoot() {
16313
- const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
16314
- if (column === null) return void 0;
16315
- return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
16316
- }
16317
- /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
16318
- function newSessionButton(root) {
16319
- const nested = root.querySelector("button[class*=\"newSession\"]");
16320
- if (nested !== null) return nested;
16321
- for (const child of root.children) if (child.tagName === "BUTTON") return child;
16322
- }
16323
- /** Build the entry row (detached; inserted once the shell is up). */
16324
- function createEntry(options) {
16325
- const entry = document.createElement("button");
16326
- entry.type = "button";
16327
- entry.setAttribute(options.rowAttribute, "");
16328
- if (options.plugin !== void 0) {
16329
- entry.setAttribute("data-dsh-plugin", options.plugin);
16330
- entry.setAttribute("data-dsh-part", "sidebar-entry");
16331
- }
16332
- entry.className = options.css["entry"] ?? "";
16333
- const labelSpan = document.createElement("span");
16334
- labelSpan.className = options.css["entryLabel"] ?? "";
16335
- const iconSpan = document.createElement("span");
16336
- iconSpan.className = options.css["entryIcon"] ?? "";
16337
- iconSpan.innerHTML = options.icon;
16338
- entry.append(iconSpan, labelSpan);
16339
- const applyLabel = () => {
16340
- entry.setAttribute("aria-label", options.label());
16341
- if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
16342
- labelSpan.textContent = options.label();
16343
- };
16344
- applyLabel();
16345
- entry.addEventListener("click", options.onToggle);
16346
- return {
16347
- entry,
16348
- applyLabel
16349
- };
16350
- }
16351
- /** Re-insert the entry after the New Session row (before the browser region). */
16352
- function placeEntry(root, entry, options) {
16353
- const button = newSessionButton(root);
16354
- if (button === void 0) return false;
16355
- if (entry.parentElement !== root) {
16356
- const row = button.closest("[class*=\"logoRow\"]");
16357
- const base = row !== null && row.parentElement === root ? row : button;
16358
- const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
16359
- const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
16360
- root.insertBefore(entry, anchor);
16361
- }
16362
- return true;
16363
- }
16364
- /**
16365
- * Mount the sidebar entry, waiting for the shell to render and self-healing
16366
- * on later React re-renders.
16367
- * @param options - the row's attribute/icon/copy/action/ordering configuration.
16368
- * @returns disposer removing the entry and its observers.
16369
- */
16370
- function mountSidebarEntry$1(options) {
16371
- if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
16372
- const { entry, applyLabel } = createEntry(options);
16373
- let root;
16374
- let placed = false;
16375
- let unsubscribeRefresh;
16376
- if (options.refresh !== void 0) try {
16377
- unsubscribeRefresh = options.refresh.subscribe(applyLabel);
16378
- } catch {}
16379
- const tryPlace = () => {
16380
- if (root !== void 0 && !root.isConnected) {
16381
- rootObserver.disconnect();
16382
- root = void 0;
16383
- placed = false;
16384
- }
16385
- if (placed) {
16386
- if (document.body.contains(entry)) return;
16387
- rootObserver.disconnect();
16388
- root = void 0;
16389
- placed = false;
16390
- }
16391
- root ??= sidebarRoot();
16392
- if (root === void 0) return;
16393
- placed = placeEntry(root, entry, options);
16394
- if (placed) rootObserver.observe(root, {
16395
- childList: true,
16396
- subtree: true
16397
- });
16398
- };
16399
- const unsubscribeBody = subscribeBodyInvalidations(() => {
16400
- tryPlace();
16401
- });
16402
- const rootObserver = new MutationObserver(() => {
16403
- if (root === void 0 || !root.isConnected) {
16404
- placed = false;
16405
- tryPlace();
16406
- return;
16407
- }
16408
- if (!root.contains(entry)) placed = placeEntry(root, entry, options);
16409
- });
16410
- const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
16411
- const syncActive = () => {
16412
- if (options.active.isOpen()) entry.dataset.active = "true";
16413
- else delete entry.dataset.active;
16414
- };
16415
- const unsubscribe = options.active.subscribe(syncActive);
16416
- syncActive();
16417
- return unsubscribe;
16418
- })();
16419
- tryPlace();
16420
- return () => {
16421
- unsubscribeBody();
16422
- rootObserver.disconnect();
16423
- unsubscribeRefresh?.();
16424
- unsubscribeActive?.();
16425
- entry.remove();
16426
- };
16427
- }
16428
- //#endregion
16429
- //#region src/client/sidebar-entry.ts
16430
- /** Stable data attribute identifying the injected entry row. */
16431
- const ENTRY_SELECTOR = "[data-dsh-ssh-entry]";
16432
- /** Inline terminal glyph sized to the shell's panel-row navigation icons. */
16433
- const ICON = "<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"1.75\" y=\"2.25\" width=\"12.5\" height=\"11.5\" rx=\"1.75\"/><path d=\"M4.25 5.25l2.75 2.75-2.75 2.75\"/><path d=\"M8.5 10.75h3.25\"/></svg>";
16434
- /**
16435
- * Mount the sidebar entry, waiting for the shell to render and self-healing
16436
- * on later React re-renders.
16437
- * @param controller - the panel controller the entry toggles.
16438
- * @param locale - locale-change source; when given, re-applies the label on
16439
- * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
16440
- * @returns disposer removing the entry and its observers.
16441
- */
16442
- function mountSidebarEntry(controller, locale) {
16443
- return mountSidebarEntry$1({
16444
- rowAttribute: "data-dsh-ssh-entry",
16445
- rowSelector: ENTRY_SELECTOR,
16446
- plugin: "ssh",
16447
- icon: ICON,
16448
- css: panel_module_css_default,
16449
- label: () => tt("entry.label"),
16450
- tooltip: () => tt("entry.tooltip"),
16451
- refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
16452
- onToggle: () => {
16453
- controller.toggle();
16454
- },
16455
- position: "after",
16456
- familySelectors: ["[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]"],
16457
- active: {
16458
- subscribe: (listener) => controller.subscribe(listener),
16459
- isOpen: () => controller.getSnapshot().panelOpen
16460
- }
16461
- });
16462
- }
16463
- //#endregion
16464
16298
  //#region src/client/telemetry.ts
16465
16299
  const VISITOR_KEY = "dsh-web-ui-telemetry-visitor";
16466
16300
  const DAY_KEY_PREFIX = "dsh-web-ui-telemetry-day:";
@@ -16468,7 +16302,7 @@ window.__ModuleLoader__.load({
16468
16302
  /** The building package's version, when the bundle carries it. */
16469
16303
  function bakedVersion() {
16470
16304
  try {
16471
- return "0.4.1";
16305
+ return "0.4.3";
16472
16306
  } catch {
16473
16307
  return;
16474
16308
  }
@@ -16566,7 +16400,9 @@ window.__ModuleLoader__.load({
16566
16400
  try {
16567
16401
  setRuntimeTranslate(ctx.locale.bind(NS));
16568
16402
  } catch {}
16569
- const controller = new PanelController();
16403
+ const controller = new PanelController({ panel: { select: (panelId) => {
16404
+ ctx.get("layout")?.selectPanel?.(panelId);
16405
+ } } });
16570
16406
  const api = new SshApi();
16571
16407
  const settings = bindSettingsReader(ctx, SETTINGS_NS, TERMINAL_FONT_FIELD);
16572
16408
  const terminalFont = {
@@ -16581,10 +16417,18 @@ window.__ModuleLoader__.load({
16581
16417
  }, "dsh-ssh: settings binding");
16582
16418
  const disposers = [];
16583
16419
  try {
16584
- disposers.push(mountSidebarEntry(controller, ctx.locale));
16585
- disposers.push(mountPanel(controller, api, terminalFont, ctx.locale));
16420
+ disposers.push(registerSshPanel(ctx, controller, api, terminalFont));
16421
+ const layoutFace = ctx.get("layout");
16422
+ if (layoutFace?.panelInfo !== void 0) {
16423
+ const sync = () => {
16424
+ const active = layoutFace.panelInfo.getSnapshot().activePanelId;
16425
+ controller.syncPanelSelection(active === "ssh" ? "ssh" : null);
16426
+ };
16427
+ sync();
16428
+ disposers.push(layoutFace.panelInfo.subscribe(sync));
16429
+ }
16586
16430
  } catch (error) {
16587
- console.warn("[dsh-ssh] mount failed:", error);
16431
+ console.warn("[dsh-ssh] panel registration failed:", error);
16588
16432
  }
16589
16433
  ctx.effect(() => () => {
16590
16434
  for (const dispose of disposers.splice(0)) dispose();