@linxin666/dsh-ssh 0.1.1 → 0.1.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.
- package/README.md +4 -1
- package/lib/client.js +46 -13
- package/lib/client.js.map +1 -0
- package/package.json +9 -6
- package/src/client/mount.tsx +31 -0
- package/src/client/panel/panel.module.css +5 -2
- package/src/client/sidebar-entry.ts +33 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -85,16 +85,19 @@
|
|
|
85
85
|
"vitest": "^3.0.0"
|
|
86
86
|
},
|
|
87
87
|
"files": [
|
|
88
|
-
"lib
|
|
89
|
-
"lib
|
|
90
|
-
"lib
|
|
91
|
-
"lib
|
|
92
|
-
"lib/types/**/*.d.ts.map",
|
|
88
|
+
"lib/**/*.js",
|
|
89
|
+
"lib/**/*.js.map",
|
|
90
|
+
"lib/**/*.d.ts",
|
|
91
|
+
"lib/**/*.d.ts.map",
|
|
93
92
|
"src",
|
|
94
93
|
"cordis.patch.yml",
|
|
95
94
|
"README.md"
|
|
96
95
|
],
|
|
97
96
|
"license": "BSD-3-Clause",
|
|
97
|
+
"repository": {
|
|
98
|
+
"type": "git",
|
|
99
|
+
"url": "https://github.com/zhu1090093659/dsh-web-ui.git"
|
|
100
|
+
},
|
|
98
101
|
"scripts": {
|
|
99
102
|
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
100
103
|
"bundle": "tsdown",
|
package/src/client/mount.tsx
CHANGED
|
@@ -20,6 +20,11 @@ export const PANEL_VIEW_SELECTOR = '[data-dsh-ssh-view]'
|
|
|
20
20
|
|
|
21
21
|
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
22
22
|
const ACTIVE_ATTR = 'data-dsh-ssh-active'
|
|
23
|
+
/** The sibling panel's activation attribute (task board), removed when this panel opens. */
|
|
24
|
+
const OTHER_ACTIVE_ATTR = 'data-dsh-taskboard-active'
|
|
25
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
26
|
+
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
27
|
+
const PANEL_NAME = 'ssh'
|
|
23
28
|
|
|
24
29
|
/** Find the center column, or undefined while the frame is not mounted. */
|
|
25
30
|
function conversationColumn(): HTMLElement | undefined {
|
|
@@ -62,16 +67,42 @@ export function mountPanel(controller: PanelController, api: SshApi): () => void
|
|
|
62
67
|
|
|
63
68
|
const applyActive = (): void => {
|
|
64
69
|
if (controller.getSnapshot().panelOpen) {
|
|
70
|
+
// Single-occupant center column: opening this panel must evict the
|
|
71
|
+
// sibling panel (task board), both its html attribute and its
|
|
72
|
+
// controller state, otherwise the two panels' visibility rules fight
|
|
73
|
+
// and the second click appears dead.
|
|
74
|
+
document.documentElement.removeAttribute(OTHER_ACTIVE_ATTR)
|
|
65
75
|
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
76
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
66
77
|
} else {
|
|
67
78
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
const onOtherActivate = (event: Event): void => {
|
|
82
|
+
if ((event as CustomEvent).detail === 'taskboard' && controller.getSnapshot().panelOpen) {
|
|
83
|
+
controller.close()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Jump out on sidebar context clicks: clicking a session/workspace row
|
|
87
|
+
// (including the already-current one, which produces no session-change
|
|
88
|
+
// event) hands the center column back to the conversation. Capture phase,
|
|
89
|
+
// so the panel closes before the shell processes the click.
|
|
90
|
+
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
91
|
+
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
92
|
+
if (!controller.getSnapshot().panelOpen) return
|
|
93
|
+
const target = event.target as HTMLElement | null
|
|
94
|
+
if (target === null) return
|
|
95
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
96
|
+
}
|
|
97
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
98
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
70
99
|
const unsubscribe = controller.subscribe(applyActive)
|
|
71
100
|
applyActive()
|
|
72
101
|
ensure()
|
|
73
102
|
|
|
74
103
|
return () => {
|
|
104
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
105
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
75
106
|
waitObserver.disconnect()
|
|
76
107
|
unsubscribe()
|
|
77
108
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
@@ -21,13 +21,16 @@
|
|
|
21
21
|
z-index: 5;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/* The center column is single-occupant; the :not() guards keep the two
|
|
25
|
+
sibling panels (task board / ssh) from fighting over visibility if both
|
|
26
|
+
activation attributes ever coexist. */
|
|
27
|
+
html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view] {
|
|
25
28
|
display: block;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/* While the panel is active, the conversation content underneath is hidden
|
|
29
32
|
(it stays mounted and stateful). */
|
|
30
|
-
html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view]) {
|
|
33
|
+
html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane='conversation'] > :not([data-dsh-ssh-view]) {
|
|
31
34
|
display: none;
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -19,21 +19,24 @@ import css from './panel/panel.module.css'
|
|
|
19
19
|
/** Stable data attribute identifying the injected entry row. */
|
|
20
20
|
export const ENTRY_SELECTOR = '[data-dsh-ssh-entry]'
|
|
21
21
|
|
|
22
|
-
/** The sidebar column is the grid item AppFrame renders with this pane marker. */
|
|
23
|
-
const SIDEBAR_COLUMN_SELECTOR = '[data-pane="sidebar"]'
|
|
24
|
-
|
|
25
22
|
/** Inline icon (matches the shell's 16px nav-icon look): a terminal prompt glyph. */
|
|
26
23
|
const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M4.5 5.5l2.5 2.5-2.5 2.5"/><path d="M8.5 10.5h3"/></svg>'
|
|
27
24
|
|
|
28
25
|
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
29
26
|
function sidebarRoot(): HTMLElement | undefined {
|
|
30
|
-
const column = document.querySelector<HTMLElement>(
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
28
|
+
if (column === null) return undefined
|
|
29
|
+
// Current shells wrap the sidebar UI: column > wrapper > root(logoRow owner).
|
|
30
|
+
// Prefer the element that owns the logo row — the real sidebar UI root —
|
|
31
|
+
// and fall back to the column's first child for legacy shells.
|
|
32
|
+
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
33
|
+
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/** The New Session button: the
|
|
36
|
+
/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
|
|
36
37
|
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
38
|
+
const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
|
|
39
|
+
if (nested !== null) return nested
|
|
37
40
|
for (const child of root.children) {
|
|
38
41
|
if (child.tagName === 'BUTTON') return child as HTMLButtonElement
|
|
39
42
|
}
|
|
@@ -53,14 +56,26 @@ function createEntry(controller: PanelController): HTMLButtonElement {
|
|
|
53
56
|
return entry
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/** Re-insert the entry after the New Session
|
|
59
|
+
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
57
60
|
function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
|
|
58
61
|
const button = newSessionButton(root)
|
|
59
62
|
if (button === undefined) return false
|
|
60
63
|
if (entry.parentElement !== root) {
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
+
// Position relative to the family block (entries injected by sibling
|
|
65
|
+
// plugins), never relative to transient logoRow geometry: every family
|
|
66
|
+
// plugin that self-heals during a re-render then lands in the same
|
|
67
|
+
// relative order, so the entries cannot swap positions regardless of
|
|
68
|
+
// observer callback order or of shell wrapper changes. There is no
|
|
69
|
+
// append-to-end fallback: appending at the end would randomly reorder
|
|
70
|
+
// the block after a shell re-render.
|
|
71
|
+
const row = button.closest('[class*="logoRow"]')
|
|
72
|
+
const base = (row !== null && row.parentElement === root) ? row : button
|
|
73
|
+
const family = Array.from(root.children).filter(
|
|
74
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'),
|
|
75
|
+
)
|
|
76
|
+
// ssh sits after the whole family block.
|
|
77
|
+
const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
|
|
78
|
+
root.insertBefore(entry, anchor)
|
|
64
79
|
}
|
|
65
80
|
return true
|
|
66
81
|
}
|
|
@@ -86,7 +101,13 @@ export function mountSidebarEntry(controller: PanelController): () => void {
|
|
|
86
101
|
root ??= sidebarRoot()
|
|
87
102
|
if (root === undefined) return
|
|
88
103
|
placed = placeEntry(root, entry)
|
|
89
|
-
if (placed)
|
|
104
|
+
if (placed) {
|
|
105
|
+
rootObserver.observe(root, { childList: true, subtree: true })
|
|
106
|
+
// Placement done; the root observer alone keeps the entry healed.
|
|
107
|
+
// Disconnect the body-wide watcher so unrelated app mutations (e.g.
|
|
108
|
+
// chat streaming) no longer churn every plugin's self-heal loop.
|
|
109
|
+
waitObserver.disconnect()
|
|
110
|
+
}
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
// The shell renders after boot settlement; watch for its arrival.
|