@linxin666/dsh-ssh 0.3.6 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +361 -310
- package/lib/client.js.map +1 -1
- package/lib/index.js +19 -14
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/mount.d.ts +4 -1
- package/lib/types/client/panel/helpers.d.ts +2 -0
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/index.d.ts +2 -1
- package/package.json +17 -17
- package/src/client/api.ts +5 -1
- package/src/client/index.ts +12 -3
- package/src/client/locales.ts +2 -0
- package/src/client/mount.tsx +11 -1
- package/src/client/panel/helpers.ts +14 -0
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/dsh-home.ts +1 -1
- package/src/index.ts +27 -14
package/lib/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
1
|
import { closeSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { Client } from "ssh2";
|
|
4
3
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -3560,7 +3559,7 @@ const inject = [
|
|
|
3560
3559
|
* surface edits. Spelled here rather than imported: the browser half spells
|
|
3561
3560
|
* the same value and must not depend on a Host package.
|
|
3562
3561
|
*/
|
|
3563
|
-
const SSH_SETTINGS_NAMESPACE =
|
|
3562
|
+
const SSH_SETTINGS_NAMESPACE = "dsh-ssh";
|
|
3564
3563
|
const Config = import_lib.default.object({
|
|
3565
3564
|
announceToAgent: import_lib.default.boolean().default(false),
|
|
3566
3565
|
enabled: import_lib.default.boolean().default(true),
|
|
@@ -3580,18 +3579,21 @@ const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维)
|
|
|
3580
3579
|
const apply = mountOnce("@linxin666/dsh-ssh", applyImpl);
|
|
3581
3580
|
function applyImpl(ctx, config) {
|
|
3582
3581
|
let current = () => config ?? {};
|
|
3582
|
+
let isSettingsBound = false;
|
|
3583
|
+
const store = new HostStore();
|
|
3584
|
+
const engine = new SshEngine(store);
|
|
3585
|
+
ctx.effect(() => () => {
|
|
3586
|
+
engine.dispose();
|
|
3587
|
+
}, "dsh-ssh: engine");
|
|
3583
3588
|
const resolve = () => {
|
|
3584
3589
|
const value = current();
|
|
3590
|
+
let enabled = value.enabled ?? true;
|
|
3591
|
+
if (enabled === false && !isSettingsBound && store.list().length > 0) enabled = true;
|
|
3585
3592
|
return {
|
|
3586
3593
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
3587
|
-
enabled
|
|
3594
|
+
enabled
|
|
3588
3595
|
};
|
|
3589
3596
|
};
|
|
3590
|
-
const store = new HostStore();
|
|
3591
|
-
const engine = new SshEngine(store);
|
|
3592
|
-
ctx.effect(() => () => {
|
|
3593
|
-
engine.dispose();
|
|
3594
|
-
}, "dsh-ssh: engine");
|
|
3595
3597
|
const { routes, upgrade } = makeRoutes({
|
|
3596
3598
|
store,
|
|
3597
3599
|
engine
|
|
@@ -3642,12 +3644,15 @@ function applyImpl(ctx, config) {
|
|
|
3642
3644
|
};
|
|
3643
3645
|
}, "dsh-ssh: tools");
|
|
3644
3646
|
};
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3647
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
3648
|
+
settingsCtx.settings.installSection(ctx, SSH_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
3649
|
+
setSource: (source) => {
|
|
3650
|
+
isSettingsBound = true;
|
|
3651
|
+
current = source;
|
|
3652
|
+
sync();
|
|
3653
|
+
},
|
|
3654
|
+
onChange: sync
|
|
3655
|
+
});
|
|
3651
3656
|
});
|
|
3652
3657
|
sync();
|
|
3653
3658
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
14
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
14
15
|
import { type SshKey } from './locales.ts';
|
|
15
16
|
declare module '@deepseek-ai/cordis' {
|
|
16
17
|
interface Context {
|
|
@@ -146,6 +146,7 @@ export declare const zh: {
|
|
|
146
146
|
readonly 'common.error': "出错:{error}";
|
|
147
147
|
readonly 'common.refresh': "刷新";
|
|
148
148
|
readonly 'common.confirm': "确定";
|
|
149
|
+
readonly 'error.disabled': "SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。";
|
|
149
150
|
};
|
|
150
151
|
export declare const en: Record<keyof typeof zh, string>;
|
|
151
152
|
/** Locale key union. */
|
|
@@ -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/lib/types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* no dsh source changes.
|
|
9
9
|
*/
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
11
12
|
import z from 'schemastery';
|
|
12
13
|
/** Stable cordis plugin name. */
|
|
13
14
|
export declare const name = "ssh";
|
|
@@ -18,7 +19,7 @@ export declare const inject: string[];
|
|
|
18
19
|
* surface edits. Spelled here rather than imported: the browser half spells
|
|
19
20
|
* the same value and must not depend on a Host package.
|
|
20
21
|
*/
|
|
21
|
-
export declare const SSH_SETTINGS_NAMESPACE:
|
|
22
|
+
export declare const SSH_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
22
23
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
23
24
|
export interface Config {
|
|
24
25
|
/**
|
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.
|
|
4
|
+
"version": "0.3.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dsh": {
|
|
28
28
|
"engines": {
|
|
29
|
-
"dsh": ">=0.1.
|
|
29
|
+
"dsh": ">=0.1.2-alpha.1"
|
|
30
30
|
},
|
|
31
31
|
"bundle": {
|
|
32
32
|
"patch": "./cordis.patch.yml"
|
|
33
33
|
},
|
|
34
34
|
"client": {
|
|
35
35
|
"inject": [
|
|
36
|
-
"@deepseek-ai/dsh-client-
|
|
37
|
-
"@deepseek-ai/dsh-client-
|
|
36
|
+
"@deepseek-ai/dsh-client-locale",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
38
38
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
39
39
|
],
|
|
40
40
|
"platform": "web"
|
|
@@ -51,19 +51,19 @@
|
|
|
51
51
|
"react-dom": "^18.2.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
55
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
62
|
-
"@deepseek-ai/dsh-scope": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
64
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
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",
|
|
67
67
|
"@types/node": "^22.20.0",
|
|
68
68
|
"@types/react": "~18.3.1",
|
|
69
69
|
"@types/react-dom": "^18.3.5",
|
package/src/client/api.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type TransferStreamLine,
|
|
19
19
|
type TunnelInfo,
|
|
20
20
|
} from '../protocol.ts'
|
|
21
|
+
import { tt } from './panel/helpers.ts'
|
|
21
22
|
|
|
22
23
|
/** Minimal File System Access API surface (not in all lib.dom versions). */
|
|
23
24
|
interface WindowWithFileSystemAccess {
|
|
@@ -51,12 +52,15 @@ async function readJson<T>(response: Response): Promise<T> {
|
|
|
51
52
|
try {
|
|
52
53
|
body = await response.json()
|
|
53
54
|
} catch {
|
|
55
|
+
if (response.status === 404) {
|
|
56
|
+
throw new SshApiError(tt('error.disabled'))
|
|
57
|
+
}
|
|
54
58
|
throw new SshApiError(`HTTP ${response.status}: invalid JSON response`)
|
|
55
59
|
}
|
|
56
60
|
if (!response.ok) {
|
|
57
61
|
const message = typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
|
|
58
62
|
? (body as { error: string }).error
|
|
59
|
-
: `HTTP ${response.status}`
|
|
63
|
+
: (response.status === 404 ? tt('error.disabled') : `HTTP ${response.status}`)
|
|
60
64
|
throw new SshApiError(message)
|
|
61
65
|
}
|
|
62
66
|
return body as T
|
package/src/client/index.ts
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
14
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
14
15
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
15
16
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
16
17
|
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
17
18
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
19
|
+
// Type-only: pulls the ctx.slots merge (the renderer owns the slot registry since 0.1.2).
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
18
21
|
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
19
22
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
20
23
|
import { SshApi } from './api.ts'
|
|
@@ -22,6 +25,7 @@ import { en, zh, type SshKey } from './locales.ts'
|
|
|
22
25
|
import { mountPanel } from './mount.tsx'
|
|
23
26
|
import { PanelController } from './panel/controller.ts'
|
|
24
27
|
import type { TerminalFontSource } from './panel/helpers.ts'
|
|
28
|
+
import { setRuntimeTranslate } from './panel/helpers.ts'
|
|
25
29
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
26
30
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
27
31
|
|
|
@@ -86,6 +90,11 @@ export function apply(ctx: ClientContext): void {
|
|
|
86
90
|
}
|
|
87
91
|
}, 'dsh-ssh: dictionaries')
|
|
88
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
|
+
|
|
89
98
|
const controller = new PanelController()
|
|
90
99
|
const api = new SshApi()
|
|
91
100
|
// Live terminal-font preference (issue #577): the settings namespace is
|
|
@@ -102,8 +111,8 @@ export function apply(ctx: ClientContext): void {
|
|
|
102
111
|
}
|
|
103
112
|
const disposers: Array<() => void> = []
|
|
104
113
|
try {
|
|
105
|
-
disposers.push(mountSidebarEntry(controller))
|
|
106
|
-
disposers.push(mountPanel(controller, api, terminalFont))
|
|
114
|
+
disposers.push(mountSidebarEntry(controller, ctx.locale))
|
|
115
|
+
disposers.push(mountPanel(controller, api, terminalFont, ctx.locale))
|
|
107
116
|
} catch (error) {
|
|
108
117
|
// DOM failures degrade the panel, never the GUI.
|
|
109
118
|
console.warn('[dsh-ssh] mount failed:', error)
|
package/src/client/locales.ts
CHANGED
|
@@ -154,6 +154,7 @@ export const zh = {
|
|
|
154
154
|
'common.error': '出错:{error}',
|
|
155
155
|
'common.refresh': '刷新',
|
|
156
156
|
'common.confirm': '确定',
|
|
157
|
+
'error.disabled': 'SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。',
|
|
157
158
|
} as const
|
|
158
159
|
|
|
159
160
|
export const en: Record<keyof typeof zh, string> = {
|
|
@@ -301,6 +302,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
301
302
|
'common.error': 'Error: {error}',
|
|
302
303
|
'common.refresh': 'Refresh',
|
|
303
304
|
'common.confirm': 'Confirm',
|
|
305
|
+
'error.disabled': 'The SSH plugin is disabled on the host. Please enable it in Settings → Web Plugins → SSH.',
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
/** Locale key union. */
|
package/src/client/mount.tsx
CHANGED
|
@@ -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
|
|
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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]'],
|
package/src/dsh-home.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
-
import {
|
|
12
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
13
|
import z from 'schemastery'
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
@@ -31,7 +31,7 @@ export const inject = ['webServer', 'tools', 'systemPrompt']
|
|
|
31
31
|
* surface edits. Spelled here rather than imported: the browser half spells
|
|
32
32
|
* the same value and must not depend on a Host package.
|
|
33
33
|
*/
|
|
34
|
-
export const SSH_SETTINGS_NAMESPACE =
|
|
34
|
+
export const SSH_SETTINGS_NAMESPACE = 'dsh-ssh' as SettingsNamespace
|
|
35
35
|
|
|
36
36
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
37
37
|
export interface Config {
|
|
@@ -77,18 +77,28 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
77
77
|
// The live source the surfaces read: the settings section once the web
|
|
78
78
|
// settings surface is served, the composition entry otherwise.
|
|
79
79
|
let current: () => Config = () => config ?? {}
|
|
80
|
+
let isSettingsBound = false
|
|
81
|
+
|
|
82
|
+
const store = new HostStore()
|
|
83
|
+
const engine = new SshEngine(store)
|
|
84
|
+
ctx.effect(() => () => { engine.dispose() }, 'dsh-ssh: engine')
|
|
85
|
+
|
|
80
86
|
const resolve = (): Config => {
|
|
81
87
|
const value = current()
|
|
88
|
+
let enabled = value.enabled ?? true
|
|
89
|
+
// When dsh-ssh was seeded with enabled: false (e.g. from an aggregate profile line),
|
|
90
|
+
// but the user has never explicitly configured settings (not bound yet)
|
|
91
|
+
// AND already has active host records in dsh-ssh.json, keep the plugin enabled so
|
|
92
|
+
// existing users are not broken upon upgrade (#1250).
|
|
93
|
+
if (enabled === false && !isSettingsBound && store.list().length > 0) {
|
|
94
|
+
enabled = true
|
|
95
|
+
}
|
|
82
96
|
return {
|
|
83
97
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
84
|
-
enabled
|
|
98
|
+
enabled,
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
const store = new HostStore()
|
|
89
|
-
const engine = new SshEngine(store)
|
|
90
|
-
ctx.effect(() => () => { engine.dispose() }, 'dsh-ssh: engine')
|
|
91
|
-
|
|
92
102
|
// The /api/dsh-ssh route family + terminal upgrade.
|
|
93
103
|
const { routes, upgrade } = makeRoutes({ store, engine })
|
|
94
104
|
let disposeRoutes: (() => void) | undefined
|
|
@@ -152,15 +162,18 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
152
162
|
)
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
166
|
+
settingsCtx.settings.installSection(ctx, SSH_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
167
|
+
setSource: (source) => {
|
|
168
|
+
isSettingsBound = true
|
|
169
|
+
current = source
|
|
170
|
+
sync()
|
|
171
|
+
},
|
|
172
|
+
onChange: sync,
|
|
173
|
+
})
|
|
161
174
|
})
|
|
162
175
|
|
|
163
176
|
// Initial registration from the composition entry (covers deployments with
|
|
164
|
-
// no settings service, whose
|
|
177
|
+
// no settings service, whose installSection never fires its hooks).
|
|
165
178
|
sync()
|
|
166
179
|
}
|