@linxin666/dsh-ssh 0.3.9 → 0.3.11

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.
@@ -1,6 +1,7 @@
1
1
  import type { SshApi } from './api.ts';
2
2
  import type { PanelController } from './panel/controller.ts';
3
3
  import type { TerminalFontSource } from './panel/helpers.ts';
4
+ import type { LocaleRefreshSource } from './sidebar-entry.ts';
4
5
  /** The injected panel container (kept in the DOM, hidden when inactive). */
5
6
  export declare const PANEL_VIEW_SELECTOR = "[data-dsh-ssh-view]";
6
7
  /**
@@ -9,6 +10,8 @@ export declare const PANEL_VIEW_SELECTOR = "[data-dsh-ssh-view]";
9
10
  * @param controller - the panel controller driving the view.
10
11
  * @param api - the SSH API client the tabs operate through.
11
12
  * @param terminalFont - live terminal-font setting source (issue #577).
13
+ * @param locale - locale-change source; when given, re-renders an open panel
14
+ * on a Language switch.
12
15
  * @returns disposer unmounting the tree and restoring the column.
13
16
  */
14
- export declare function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource): () => void;
17
+ export declare function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource, locale?: LocaleRefreshSource): () => void;
@@ -8,6 +8,8 @@ import { type SshKey } from '../locales.ts';
8
8
  export type TranslateValues = Record<string, string | number>;
9
9
  /** Active dictionary, picked by the document language at call time. */
10
10
  export declare function dictionary(): Record<string, string>;
11
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
12
+ export declare function setRuntimeTranslate(t: ((key: SshKey, values?: TranslateValues) => string) | undefined): void;
11
13
  /** Translate a key with optional {name} template params (current language). */
12
14
  export declare function tt(key: SshKey, values?: TranslateValues): string;
13
15
  /** Human-readable error text from an unknown thrown value. */
@@ -34,6 +34,16 @@ export interface SidebarEntryOptions {
34
34
  label(): string;
35
35
  /** Optional localized tooltip (title attribute). */
36
36
  tooltip?(): string;
37
+ /**
38
+ * Optional locale-change subscription: re-applies label / aria-label /
39
+ * tooltip whenever the active locale changes. Plain-DOM rows would
40
+ * otherwise keep the label captured at mount; pass the SDK locale runtime
41
+ * subscription (ctx.locale.subscribe) so the row follows the language
42
+ * switch without a reload.
43
+ */
44
+ refresh?: {
45
+ subscribe(listener: () => void): () => void;
46
+ };
37
47
  /** Click action (open/toggle the owning panel). */
38
48
  onToggle(): void;
39
49
  /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
@@ -11,10 +11,16 @@
11
11
  import type { PanelController } from './panel/controller.ts';
12
12
  /** Stable data attribute identifying the injected entry row. */
13
13
  export declare const ENTRY_SELECTOR = "[data-dsh-ssh-entry]";
14
+ /** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
15
+ export interface LocaleRefreshSource {
16
+ subscribe(listener: () => void): () => void;
17
+ }
14
18
  /**
15
19
  * Mount the sidebar entry, waiting for the shell to render and self-healing
16
20
  * on later React re-renders.
17
21
  * @param controller - the panel controller the entry toggles.
22
+ * @param locale - locale-change source; when given, re-applies the label on
23
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
18
24
  * @returns disposer removing the entry and its observers.
19
25
  */
20
- export declare function mountSidebarEntry(controller: PanelController): () => void;
26
+ export declare function mountSidebarEntry(controller: PanelController, locale?: LocaleRefreshSource): () => void;
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.3.9",
4
+ "version": "0.3.11",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dsh": {
28
28
  "engines": {
29
- "dsh": ">=0.1.2-alpha.1"
29
+ "dsh": ">=0.1.2-alpha.3"
30
30
  },
31
31
  "bundle": {
32
32
  "patch": "./cordis.patch.yml"
@@ -52,18 +52,18 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@deepseek-ai/cordis": "^4.0.2",
55
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
56
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
57
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
58
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
60
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
61
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
62
- "@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
63
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
64
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
65
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
66
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
55
+ "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.3",
56
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.3",
57
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.3",
58
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
59
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
60
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.3",
61
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
62
+ "@deepseek-ai/dsh-scope": "^0.1.2-alpha.3",
63
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.3",
64
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
65
+ "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.3",
66
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
67
67
  "@types/node": "^22.20.0",
68
68
  "@types/react": "~18.3.1",
69
69
  "@types/react-dom": "^18.3.5",
@@ -25,6 +25,7 @@ import { en, zh, type SshKey } from './locales.ts'
25
25
  import { mountPanel } from './mount.tsx'
26
26
  import { PanelController } from './panel/controller.ts'
27
27
  import type { TerminalFontSource } from './panel/helpers.ts'
28
+ import { setRuntimeTranslate } from './panel/helpers.ts'
28
29
  import { mountSidebarEntry } from './sidebar-entry.ts'
29
30
  import { reportDailyHeartbeat } from './telemetry.ts'
30
31
 
@@ -89,6 +90,11 @@ export function apply(ctx: ClientContext): void {
89
90
  }
90
91
  }, 'dsh-ssh: dictionaries')
