@linxin666/dsh-ssh 0.4.2 → 0.4.4

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 (37) hide show
  1. package/lib/client.js +371 -461
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +707 -466
  4. package/lib/types/client/api.d.ts +55 -4
  5. package/lib/types/client/locales.d.ts +1 -0
  6. package/lib/types/client/native-panel.d.ts +70 -0
  7. package/lib/types/client/panel/SshPanel.d.ts +1 -3
  8. package/lib/types/client/panel/TerminalTab.d.ts +6 -1
  9. package/lib/types/client/panel/controller.d.ts +71 -4
  10. package/lib/types/engine/terminal-sessions.d.ts +67 -0
  11. package/lib/types/mount-once.d.ts +38 -7
  12. package/lib/types/protocol.d.ts +12 -1
  13. package/lib/types/routes.d.ts +2 -0
  14. package/package.json +19 -16
  15. package/src/client/api.ts +137 -24
  16. package/src/client/index.ts +31 -9
  17. package/src/client/locales.ts +2 -0
  18. package/src/client/native-panel.tsx +141 -0
  19. package/src/client/panel/SshPanel.tsx +25 -28
  20. package/src/client/panel/TerminalTab.tsx +54 -14
  21. package/src/client/panel/controller.ts +126 -7
  22. package/src/client/panel/panel.module.css +8 -128
  23. package/src/engine/terminal-sessions.ts +280 -0
  24. package/src/index.ts +4 -1
  25. package/src/mount-once.ts +107 -13
  26. package/src/protocol.ts +6 -1
  27. package/src/routes.ts +26 -107
  28. package/lib/types/client/body-mutations.d.ts +0 -45
  29. package/lib/types/client/mount.d.ts +0 -27
  30. package/lib/types/client/panel-mount-core.d.ts +0 -56
  31. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  32. package/lib/types/client/sidebar-entry.d.ts +0 -26
  33. package/src/client/body-mutations.ts +0 -133
  34. package/src/client/mount.tsx +0 -47
  35. package/src/client/panel-mount-core.ts +0 -155
  36. package/src/client/sidebar-entry-core.ts +0 -237
  37. 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",
