@linxin666/dsh-ssh 0.3.24 → 0.4.1

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.
@@ -11,20 +11,7 @@
11
11
  * cordis loading needs plus types only — all value exports stay internal.
12
12
  */
13
13
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
14
- import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
15
14
  import { type SshKey } from './locales.ts';
16
- declare module '@deepseek-ai/cordis' {
17
- interface Context {
18
- /**
19
- * Optional rc.6 compatibility binder provided by dsh-web-settings;
20
- * absent when that group plugin is not installed, so callers fall back to
21
- * the official settings scope.
22
- */
23
- webUiSettings?: {
24
- bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S>;
25
- };
26
- }
27
- }
28
15
  declare module '@deepseek-ai/dsh-client-ui-slots' {
29
16
  interface LocaleNamespaceMap {
30
17
  /** dsh-ssh surface copy. */
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Settings reader of the dsh-ssh browser half.
3
+ *
4
+ * The plugin's browser half owns one preference: the xterm `fontFamily` the
5
+ * panel applies to its terminals. Under the 0.1.7 settings model that field
6
+ * belongs to the plugin's own profile entry (its Cordis Config schema), and
7
+ * the browser surface addresses settings as one form per profile entry id — so
8
+ * the entry has to be resolved before its section can be read:
9
+ *
10
+ * - the family binder (`ctx.get('webUiSettings')`, published while
11
+ * dsh-web-settings is loaded) resolves the dsh-ssh namespace to its entry id
12
+ * through the Host bridge and hands back the native shared form;
13
+ * - without that group the shared configuration forms serve the form directly,
14
+ * and the entry is recognised by the one field this plugin's own schema
15
+ * declares: the entry whose resolved section carries `terminalFontFamily`.
16
+ */
17
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
18
+ import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
19
+ /** Domain-owned description of one settings namespace a family surface binds. */
20
+ export interface ConfigFormSpec<T> {
21
+ /** Settings namespace the surface edits. */
22
+ namespace: string;
23
+ /**
24
+ * Narrow one wire section; undefined keeps the section the Host resolved.
25
+ * The shared form already validates against the namespace's serialized
26
+ * schema, so a decoder exists only to narrow beyond it.
27
+ */
28
+ decode?: (section: unknown) => T | undefined;
29
+ }
30
+ declare module '@deepseek-ai/cordis' {
31
+ interface Context {
32
+ /**
33
+ * Optional family settings binder provided by dsh-web-settings; absent when
34
+ * that group plugin is not installed, so callers fall back to the shared
35
+ * configuration forms service (`ctx.configForms`).
36
+ */
37
+ webUiSettings?: {
38
+ bind<S>(spec: ConfigFormSpec<S>): ConfigForm<S>;
39
+ };
40
+ }
41
+ }
42
+ /** The read face this plugin needs from its own settings entry. */
43
+ export interface SettingsReader<T> {
44
+ /** @returns the current snapshot (stable reference until the next change). */
45
+ getSnapshot(): ConfigFormSnapshot<T>;
46
+ /** Observe snapshot replacements. */
47
+ subscribe(listener: () => void): () => void;
48
+ }
49
+ /** A reader plus the release of every subscription it installed. */
50
+ export interface SettingsBinding<T> extends SettingsReader<T> {
51
+ /** Release the reader's subscriptions; later calls are no-ops. */
52
+ dispose(): void;
53
+ }
54
+ /**
55
+ * Bind this plugin's settings reader.
56
+ * @param ctx - client context carrying the family binder and/or the shared forms service.
57
+ * @param namespace - the family settings namespace this plugin owns.
58
+ * @param field - the field this plugin's own schema declares, used to recognise its entry when the family binder is absent.
59
+ * @returns the reader and the release of its subscriptions.
60
+ */
61
+ export declare function bindSettingsReader<T>(ctx: ClientContext, namespace: string, field: string): SettingsBinding<T>;
@@ -1,29 +1,3 @@
1
- /**
2
- * One trailing action button seated at the entry row's right edge (e.g. a
3
- * refresh command or a collapse chevron). Rows with actions switch from the
4
- * classic single-button structure to a container with a main button plus the
5
- * action buttons (nested interactive elements are invalid HTML); rows without
6
- * actions keep the classic structure untouched.
7
- */
8
- export interface SidebarEntryAction {
9
- /** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
10
- id: string;
11
- /** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
12
- icon: string;
13
- /**
14
- * Optional icon while the row is inactive. Setting it makes the action a
15
- * state mirror (a collapse chevron): the core swaps the icon and stamps
16
- * aria-expanded on every active-state change. Requires the active bridge.
17
- */
18
- inactiveIcon?: string;
19
- /** Localized accessible label (aria-label + title); receives the active state. */
20
- label(active: boolean): string;
21
- /**
22
- * Click handler; receives the button so the caller can drive transient
23
- * affordances (a refresh cooldown) without a re-render.
24
- */
25
- onClick(button: HTMLButtonElement): void;
26
- }
27
1
  /** Per-package configuration for one sidebar entry row. */
28
2
  export interface SidebarEntryOptions {
29
3
  /** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
@@ -57,12 +31,6 @@ export interface SidebarEntryOptions {
57
31
  };
58
32
  /** Click action (open/toggle the owning panel). */
59
33
  onToggle(): void;
60
- /**
61
- * Optional trailing action buttons at the row's right edge. Requires the
62
- * package CSS to seat entryMain / entryAction; empty keeps the classic
63
- * single-button row.
64
- */
65
- actions?: readonly SidebarEntryAction[];
66
34
  /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
67
35
  position: 'before' | 'after';
68
36
  /**
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Shared JSON body/response helpers for the host route families: one strict
3
3
  * bounded body reader, one lenient bounded body reader, one JSON object
4
- * narrow, and one JSON writer. Previously these were copy-pasted across the
4
+ * narrow, one JSON writer, and the outbound request-init helper that keeps a
5
+ * host fetch body decodable (withIdentityEncoding). Previously the readers
6
+ * were copy-pasted across the
5
7
  * package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
6
8
  * family's route module) with drifting contracts: body caps ranging 4 KiB to
7
9
  * 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
@@ -34,6 +36,21 @@ export declare function readJsonBody(req: IncomingMessage, opts?: {
34
36
  }): Promise<unknown | null>;
35
37
  /** Narrow a value to a JSON object, or undefined when it is not one. */
36
38
  export declare function asJsonObject(value: unknown): Record<string, unknown> | undefined;
39
+ /**
40
+ * Request init that asks a remote origin for an uncompressed body.
41
+ *
42
+ * The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
43
+ * the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
44
+ * Node's built-in `fetch()` reads. That cross-major wrapper drops the
45
+ * `content-encoding` header and automatic decompression, so a plain fetch
46
+ * resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
47
+ * consumer can read. Every host fetch that parses a remote body must request
48
+ * identity encoding. Caller headers survive; `accept-encoding` is forced to
49
+ * `identity` because a caller value has no decoder behind it in this host.
50
+ * @param init - the caller's request init (signal, headers, method, ...).
51
+ * @returns a copy with `accept-encoding: identity` merged in.
52
+ */
53
+ export declare function withIdentityEncoding(init?: RequestInit): RequestInit;
37
54
  /**
38
55
  * Write one JSON response. Default headers are the family defaults
39
56
  * (content-type and referrer-policy); caller headers are appended or
@@ -8,23 +8,27 @@
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';
12
- import z from 'schemastery';
11
+ import z from '@deepseek-ai/schemastery';
13
12
  /** Stable cordis plugin name. */
14
13
  export declare const name = "ssh";
15
14
  /** Services required before the SSH surfaces can mount. */
16
15
  export declare const inject: string[];
17
- /**
18
- * Settings namespace of the SSH capability — the section the web settings
19
- * surface edits. Spelled here rather than imported: the browser half spells
20
- * the same value and must not depend on a Host package.
21
- */
22
- export declare const SSH_SETTINGS_NAMESPACE: SettingsNamespace;
23
- /** Plugin config, validated by the same-named schemastery schema. */
16
+ declare module '@deepseek-ai/cordis' {
17
+ interface Events {
18
+ /**
19
+ * Volatile config values were committed into the running instance without a
20
+ * remount (cordis-plugin-loader); dispatched to the owning fiber only.
21
+ * @param paths - the changed config paths, as key arrays.
22
+ * @mode emit
23
+ */
24
+ 'loader/volatile-update'(paths: readonly (readonly string[])[]): void;
25
+ }
26
+ }
27
+ /** Plugin config as a profile patch declares it, before schema resolution. */
24
28
  export interface Config {
25
29
  /**
26
- * When true (default), a system-prompt section announces the SSH plugin to
27
- * every agent (tools + host store). Set false to keep it silent.
30
+ * When true, a system-prompt section announces the SSH plugin to every agent
31
+ * (tools + host store). Off by default to keep prompts clean.
28
32
  */
29
33
  announceToAgent?: boolean;
30
34
  /** Master switch for the plugin (routes, tools, prompt section). */
@@ -37,7 +41,36 @@ export interface Config {
37
41
  */
38
42
  terminalFontFamily?: string;
39
43
  }
40
- export declare const Config: z<Config>;
44
+ /** The stable reference a `volatile()` config field resolves to; its owner updates it in place. */
45
+ interface ConfigRef<T> {
46
+ /** @returns the field's current value. */
47
+ get(): T;
48
+ }
49
+ /** One resolved config field: a live reference, or a plain value from a hand-built context. */
50
+ type ConfigField<T> = ConfigRef<T> | T;
51
+ /** The config the Host hands to {@link apply} — the runtime face of {@link Config}. */
52
+ export interface ResolvedConfig {
53
+ announceToAgent?: ConfigField<boolean>;
54
+ enabled?: ConfigField<boolean>;
55
+ terminalFontFamily?: ConfigField<string>;
56
+ }
57
+ /**
58
+ * Plugin config schema. Under the 0.1.7 settings model this schema IS the
59
+ * entry's settings page: the Host derives one form per profile entry from it
60
+ * and serves it through the shared configuration forms. Every field is
61
+ * `volatile()`, which is what puts it on that page and what lets an edit reach
62
+ * a running instance without a remount: the loader commits the new value into
63
+ * the field's reference and announces `loader/volatile-update` on this fiber.
64
+ */
65
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
66
+ announceToAgent: z<boolean, boolean, "volatile-defined">;
67
+ enabled: z<boolean, boolean, "volatile-defined">;
68
+ terminalFontFamily: z<string, string, "volatile-defined">;
69
+ }>>, Schemastery.ObjectT<NoInfer<{
70
+ announceToAgent: z<boolean, boolean, "volatile-defined">;
71
+ enabled: z<boolean, boolean, "volatile-defined">;
72
+ terminalFontFamily: z<string, string, "volatile-defined">;
73
+ }>>, "plain">;
41
74
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
42
75
  export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 $DSH_HOME/dsh-ssh.json\uFF08\u9ED8\u8BA4 ~/.dsh\uFF09\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u6309 aliases/environment/tags \u81F3\u5C11\u4E00\u79CD\u975E\u7A7A selector \u7B5B\u9009\u540E\u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u3001ProxyJump \u8DF3\u677F\u673A\uFF08\u522B\u540D\u6216 [user@]host[:port] \u5730\u5740\uFF09\u4E0E OpenSSH \u8BED\u4E49\u7684 ProxyCommand\uFF08\u8DF3\u8FC7\u5821\u5792\u673A\u5BA2\u6237\u7AEF\u573A\u666F\uFF0C\u53EA\u80FD\u7531\u7528\u6237\u5728 GUI \u914D\u7F6E\uFF0Cagent \u4E0D\u53EF\u5199\uFF09\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u8DEF\u5F84\u533A\u5206\uFF1A\u672C\u673A\uFF08dsh host\uFF09\u4E0A\u7684\u6587\u4EF6\u4E0E\u547D\u4EE4\u4E00\u5F8B\u7528\u672C\u5730\u5DE5\u5177\uFF08read / write / edit / bash\uFF09\uFF0Cssh_* \u5DE5\u5177\u53EA\u9488\u5BF9\u8FDC\u7A0B\u4E3B\u673A\u4E0A\u7684\u8DEF\u5F84\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
43
76
  /**
@@ -46,5 +79,5 @@ export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63
46
79
  * @param config - resolved plugin config (schema defaults applied by the loader).
47
80
  */
48
81
  export declare const apply: typeof applyImpl;
49
- declare function applyImpl(ctx: Context, config?: Config): void;
82
+ declare function applyImpl(ctx: Context, config?: ResolvedConfig): void;
50
83
  export {};
package/package.json CHANGED
@@ -1,7 +1,8 @@
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.24",
4
+ "version": "0.4.1",
5
+ "icon": "icon.svg",
5
6
  "type": "module",
6
7
  "engines": {
7
8
  "node": "^22.19.0 || >=24.0.0"
@@ -26,7 +27,7 @@
26
27
  },
27
28
  "dsh": {
28
29
  "engines": {
29
- "dsh": ">=0.1.5-rc.1"
30
+ "dsh": ">=0.1.7-rc.1"
30
31
  },
31
32
  "bundle": {
32
33
  "patch": "./cordis.patch.yml"
@@ -41,42 +42,43 @@
41
42
  }
42
43
  },
43
44
  "dependencies": {
45
+ "@deepseek-ai/schemastery": "^3.18.4",
44
46
  "@xterm/addon-fit": "^0.11.0",
45
47
  "@xterm/xterm": "^6.0.0",
46
48
  "ssh2": "^1.17.0",
47
49
  "ws": "^8.18.0"
48
50
  },
49
51
  "peerDependencies": {
52
+ "@deepseek-ai/dsh": ">=0.1.7-rc.1",
50
53
  "react": "^18.2.0",
51
54
  "react-dom": "^18.2.0"
52
55
  },
53
56
  "devDependencies": {
54
- "@deepseek-ai/cordis": "^4.0.2",
55
- "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.1",
56
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
57
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
58
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
60
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
61
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
62
- "@deepseek-ai/dsh-scope": "^0.1.5-rc.1",
63
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
64
- "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.1",
65
- "@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
66
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
57
+ "@deepseek-ai/cordis": "^4.0.4",
58
+ "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
59
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
60
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
61
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
62
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
63
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
64
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
65
+ "@deepseek-ai/dsh-scope": "^0.1.7-rc.1",
66
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
67
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1",
68
+ "@deepseek-ai/dsh-timeout": "^0.1.7-rc.1",
69
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
67
70
  "@types/node": "^22.20.0",
68
71
  "@types/react": "~18.3.1",
69
72
  "@types/react-dom": "^18.3.5",
70
73
  "@types/ssh2": "^1.15.5",
71
74
  "@types/ws": "^8.5.13",
72
- "@vitest/coverage-v8": "^3.2.7",
75
+ "@vitest/coverage-v8": "^4.1.11",
73
76
  "jsdom": "^25.0.0",
74
77
  "react": "^18.3.1",
75
78
  "react-dom": "^18.3.1",
76
- "schemastery": "^3.18.0",
77
79
  "tsdown": "0.22.2",
78
80
  "typescript": "~5.7.2",
79
- "vitest": "^3.0.0"
81
+ "vitest": "^4.1.11"
80
82
  },
81
83
  "files": [
82
84
  "lib/**/*.js",
@@ -85,7 +87,8 @@
85
87
  "lib/**/*.d.ts.map",
86
88
  "src",
87
89
  "cordis.patch.yml",
88
- "README.md"
90
+ "README.md",
91
+ "icon.svg"
89
92
  ],
90
93
  "license": "Apache-2.0",
91
94
  "repository": {
@@ -11,10 +11,9 @@
11
11
  * cordis loading needs plus types only — all value exports stay internal.
12
12
  */
13
13
  import type { Context as ClientContext } from '@deepseek-ai/cordis'
14
- import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client'
15
14
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
16
15
  import type {} from '@deepseek-ai/dsh-client-locale/client'
17
- // Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
16
+ // Type-only: pulls the shared-forms Context merge (ctx.configForms).
18
17
  import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
19
18
  // Type-only: pulls the ctx.slots merge (the renderer owns the slot registry since 0.1.2).
20
19
  import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
@@ -26,31 +25,27 @@ import { mountPanel } from './mount.tsx'
26
25
  import { PanelController } from './panel/controller.ts'
27
26
  import type { TerminalFontSource } from './panel/helpers.ts'
28
27
  import { setRuntimeTranslate } from './panel/helpers.ts'
28
+ import { bindSettingsReader } from './settings-binding.ts'
29
29
  import { mountSidebarEntry } from './sidebar-entry.ts'
30
30
  import { reportDailyHeartbeat } from './telemetry.ts'
31
31
 
32
32
  /** Locale namespace this plugin owns. */
33
33
  const NS = 'dsh-ssh'
34
34
 
35
- /** Settings namespace the terminal-font preference lives in (issue #577). */
35
+ /**
36
+ * Family settings namespace the terminal-font preference is addressed by
37
+ * (issue #577); dsh-web-settings maps it to the owning profile entry id.
38
+ */
36
39
  const SETTINGS_NS = 'dsh-ssh'
37
40
 
38
- /** The dsh-ssh settings surface the browser half reads. */
41
+ /** The dsh-ssh settings section the browser half reads. */
39
42
  interface SshClientSettings {
40
43
  /** User-configured xterm fontFamily; empty/undefined means the CSS chain. */
41
44
  terminalFontFamily?: string
42
45
  }
43
46
 
44
- declare module '@deepseek-ai/cordis' {
45
- interface Context {
46
- /**
47
- * Optional rc.6 compatibility binder provided by dsh-web-settings;
48
- * absent when that group plugin is not installed, so callers fall back to
49
- * the official settings scope.
50
- */
51
- webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }
52
- }
53
- }
47
+ /** The one field this plugin's own Config schema declares (its entry identity on the shared settings surface). */
48
+ const TERMINAL_FONT_FIELD = 'terminalFontFamily' satisfies keyof SshClientSettings
54
49
 
55
50
  declare module '@deepseek-ai/dsh-client-ui-slots' {
56
51
  interface LocaleNamespaceMap {
@@ -60,7 +55,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
60
55
  }
61
56
 
62
57
  /** Required services (fiber inject waiting — the runtime must be up first). */
63
- export const inject = ['slots', 'locale', 'settingsScope']
58
+ export const inject = ['slots', 'locale', 'configForms']
64
59
 
65
60
  /** Type-only surface (export discipline: no value exports beyond the plugin contract). */
66
61
  export type { PanelControllerSnapshot } from './panel/controller.ts'
@@ -97,18 +92,19 @@ export function apply(ctx: ClientContext): void {
97
92
 
98
93
  const controller = new PanelController()
99
94
  const api = new SshApi()
100
- // Live terminal-font preference (issue #577): the settings namespace is
101
- // edited by the host-registered GUI section; the panel re-applies changes
102
- // to open terminals without a reconnect.
103
- const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
104
- const scope = binder.bind<SshClientSettings>({ namespace: SETTINGS_NS })
95
+ // Live terminal-font preference (issue #577): the field lives in this
96
+ // plugin's own profile entry, whose settings page the Host generates from
97
+ // the plugin Config schema; the panel re-applies a change to open terminals
98
+ // without a reconnect.
99
+ const settings = bindSettingsReader<SshClientSettings>(ctx, SETTINGS_NS, TERMINAL_FONT_FIELD)
105
100
  const terminalFont: TerminalFontSource = {
106
101
  get: () => {
107
- const snapshot = scope.getSnapshot()
102
+ const snapshot = settings.getSnapshot()
108
103
  return snapshot.status === 'ready' ? snapshot.value?.terminalFontFamily : undefined
109
104
  },
110
- subscribe: (listener) => scope.subscribe(listener),
105
+ subscribe: (listener) => settings.subscribe(listener),
111
106
  }
107
+ ctx.effect(() => () => { settings.dispose() }, 'dsh-ssh: settings binding')
112
108
  const disposers: Array<() => void> = []
113
109
  try {
114
110
  disposers.push(mountSidebarEntry(controller, ctx.locale))
@@ -48,20 +48,27 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
48
48
 
49
49
  /* --- sidebar entry row ------------------------------------------------------- */
50
50
 
51
+ /* Same geometry as the shell's own panel rows (SidebarRoot .panelRow): 2px
52
+ inset with a 12px radius, 8px content padding, 36px row, 14/22 type, and the
53
+ primary ink — so an injected row and an official row line up column for
54
+ column instead of each carrying its own box. */
51
55
  .entry {
52
56
  box-sizing: border-box;
53
57
  display: flex;
54
58
  align-items: center;
55
59
  gap: 8px;
56
- width: 100%;
57
60
  min-height: 36px;
58
- padding: 0 10px;
61
+ padding: 7px 8px;
62
+ margin: 0 2px;
59
63
  background: transparent;
60
64
  border: none;
61
- border-radius: 8px;
62
- color: var(--dsw-alias-label-secondary);
65
+ border-radius: 12px;
66
+ color: var(--dsw-alias-label-primary);
63
67
  cursor: pointer;
64
- font-size: 13px;
68
+ font: inherit;
69
+ font-size: 14px;
70
+ line-height: 22px;
71
+ text-align: left;
65
72
  white-space: nowrap;
66
73
  }
67
74
 
@@ -76,19 +83,21 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
76
83
  font-weight: 600;
77
84
  }
78
85
 
86
+ /* Icon box matches the shell's panel glyph exactly (16px), so the glyph column
87
+ and the label column land on the official rows' x positions. */
79
88
  .entryIcon {
80
89
  display: inline-flex;
81
90
  align-items: center;
82
91
  justify-content: center;
83
- width: 24px;
84
- height: 24px;
92
+ width: 16px;
93
+ height: 16px;
85
94
  flex: none;
86
95
  }
87
96
 
88
97
  .entryIcon svg {
89
98
  display: block;
90
- width: 18px;
91
- height: 18px;
99
+ width: 16px;
100
+ height: 16px;
92
101
  }
93
102
 
94
103
  .entryLabel {
@@ -96,7 +105,9 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
96
105
  text-overflow: ellipsis;
97
106
  }
98
107
 
99
- /* Collapsed rail: icon-only, centered, matching the shell's 56px rail. */
108
+ /* Collapsed rail: icon-only, centered, matching the shell's 56px rail. The
109
+ shell asks its own panel rows for an 18px glyph on the rail, so the icon box
110
+ and the svg step up together with the 12px rail radius. */
100
111
  :global([data-dsh-frame][data-sidebar-collapsed]) .entry,
101
112
  :global([data-sidebar-collapsed]) .entry {
102
113
  justify-content: center;
@@ -104,7 +115,15 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
104
115
  width: 36px;
105
116
  min-height: 36px;
106
117
  margin: 0 auto 12px;
107
- border-radius: 50%;
118
+ border-radius: 12px;
119
+ }
120
+
121
+ :global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon,
122
+ :global([data-sidebar-collapsed]) .entryIcon,
123
+ :global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon svg,
124
+ :global([data-sidebar-collapsed]) .entryIcon svg {
125
+ width: 18px;
126
+ height: 18px;
108
127
  }
109
128
 
110
129
  :global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,