91
92
 
93
+ // Wire the SDK translate seat into the module-level tt (sidebar row and
94
+ // other plain-DOM callers): reads the active locale at call time, so they
95
+ // follow the Language setting without a reload.
96
+ try { setRuntimeTranslate(ctx.locale.bind(NS)) } catch { /* locale missing: document-language fallback stays */ }
97
+
92
98
  const controller = new PanelController()
93
99
  const api = new SshApi()
94
100
  // Live terminal-font preference (issue #577): the settings namespace is
@@ -105,8 +111,8 @@ export function apply(ctx: ClientContext): void {
105
111
  }
106
112
  const disposers: Array<() => void> = []
107
113
  try {
108
- disposers.push(mountSidebarEntry(controller))
109
- disposers.push(mountPanel(controller, api, terminalFont))
114
+ disposers.push(mountSidebarEntry(controller, ctx.locale))
115
+ disposers.push(mountPanel(controller, api, terminalFont, ctx.locale))
110
116
  } catch (error) {
111
117
  // DOM failures degrade the panel, never the GUI.
112
118
  console.warn('[dsh-ssh] mount failed:', error)
@@ -16,6 +16,7 @@ import type { SshApi } from './api.ts'
16
16
  import type { PanelController } from './panel/controller.ts'
17
17
  import type { TerminalFontSource } from './panel/helpers.ts'
18
18
  import { SshPanel } from './panel/SshPanel.tsx'
19
+ import type { LocaleRefreshSource } from './sidebar-entry.ts'
19
20
  import css from './panel/panel.module.css'
20
21
 
21
22
  /** The injected panel container (kept in the DOM, hidden when inactive). */
@@ -40,11 +41,19 @@ function conversationColumn(): HTMLElement | undefined {
40
41
  * @param controller - the panel controller driving the view.
41
42
  * @param api - the SSH API client the tabs operate through.
42
43
  * @param terminalFont - live terminal-font setting source (issue #577).
44
+ * @param locale - locale-change source; when given, re-renders an open panel
45
+ * on a Language switch.
43
46
  * @returns disposer unmounting the tree and restoring the column.
44
47
  */
45
- export function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource): () => void {
48
+ export function mountPanel(controller: PanelController, api: SshApi, terminalFont?: TerminalFontSource, locale?: LocaleRefreshSource): () => void {
46
49
  let root: Root | undefined
47
50
  let container: HTMLDivElement | undefined
51
+ let unsubscribeLocale: (() => void) | undefined
52
+ try {
53
+ unsubscribeLocale = locale?.subscribe(() => {
54
+ if (root !== undefined) root.render(<SshPanel controller={controller} api={api} terminalFont={terminalFont} />)
55
+ })
56
+ } catch { /* locale service absent: the panel follows its next natural re-render */ }
48
57
 
49
58
  const ensure = (): void => {
50
59
  if (container !== undefined) {
@@ -110,6 +119,7 @@ export function mountPanel(controller: PanelController, api: SshApi, terminalFon
110
119
  document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
111
120
  waitObserver.disconnect()
112
121
  unsubscribe()
122
+ unsubscribeLocale?.()
113
123
  document.documentElement.removeAttribute(ACTIVE_ATTR)
114
124
  root?.unmount()
115
125
  root = undefined
@@ -14,8 +14,22 @@ export function dictionary(): Record<string, string> {
14
14
  return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
15
15
  }
16
16
 
17
+ /**
18
+ * SDK translate seat wired by the browser apply() once ctx.locale is bound
19
+ * (setRuntimeTranslate): reads the ACTIVE locale at call time, so plain-DOM
20
+ * surfaces follow a runtime language switch. The document-language pick stays
21
+ * as the unwired fallback (locale service absent, module-scope early callers).
22
+ */
23
+ let runtimeT: ((key: SshKey, values?: TranslateValues) => string) | undefined
24
+
25
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
26
+ export function setRuntimeTranslate(t: ((key: SshKey, values?: TranslateValues) => string) | undefined): void {
27
+ runtimeT = t
28
+ }
29
+
17
30
  /** Translate a key with optional {name} template params (current language). */
18
31
  export function tt(key: SshKey, values?: TranslateValues): string {
32
+ if (runtimeT !== undefined) return runtimeT(key, values)
19
33
  return t(dictionary(), key, values)
20
34
  }
21
35
 
@@ -97,7 +97,8 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
97
97
  }
98
98
 
99
99
  /* Collapsed rail: icon-only, centered, matching the shell's 56px rail. */
100
- [data-dsh-frame][data-sidebar-collapsed] .entry {
100
+ :global([data-dsh-frame][data-sidebar-collapsed]) .entry,
101
+ :global([data-sidebar-collapsed]) .entry {
101
102
  justify-content: center;
102
103
  padding: 0;
103
104
  width: 36px;
@@ -106,7 +107,8 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
106
107
  border-radius: 50%;
107
108
  }
108
109
 
109
- [data-dsh-frame][data-sidebar-collapsed] .entryLabel {
110
+ :global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
111
+ :global([data-sidebar-collapsed]) .entryLabel {
110
112
  display: none;
111
113
  }
112
114
 
@@ -36,6 +36,14 @@ export interface SidebarEntryOptions {
36
36
  label(): string
37
37
  /** Optional localized tooltip (title attribute). */
38
38
  tooltip?(): string
39
+ /**
40
+ * Optional locale-change subscription: re-applies label / aria-label /
41
+ * tooltip whenever the active locale changes. Plain-DOM rows would
42
+ * otherwise keep the label captured at mount; pass the SDK locale runtime
43
+ * subscription (ctx.locale.subscribe) so the row follows the language
44
+ * switch without a reload.
45
+ */
46
+ refresh?: { subscribe(listener: () => void): () => void }
39
47
  /** Click action (open/toggle the owning panel). */
40
48
  onToggle(): void
41
49
  /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
@@ -76,7 +84,7 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
76
84
  }
77
85
 
78
86
  /** Build the entry row (a detached button; insert once the shell is up). */
79
- function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
87
+ function createEntry(options: SidebarEntryOptions): { entry: HTMLButtonElement; applyLabel: () => void } {
80
88
  const entry = document.createElement('button')
81
89
  entry.type = 'button'
82
90
  entry.setAttribute(options.rowAttribute, '')
@@ -85,12 +93,20 @@ function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
85
93
  entry.setAttribute('data-dsh-part', 'sidebar-entry')
86
94
  }
87
95
  entry.className = options.css['entry'] ?? ''
88
- entry.setAttribute('aria-label', options.label())
89
- if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
90
- entry.innerHTML = '<span class="' + (options.css['entryIcon'] ?? '') + '">' + options.icon
91
- + '</span><span class="' + (options.css['entryLabel'] ?? '') + '">' + options.label() + '</span>'
96
+ const labelSpan = document.createElement('span')
97
+ labelSpan.className = options.css['entryLabel'] ?? ''
98
+ const iconSpan = document.createElement('span')
99
+ iconSpan.className = options.css['entryIcon'] ?? ''
100
+ iconSpan.innerHTML = options.icon
101
+ entry.append(iconSpan, labelSpan)
102
+ const applyLabel = (): void => {
103
+ entry.setAttribute('aria-label', options.label())
104
+ if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
105
+ labelSpan.textContent = options.label()
106
+ }
107
+ applyLabel()
92
108
  entry.addEventListener('click', options.onToggle)
93
- return entry
109
+ return { entry, applyLabel }
94
110
  }
95
111
 
96
112
  /** Re-insert the entry after the New Session row (before the browser region). */
@@ -132,9 +148,18 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
132
148
  if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
133
149
  return () => {}
134
150
  }
135
- const entry = createEntry(options)
151
+ const { entry, applyLabel } = createEntry(options)
136
152
  let root: HTMLElement | undefined
137
153
  let placed = false
154
+ let unsubscribeRefresh: (() => void) | undefined
155
+ if (options.refresh !== undefined) {
156
+ try {
157
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel)
158
+ } catch {
159
+ // A throwing subscription must not break the mount; the label stays at
160
+ // its initial value and the next reload resolves it again.
161
+ }
162
+ }
138
163
 
139
164
  const tryPlace = (): void => {
140
165
  if (root !== undefined && !root.isConnected) {
@@ -202,6 +227,7 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
202
227
  return () => {
203
228
  waitObserver.disconnect()
204
229
  rootObserver.disconnect()
230
+ unsubscribeRefresh?.()
205
231
  unsubscribeActive?.()
206
232
  entry.remove()
207
233
  }
@@ -19,13 +19,18 @@ export const ENTRY_SELECTOR = '[data-dsh-ssh-entry]'
19
19
  /** Inline terminal glyph sized to the shell's current sidebar navigation icons. */
20
20
  const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" 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>'
21
21
 
22
+ /** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
23
+ export interface LocaleRefreshSource { subscribe(listener: () => void): () => void }
24
+
22
25
  /**
23
26
  * Mount the sidebar entry, waiting for the shell to render and self-healing
24
27
  * on later React re-renders.
25
28
  * @param controller - the panel controller the entry toggles.
29
+ * @param locale - locale-change source; when given, re-applies the label on
30
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
26
31
  * @returns disposer removing the entry and its observers.
27
32
  */
28
- export function mountSidebarEntry(controller: PanelController): () => void {
33
+ export function mountSidebarEntry(controller: PanelController, locale?: LocaleRefreshSource): () => void {
29
34
  return mountSharedSidebarEntry({
30
35
  rowAttribute: 'data-dsh-ssh-entry',
31
36
  rowSelector: ENTRY_SELECTOR,
@@ -34,6 +39,7 @@ export function mountSidebarEntry(controller: PanelController): () => void {
34
39
  css,
35
40
  label: () => tt('entry.label'),
36
41
  tooltip: () => tt('entry.tooltip'),
42
+ refresh: locale === undefined ? undefined : { subscribe: (listener) => locale.subscribe(listener) },
37
43
  onToggle: () => { controller.toggle() },
38
44
  position: 'after',
39
45
  familySelectors: ['[data-dsh-taskboard-entry]', '[data-dsh-ssh-entry]'],