@@ -116,6 +115,7 @@ window.__ModuleLoader__.load({
116
115
  "terminal.ready": "终端已连接({alias})",
117
116
  "terminal.exited": "终端已退出({alias})",
118
117
  "terminal.error": "终端错误:{error}",
118
+ "terminal.noWebSocket": "当前页面不支持 WebSocket(应用外壳只转发 HTTP),请改用浏览器打开的 Web 界面使用终端。",
119
119
  "terminal.auth.title": "二次身份验证 (2FA / 交互式认证)",
120
120
  "terminal.auth.submit": "提交验证码",
121
121
  "terminal.auth.cancel": "取消",
@@ -271,6 +271,7 @@ window.__ModuleLoader__.load({
271
271
  "terminal.ready": "Terminal connected ({alias})",
272
272
  "terminal.exited": "Terminal exited ({alias})",
273
273
  "terminal.error": "Terminal error: {error}",
274
+ "terminal.noWebSocket": "This page cannot carry a WebSocket (the application shell forwards HTTP only). Open the Web UI in a browser to use the terminal.",
274
275
  "terminal.auth.title": "Two-Factor Authentication (2FA)",
275
276
  "terminal.auth.submit": "Submit Code",
276
277
  "terminal.auth.cancel": "Cancel",
@@ -448,6 +449,63 @@ window.__ModuleLoader__.load({
448
449
  const text = search.toString();
449
450
  return text === "" ? "" : "?" + text;
450
451
  }
452
+ /**
453
+ * The WebSocket URL the terminal route is reached at, or undefined when this
454
+ * page cannot carry one.
455
+ *
456
+ * A page delivered by an application on this machine (the official DSH
457
+ * Desktop shell serves its Web GUI from `dsh-app://app/`) has no WebSocket
458
+ * transport: its scheme handler forwards HTTP requests to the local host but
459
+ * cannot upgrade a socket, so `ws://app/...` never connects and the terminal
460
+ * reports "connection error" (issue #1744). Every other page is a web page and
461
+ * resolves the socket against its own origin, exactly as before.
462
+ *
463
+ * A blank authority is treated the same as an application scheme: there is
464
+ * nothing to dial, and the caller gets the actionable reason instead of a
465
+ * socket that can only fail.
466
+ *
467
+ * @param location - the page location to read.
468
+ * @param search - the query string carrying `alias` or `session`.
469
+ * @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
470
+ */
471
+ function terminalSocketUrl(location, search) {
472
+ if (WEB_PAGE_PROTOCOLS.includes(location.protocol)) return (location.protocol === "https:" ? "wss" : "ws") + "://" + location.host + SSH_API.terminal + search;
473
+ }
474
+ /**
475
+ * Schemes a network page can be delivered with. Every other scheme belongs to
476
+ * an application on this machine, which is the same classification the remote
477
+ * channel and the update seat use (`isWebPageProtocol` / `isApplicationDeliveredPage`).
478
+ */
479
+ const WEB_PAGE_PROTOCOLS = [
480
+ "http:",
481
+ "https:",
482
+ "blob:",
483
+ "data:",
484
+ "about:",
485
+ "filesystem:"
486
+ ];
487
+ /**
488
+ * A terminal connection that never opened: it reports one failure once the
489
+ * view has attached its `onExit`, so the tab shows why instead of sitting on a
490
+ * spinner, and every later frame is a no-op.
491
+ */
492
+ function failedTerminal(reason) {
493
+ const connection = {
494
+ onReady: void 0,
495
+ onOutput: void 0,
496
+ onExit: void 0,
497
+ onAuthPrompt: void 0,
498
+ send: () => void 0,
499
+ resize: () => void 0,
500
+ sendAuthResponse: () => void 0,
501
+ detach: () => void 0,
502
+ close: () => void 0
503
+ };
504
+ queueMicrotask(() => {
505
+ connection.onExit?.(null, reason);
506
+ });
507
+ return connection;
508
+ }
451
509
  /** The browser half's only data entry point. */
452
510
  var SshApi = class {
453
511
  async listHosts(queryText) {
@@ -644,39 +702,83 @@ window.__ModuleLoader__.load({
644
702
  bytes: received
645
703
  };
646
704
  }
647
- /** Open a WebSocket terminal session. */
705
+ /**
706
+ * Open a new host terminal session for the alias.
707
+ * @param alias - the configured host alias.
708
+ * @param cols - initial PTY width.
709
+ * @param rows - initial PTY height.
710
+ * @returns the live connection; its ready callback carries the session id
711
+ * {@link attachTerminal} uses after a view detach.
712
+ */
648
713
  openTerminal(alias, cols, rows) {
649
- const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + query({
714
+ return this.terminalSocket(query({
650
715
  alias,
651
716
  cols,
652
717
  rows
653
- });
654
- const socket = new WebSocket(url);
718
+ }));
719
+ }
720
+ /**
721
+ * Reattach to a host-side session that outlived its view (a panel switch, a
722
+ * page navigation). The host replays its scrollback before the ready frame.
723
+ * @param sessionId - the id the previous connection reported on ready.
724
+ * @param cols - the view's current width.
725
+ * @param rows - the view's current height.
726
+ */
727
+ attachTerminal(sessionId, cols, rows) {
728
+ return this.terminalSocket(query({
729
+ session: sessionId,
730
+ cols,
731
+ rows
732
+ }));
733
+ }
734
+ /** One terminal socket over either an alias (open) or a session id (attach). */
735
+ terminalSocket(search) {
736
+ const target = terminalSocketUrl(window.location, search);
737
+ if (target === void 0) return failedTerminal(tt("terminal.noWebSocket"));
738
+ let socket;
739
+ try {
740
+ socket = new WebSocket(target);
741
+ } catch (error) {
742
+ return failedTerminal(error instanceof Error ? error.message : String(error));
743
+ }
744
+ let leaving = false;
745
+ const sendFrame = (frame) => {
746
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(frame));
747
+ };
655
748
  const connection = {
656
749
  onReady: void 0,
657
750
  onOutput: void 0,
658
751
  onExit: void 0,
659
752
  onAuthPrompt: void 0,
660
753
  send: (data) => {
661
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
754
+ sendFrame({
662
755
  type: "input",
663
756
  data
664
- }));
757
+ });
665
758
  },
666
759
  resize: (cols, rows) => {
667
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
760
+ sendFrame({
668
761
  type: "resize",
669
762
  cols,
670
763
  rows
671
- }));
764
+ });
672
765
  },
673
766
  sendAuthResponse: (responses) => {
674
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
767
+ sendFrame({
675
768
  type: "auth_response",
676
769
  responses
677
- }));
770
+ });
771
+ },
772
+ detach: () => {
773
+ leaving = true;
774
+ sendFrame({ type: "detach" });
775
+ try {
776
+ socket.close();
777
+ } catch {}
678
778
  },
679
779
  close: () => {
780
+ leaving = true;
781
+ sendFrame({ type: "close" });
680
782
  try {
681
783
  socket.close();
682
784
  } catch {}
@@ -689,23 +791,127 @@ window.__ModuleLoader__.load({
689
791
  } catch {
690
792
  return;
691
793
  }
692
- if (frame.type === "ready") connection.onReady?.();
794
+ if (frame.type === "ready") connection.onReady?.(frame.sessionId, frame.alias);
693
795
  else if (frame.type === "output") connection.onOutput?.(frame.data);
694
796
  else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
695
797
  else if (frame.type === "auth_prompt") connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts);
696
798
  };
697
799
  socket.onclose = () => {
698
- connection.onExit?.(null, "connection closed");
800
+ if (!leaving) connection.onExit?.(null, "connection closed");
699
801
  };
700
802
  socket.onerror = () => {
701
- connection.onExit?.(null, "connection error");
803
+ if (!leaving) connection.onExit?.(null, "connection error");
702
804
  };
703
805
  return connection;
704
806
  }
705
807
  };
808
+ /** The panel state owner the sidebar row and the view both read. */
809
+ var PanelController = class {
810
+ deps;
811
+ panelOpen = false;
812
+ activeTab = "hosts";
813
+ connectRequest = null;
814
+ terminalSessionId;
815
+ listeners = /* @__PURE__ */ new Set();
816
+ /** Cached so getSnapshot is referentially stable between changes. */
817
+ snapshot = {
818
+ panelOpen: false,
819
+ activeTab: "hosts",
820
+ connectRequest: null,
821
+ terminalSessionId: void 0
822
+ };
823
+ constructor(deps = {}) {
824
+ this.deps = deps;
825
+ }
826
+ getSnapshot() {
827
+ return this.snapshot;
828
+ }
829
+ subscribe(fn) {
830
+ this.listeners.add(fn);
831
+ return () => {
832
+ this.listeners.delete(fn);
833
+ };
834
+ }
835
+ /**
836
+ * Show the panel. The layout owns which panel the column renders, so the
837
+ * state flip and the selection travel together; the selection is requested
838
+ * after the snapshot flips, so a subscriber never observes "open" while the
839
+ * shell still shows the conversation.
840
+ */
841
+ open() {
842
+ if (this.panelOpen) return;
843
+ this.panelOpen = true;
844
+ this.commit();
845
+ this.selectPanel("ssh");
846
+ }
847
+ /** Return the column to the conversation, asking the layout explicitly. */
848
+ close() {
849
+ if (!this.panelOpen) return;
850
+ this.panelOpen = false;
851
+ this.commit();
852
+ this.selectPanel(null);
853
+ }
854
+ toggle() {
855
+ if (this.panelOpen) this.close();
856
+ else this.open();
857
+ }
858
+ /**
859
+ * Reflect a selection made outside this controller (the user clicked another
860
+ * sidebar row, or the layout dropped the panel id).
861
+ * @param panelId - the layout's current panel id, or null for the conversation.
862
+ */
863
+ syncPanelSelection(panelId) {
864
+ const open = panelId === "ssh";
865
+ if (open === this.panelOpen) return;
866
+ this.panelOpen = open;
867
+ this.commit();
868
+ }
869
+ /** Switch tabs, keeping every other piece of view state. */
870
+ setActiveTab(tab) {
871
+ if (this.activeTab === tab) return;
872
+ this.activeTab = tab;
873
+ this.commit();
874
+ }
875
+ /** Hand the terminal tab a host to connect to (hosts-tab "connect" action). */
876
+ requestConnect(alias) {
877
+ this.connectRequest = {
878
+ alias,
879
+ nonce: Date.now()
880
+ };
881
+ this.activeTab = "terminal";
882
+ this.commit();
883
+ }
884
+ /** Record the host-side session so a later page mount can reattach. */
885
+ setTerminalSession(sessionId) {
886
+ if (this.terminalSessionId === sessionId) return;
887
+ this.terminalSessionId = sessionId;
888
+ this.commit();
889
+ }
890
+ /** Forget the session (it exited, or the user disconnected). */
891
+ clearTerminalSession() {
892
+ if (this.terminalSessionId === void 0) return;
893
+ this.terminalSessionId = void 0;
894
+ this.commit();
895
+ }
896
+ commit() {
897
+ this.snapshot = {
898
+ panelOpen: this.panelOpen,
899
+ activeTab: this.activeTab,
900
+ connectRequest: this.connectRequest,
901
+ terminalSessionId: this.terminalSessionId
902
+ };
903
+ for (const fn of [...this.listeners]) fn();
904
+ }
905
+ /** Ask the layout to select a panel; a shell without the face is a no-op. */
906
+ selectPanel(panelId) {
907
+ try {
908
+ this.deps.panel?.select(panelId);
909
+ } catch {}
910
+ }
911
+ };
706
912
  //#endregion
707
913
  //#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}";
914
+ 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
915
  const tagId = "@linxin666/dsh-ssh/packages/dsh-ssh/src/client/panel/panel.module.css";
710
916
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
711
917
  const tag = document.createElement("style");
@@ -736,9 +942,6 @@ window.__ModuleLoader__.load({
736
942
  "dirType": "mL8Uca_dirType",
737
943
  "dshSshSpin": "mL8Uca_dshSshSpin",
738
944
  "empty": "mL8Uca_empty",
739
- "entry": "mL8Uca_entry",
740
- "entryIcon": "mL8Uca_entryIcon",
741
- "entryLabel": "mL8Uca_entryLabel",
742
945
  "field": "mL8Uca_field",
743
946
  "fieldLabel": "mL8Uca_fieldLabel",
744
947
  "fillBody": "mL8Uca_fillBody",
@@ -14893,7 +15096,7 @@ window.__ModuleLoader__.load({
14893
15096
  subscribe: () => () => void 0
14894
15097
  };
14895
15098
  /** The xterm terminal view. */
14896
- function TerminalTab({ api, presetAlias, requestId, terminalFont }) {
15099
+ function TerminalTab({ api, controller, sessionId, presetAlias, requestId, terminalFont }) {
14897
15100
  const [hosts, setHosts] = (0, react.useState)([]);
14898
15101
  const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
14899
15102
  const [status, setStatus] = (0, react.useState)({ kind: "idle" });
@@ -14938,7 +15141,12 @@ window.__ModuleLoader__.load({
14938
15141
  (0, react.useEffect)(() => {
14939
15142
  if (presetAlias !== void 0) setAlias(presetAlias);
14940
15143
  }, [presetAlias, requestId]);
14941
- const teardown = () => {
15144
+ /**
15145
+ * Tear the view down.
15146
+ * @param leave - `detach` leaves the host session alive for a reattach
15147
+ * (panel switch, unmount); `close` ends it (the disconnect control).
15148
+ */
15149
+ const teardown = (leave) => {
14942
15150
  setAuthPrompt(void 0);
14943
15151
  setAuthInputs([]);
14944
15152
  const connection = connRef.current;
@@ -14948,7 +15156,8 @@ window.__ModuleLoader__.load({
14948
15156
  connection.onOutput = void 0;
14949
15157
  connection.onExit = void 0;
14950
15158
  connection.onAuthPrompt = void 0;
14951
- connection.close();
15159
+ if (leave === "detach") connection.detach();
15160
+ else connection.close();
14952
15161
  }
14953
15162
  dataSubRef.current?.dispose();
14954
15163
  dataSubRef.current = null;
@@ -14957,7 +15166,7 @@ window.__ModuleLoader__.load({
14957
15166
  fitRef.current = null;
14958
15167
  };
14959
15168
  (0, react.useEffect)(() => () => {
14960
- teardown();
15169
+ teardown("detach");
14961
15170
  }, []);
14962
15171
  (0, react.useEffect)(() => {
14963
15172
  let lastCols = -1;
@@ -14988,12 +15197,15 @@ window.__ModuleLoader__.load({
14988
15197
  window.removeEventListener("resize", sync);
14989
15198
  };
14990
15199
  }, []);
14991
- const connect = () => {
14992
- const target = alias;
15200
+ /**
15201
+ * Build the xterm view and bind one connection to it.
15202
+ * @param open - opens the transport once the terminal has a size.
15203
+ */
15204
+ const startSession = (open) => {
14993
15205
  const container = containerRef.current;
14994
- if (target === "" || container === null) return;
15206
+ if (container === null) return;
14995
15207
  if (status.kind === "connecting" || status.kind === "connected") return;
14996
- teardown();
15208
+ teardown("detach");
14997
15209
  setStatus({ kind: "connecting" });
14998
15210
  const term = new import_xterm.Terminal({
14999
15211
  convertEol: false,
@@ -15010,11 +15222,12 @@ window.__ModuleLoader__.load({
15010
15222
  term.loadAddon(fit);
15011
15223
  term.open(container);
15012
15224
  fit.fit();
15013
- const connection = api.openTerminal(target, term.cols, term.rows);
15225
+ const connection = open(term.cols, term.rows);
15014
15226
  termRef.current = term;
15015
15227
  fitRef.current = fit;
15016
15228
  connRef.current = connection;
15017
15229
  let settled = false;
15230
+ let connectedAlias = "";
15018
15231
  dataSubRef.current = term.onData((data) => {
15019
15232
  connection.send(data);
15020
15233
  });
@@ -15026,12 +15239,14 @@ window.__ModuleLoader__.load({
15026
15239
  });
15027
15240
  setAuthInputs(prompts.map(() => ""));
15028
15241
  };
15029
- connection.onReady = () => {
15242
+ connection.onReady = (id, readyAlias) => {
15243
+ connectedAlias = readyAlias;
15244
+ controller.setTerminalSession(id);
15030
15245
  setAuthPrompt(void 0);
15031
15246
  setAuthInputs([]);
15032
15247
  setStatus({
15033
15248
  kind: "connected",
15034
- alias: target
15249
+ alias: readyAlias
15035
15250
  });
15036
15251
  };
15037
15252
  connection.onOutput = (data) => {
@@ -15046,15 +15261,29 @@ window.__ModuleLoader__.load({
15046
15261
  dataSubRef.current = null;
15047
15262
  term.options.disableStdin = true;
15048
15263
  connRef.current = null;
15264
+ controller.clearTerminalSession();
15049
15265
  setStatus({
15050
15266
  kind: "exited",
15051
- alias: target,
15267
+ alias: connectedAlias !== "" ? connectedAlias : alias,
15052
15268
  detail: error
15053
15269
  });
15054
15270
  };
15055
15271
  };
15272
+ const connect = () => {
15273
+ const target = alias;
15274
+ if (target === "") return;
15275
+ startSession((cols, rows) => api.openTerminal(target, cols, rows));
15276
+ };
15277
+ const reattachedRef = (0, react.useRef)(void 0);
15278
+ (0, react.useEffect)(() => {
15279
+ if (sessionId === void 0 || connRef.current !== null) return;
15280
+ if (reattachedRef.current === sessionId) return;
15281
+ reattachedRef.current = sessionId;
15282
+ startSession((cols, rows) => api.attachTerminal(sessionId, cols, rows));
15283
+ }, [sessionId]);
15056
15284
  const disconnect = () => {
15057
- teardown();
15285
+ teardown("close");
15286
+ controller.clearTerminalSession();
15058
15287
  setStatus({ kind: "idle" });
15059
15288
  };
15060
15289
  const submitAuth = (e) => {
@@ -15830,10 +16059,14 @@ window.__ModuleLoader__.load({
15830
16059
  //#region src/client/panel/SshPanel.tsx
15831
16060
  /**
15832
16061
  * 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.
16062
+ * bar, and the active tab's content.
16063
+ *
16064
+ * The active tab, the pending connect request and the live terminal session id
16065
+ * live in the controller, not in component state: the layout mounts this page
16066
+ * only while the panel is selected, so local state would reset the tab on
16067
+ * every panel switch and lose the id the terminal tab needs to reattach its
16068
+ * host-side session. Inactive tabs unmount, so each tab fetches its own data
16069
+ * on activation.
15837
16070
  */
15838
16071
  /** The tab bar definition (labels resolved at render time). */
15839
16072
  const TABS = [
@@ -15860,16 +16093,7 @@ window.__ModuleLoader__.load({
15860
16093
  ];
15861
16094
  /** The tabbed SSH panel. */
15862
16095
  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
- };
16096
+ const { panelOpen, activeTab, connectRequest, terminalSessionId } = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => controller.subscribe(listener), [controller]), (0, react.useCallback)(() => controller.getSnapshot(), [controller]));
15873
16097
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15874
16098
  className: panel_module_css_default.panel,
15875
16099
  "data-dsh-plugin": "ssh",
@@ -15905,7 +16129,7 @@ window.__ModuleLoader__.load({
15905
16129
  "data-dsh-part": "tab",
15906
16130
  className: panel_module_css_default.tab,
15907
16131
  onClick: () => {
15908
- setActiveTab(tab.id);
16132
+ controller.setActiveTab(tab.id);
15909
16133
  },
15910
16134
  children: tab.label()
15911
16135
  }, tab.id))
@@ -15915,12 +16139,16 @@ window.__ModuleLoader__.load({
15915
16139
  children: [
15916
16140
  activeTab === "hosts" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostsTab, {
15917
16141
  api,
15918
- onConnect: handleConnect
16142
+ onConnect: (alias) => {
16143
+ controller.requestConnect(alias);
16144
+ }
15919
16145
  }),
15920
16146
  activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
15921
16147
  api,
16148
+ controller,
15922
16149
  presetAlias: connectRequest?.alias,
15923
16150
  requestId: connectRequest?.nonce,
16151
+ sessionId: terminalSessionId,
15924
16152
  terminalFont
15925
16153
  }),
15926
16154
  activeTab === "transfer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferTab, { api }),
@@ -15935,261 +16163,101 @@ window.__ModuleLoader__.load({
15935
16163
  });
15936
16164
  }
15937
16165
  //#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
- }
15946
- /**
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
- }
16166
+ //#region src/client/native-panel.tsx
16167
+ /** Row order among the shell's global panel rows (Plugins 0, Schedule 10, board 20, skill center 30). */
16168
+ const PANEL_ORDER = 40;
15958
16169
  /**
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.
16029
- *
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.
16170
+ * The sidebar row glyph the shell asks for at its own size and active state.
16171
+ * The shell owns the button, label, tooltip and rail geometry; this component
16172
+ * draws only the glyph, like every other panel row.
16040
16173
  *
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).
16174
+ * The glyph carries `data-dsh-panel-entry` because it is the only DOM this
16175
+ * panel's own code owns inside that shell-owned row: the L2 contract (skins)
16176
+ * resolves which row belongs to which plugin through it, since the shell
16177
+ * stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
16178
+ * @param props - the shell's icon share: square edge and selection state.
16179
+ * @returns the decorative terminal glyph.
16046
16180
  */
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;
16181
+ function SshPanelIcon({ size }) {
16182
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
16183
+ "data-dsh-panel-entry": "ssh",
16184
+ viewBox: "0 0 16 16",
16185
+ width: size,
16186
+ height: size,
16187
+ fill: "none",
16188
+ stroke: "currentColor",
16189
+ strokeWidth: "1.5",
16190
+ strokeLinecap: "round",
16191
+ strokeLinejoin: "round",
16192
+ "aria-hidden": "true",
16193
+ children: [
16194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
16195
+ x: "1.75",
16196
+ y: "2.25",
16197
+ width: "12.5",
16198
+ height: "11.5",
16199
+ rx: "1.75"
16200
+ }),
16201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.25 5.25l2.75 2.75-2.75 2.75" }),
16202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.5 10.75h3.25" })
16203
+ ]
16204
+ });
16054
16205
  }
16055
16206
  /**
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.
16207
+ * The main-slot page. The layout mounts it only while this panel is selected;
16208
+ * the wrapper carries the pinned `data-dsh-ssh-view` semantic anchor (L2
16209
+ * contract, skins) the takeover container used to own.
16210
+ * @param props - the framework main-slot share plus this entry's injected face.
16211
+ * @returns the SSH panel page.
16059
16212
  */
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();
16213
+ function SshPanelPage({ controller, api, terminalFont }) {
16214
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16215
+ className: panel_module_css_default.view,
16216
+ "data-dsh-ssh-view": "",
16217
+ "data-dsh-plugin": "ssh",
16218
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
16219
+ controller,
16220
+ api,
16221
+ terminalFont
16222
+ })
16091
16223
  });
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
16224
  }
16127
- //#endregion
16128
- //#region src/client/mount.tsx
16129
16225
  /**
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.
16226
+ * Register the SSH panel's sidebar row and center-column page.
16227
+ *
16228
+ * Both seats are declared by shell plugins this package does not depend on at
16229
+ * runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
16230
+ * runs only after the owning entry declares the seat, and a shell that never
16231
+ * declares it leaves the panel simply absent instead of failing boot.
16232
+ * @param ctx - client root context (services: slots).
16233
+ * @param controller - the controller the page and the row drive.
16234
+ * @param api - the SSH API client the page operates through.
16134
16235
  * @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.
16236
+ * @returns disposer releasing both registrations.
16138
16237
  */
16139
- function mountPanel(controller, api, terminalFont, locale) {
16140
- return mountCenterPanel({
16141
- render: (root) => root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
16238
+ function registerSshPanel(ctx, controller, api, terminalFont) {
16239
+ const slots = ctx.slots;
16240
+ const disposers = [];
16241
+ disposers.push(slots.inject("sidebar.panellist", () => slots.register({
16242
+ name: "sidebar.panellist",
16243
+ id: "ssh",
16244
+ order: PANEL_ORDER,
16245
+ label: () => tt("entry.label")
16246
+ }, SshPanelIcon)));
16247
+ disposers.push(slots.inject("main", () => slots.register({
16248
+ name: "main",
16249
+ key: "ssh",
16250
+ inject: () => ({
16142
16251
  controller,
16143
16252
  api,
16144
16253
  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
- });
16254
+ })
16255
+ }, SshPanelPage)));
16256
+ return () => {
16257
+ for (const dispose of disposers.splice(0)) dispose();
16258
+ };
16158
16259
  }
16159
16260
  //#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
16261
  //#region src/client/settings-binding.ts
16194
16262
  /**
16195
16263
  * The snapshot a reader reports before the Host has answered with this
@@ -16292,175 +16360,6 @@ window.__ModuleLoader__.load({
16292
16360
  return new SharedFormsReader(ctx.configForms, field);
16293
16361
  }
16294
16362
  //#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
16363
  //#region src/client/telemetry.ts
16465
16364
  const VISITOR_KEY = "dsh-web-ui-telemetry-visitor";
16466
16365
  const DAY_KEY_PREFIX = "dsh-web-ui-telemetry-day:";
@@ -16468,7 +16367,7 @@ window.__ModuleLoader__.load({
16468
16367
  /** The building package's version, when the bundle carries it. */
16469
16368
  function bakedVersion() {
16470
16369
  try {
16471
- return "0.4.2";
16370
+ return "0.4.4";
16472
16371
  } catch {
16473
16372
  return;
16474
16373
  }
@@ -16545,7 +16444,8 @@ window.__ModuleLoader__.load({
16545
16444
  const inject = [
16546
16445
  "slots",
16547
16446
  "locale",
16548
- "configForms"
16447
+ "configForms",
16448
+ "layout"
16549
16449
  ];
16550
16450
  /**
16551
16451
  * Mount the SSH panel.
@@ -16566,7 +16466,9 @@ window.__ModuleLoader__.load({
16566
16466
  try {
16567
16467
  setRuntimeTranslate(ctx.locale.bind(NS));
16568
16468
  } catch {}
16569
- const controller = new PanelController();
16469
+ const controller = new PanelController({ panel: { select: (panelId) => {
16470
+ (ctx.get?.("layout"))?.selectPanel?.(panelId);
16471
+ } } });
16570
16472
  const api = new SshApi();
16571
16473
  const settings = bindSettingsReader(ctx, SETTINGS_NS, TERMINAL_FONT_FIELD);
16572
16474
  const terminalFont = {
@@ -16581,10 +16483,18 @@ window.__ModuleLoader__.load({
16581
16483
  }, "dsh-ssh: settings binding");
16582
16484
  const disposers = [];
16583
16485
  try {
16584
- disposers.push(mountSidebarEntry(controller, ctx.locale));
16585
- disposers.push(mountPanel(controller, api, terminalFont, ctx.locale));
16486
+ disposers.push(registerSshPanel(ctx, controller, api, terminalFont));
16487
+ const layoutFace = ctx.get?.("layout");
16488
+ if (layoutFace?.panelInfo !== void 0) {
16489
+ const sync = () => {
16490
+ const active = layoutFace.panelInfo.getSnapshot().activePanelId;
16491
+ controller.syncPanelSelection(active === "ssh" ? "ssh" : null);
16492
+ };
16493
+ sync();
16494
+ disposers.push(layoutFace.panelInfo.subscribe(sync));
16495
+ }
16586
16496
  } catch (error) {
16587
- console.warn("[dsh-ssh] mount failed:", error);
16497
+ console.warn("[dsh-ssh] panel registration failed:", error);
16588
16498
  }
16589
16499
  ctx.effect(() => () => {
16590
16500
  for (const dispose of disposers.splice(0)) dispose();