dsh-over-ssh 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +119 -2
- package/agents/agent-manifest.json +20 -0
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/cordis.patch.yml +84 -0
- package/lib/client.js +6611 -0
- package/lib/file-references.js +366 -0
- package/lib/fs.js +1368 -0
- package/lib/index.js +19034 -0
- package/lib/local-bridge-refs.js +16 -0
- package/lib/local-bridge.js +16 -0
- package/lib/terminal-wrapper.js +294 -0
- package/lib/types/src/client/controls.d.ts +131 -0
- package/lib/types/src/client/core/card-focus-trap.d.ts +45 -0
- package/lib/types/src/client/core/controls.d.ts +164 -0
- package/lib/types/src/client/core/latest.d.ts +48 -0
- package/lib/types/src/client/core/locale.d.ts +38 -0
- package/lib/types/src/client/core/message.d.ts +37 -0
- package/lib/types/src/client/core/panel-model.d.ts +134 -0
- package/lib/types/src/client/core/portal.d.ts +266 -0
- package/lib/types/src/client/core/remote.d.ts +84 -0
- package/lib/types/src/client/core/styles.d.ts +458 -0
- package/lib/types/src/client/file-tree-io.d.ts +19 -0
- package/lib/types/src/client/file-tree.d.ts +271 -0
- package/lib/types/src/client/flows/directory-browser.d.ts +587 -0
- package/lib/types/src/client/flows/session-directory.d.ts +415 -0
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +480 -0
- package/lib/types/src/client/index.d.ts +49 -0
- package/lib/types/src/client/locale.d.ts +31 -0
- package/lib/types/src/client/panel-model.d.ts +37 -0
- package/lib/types/src/client/panel.d.ts +77 -0
- package/lib/types/src/client/roots/connection-test.d.ts +69 -0
- package/lib/types/src/client/roots/roots-panel-state.d.ts +307 -0
- package/lib/types/src/client/roots/roots-panel-view.d.ts +280 -0
- package/lib/types/src/client/roots/roots-panel.d.ts +364 -0
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +35 -0
- package/lib/types/src/client/roots/transfer-panel.d.ts +241 -0
- package/lib/types/src/client/roots/transfer-progress.d.ts +67 -0
- package/lib/types/src/client/roots-panel-view.d.ts +236 -0
- package/lib/types/src/client/roots-panel.d.ts +347 -0
- package/lib/types/src/client/session/session-panel-view.d.ts +336 -0
- package/lib/types/src/client/session/session-panel.d.ts +322 -0
- package/lib/types/src/client/session/session-remote-mark.d.ts +110 -0
- package/lib/types/src/client/session/session-row-actions.d.ts +398 -0
- package/lib/types/src/client/session-panel.d.ts +152 -0
- package/lib/types/src/client/styles.d.ts +252 -0
- package/lib/types/src/client/workspace-directory-flow.d.ts +342 -0
- package/lib/types/src/config.d.ts +312 -0
- package/lib/types/src/file-references.d.ts +25 -0
- package/lib/types/src/fs.d.ts +46 -0
- package/lib/types/src/host/app/backend.d.ts +170 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +247 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +38 -0
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +215 -0
- package/lib/types/src/host/app/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin-world.d.ts +35 -0
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +369 -0
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +130 -0
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +26 -0
- package/lib/types/src/host/app/roots/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/roots/user-roots.d.ts +552 -0
- package/lib/types/src/host/app/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots-api-contract.d.ts +339 -0
- package/lib/types/src/host/app/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots-api-support.d.ts +182 -0
- package/lib/types/src/host/app/roots-api-transfers.d.ts +13 -0
- package/lib/types/src/host/app/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/routed-fs.d.ts +239 -0
- package/lib/types/src/host/app/serialize.d.ts +37 -0
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +221 -0
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +40 -0
- package/lib/types/src/host/app/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin-contract.d.ts +214 -0
- package/lib/types/src/host/app/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin-service.d.ts +38 -0
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +227 -0
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +55 -0
- package/lib/types/src/host/app/transport/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +82 -0
- package/lib/types/src/host/app/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport-service.d.ts +75 -0
- package/lib/types/src/host/app/user-roots.d.ts +461 -0
- package/lib/types/src/host/domain/bin-location.d.ts +79 -0
- package/lib/types/src/host/domain/capability.d.ts +78 -0
- package/lib/types/src/host/domain/connection.d.ts +95 -0
- package/lib/types/src/host/domain/errors.d.ts +55 -0
- package/lib/types/src/host/domain/glob-pattern.d.ts +62 -0
- package/lib/types/src/host/domain/literal-text.d.ts +63 -0
- package/lib/types/src/host/domain/path-grammar.d.ts +84 -0
- package/lib/types/src/host/domain/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/root.d.ts +74 -0
- package/lib/types/src/host/domain/roots/bin-location.d.ts +91 -0
- package/lib/types/src/host/domain/roots/connection.d.ts +106 -0
- package/lib/types/src/host/domain/roots/root.d.ts +108 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +102 -0
- package/lib/types/src/host/domain/roots/session-roots.d.ts +151 -0
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/route-table.d.ts +80 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +345 -0
- package/lib/types/src/host/domain/session/terminal-target.d.ts +136 -0
- package/lib/types/src/host/domain/session-admin.d.ts +330 -0
- package/lib/types/src/host/domain/shell.d.ts +47 -0
- package/lib/types/src/host/domain/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/terminal-target.d.ts +132 -0
- package/lib/types/src/host/domain/tool-requests.d.ts +369 -0
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/transfer/transfer.d.ts +260 -0
- package/lib/types/src/host/domain/transfer.d.ts +260 -0
- package/lib/types/src/host/infra/backend/local-backend.d.ts +84 -0
- package/lib/types/src/host/infra/backend/local-version.d.ts +43 -0
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +89 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +239 -0
- package/lib/types/src/host/infra/cache/lru.d.ts +52 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +77 -0
- package/lib/types/src/host/infra/temp-name.d.ts +19 -0
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +132 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +140 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +564 -0
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +418 -0
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +123 -0
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +131 -0
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +509 -0
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +353 -0
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/command-pool.d.ts +61 -0
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +140 -0
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +63 -0
- package/lib/types/src/host/infra/transport/git-status.d.ts +101 -0
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +26 -0
- package/lib/types/src/host/infra/transport/session-window.d.ts +51 -0
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +59 -0
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +62 -0
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +328 -0
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +302 -0
- package/lib/types/src/host/infra/transport/transport-base.d.ts +34 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +527 -0
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +66 -0
- package/lib/types/src/host/integration/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/askpass-registry.d.ts +59 -0
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/file-reference-provider.d.ts +115 -0
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +142 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +368 -0
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +21 -0
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +94 -0
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +369 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +177 -0
- package/lib/types/src/host/integration/fs-provider.d.ts +226 -0
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +38 -0
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +67 -0
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +130 -0
- package/lib/types/src/host/integration/management-tools.d.ts +64 -0
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +247 -0
- package/lib/types/src/host/integration/remote/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/remote/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +108 -0
- package/lib/types/src/host/integration/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote-file-tools.d.ts +241 -0
- package/lib/types/src/host/integration/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/remote-index.d.ts +165 -0
- package/lib/types/src/host/integration/remote-job-tools.d.ts +295 -0
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +97 -0
- package/lib/types/src/host/integration/remote-tools.d.ts +206 -0
- package/lib/types/src/host/integration/root-registry.d.ts +115 -0
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +144 -0
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +219 -0
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +52 -0
- package/lib/types/src/host/integration/roots-api.d.ts +407 -0
- package/lib/types/src/host/integration/roots-wiring.d.ts +211 -0
- package/lib/types/src/host/integration/session/session-api.d.ts +88 -0
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session/session-header.d.ts +151 -0
- package/lib/types/src/host/integration/session/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-admin-service.d.ts +236 -0
- package/lib/types/src/host/integration/session-api.d.ts +81 -0
- package/lib/types/src/host/integration/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session-header.d.ts +118 -0
- package/lib/types/src/host/integration/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-scope.d.ts +113 -0
- package/lib/types/src/host/integration/session-tools.d.ts +44 -0
- package/lib/types/src/host/integration/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +62 -0
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +60 -0
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +167 -0
- package/lib/types/src/host/integration/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/tools/management-tools.d.ts +245 -0
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +316 -0
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +43 -0
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +236 -0
- package/lib/types/src/host/integration/tools/session-target.d.ts +52 -0
- package/lib/types/src/host/integration/tools/session-tools.d.ts +46 -0
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +66 -0
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +48 -0
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +212 -0
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/transfer-adapters.d.ts +44 -0
- package/lib/types/src/host/integration/transfer-service.d.ts +164 -0
- package/lib/types/src/host/integration/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/user-roots.d.ts +461 -0
- package/lib/types/src/index.d.ts +66 -0
- package/lib/types/src/local-bridge-refs.d.ts +26 -0
- package/lib/types/src/local-bridge.d.ts +36 -0
- package/lib/types/src/shared/format.d.ts +13 -0
- package/lib/types/src/wiring/connections.d.ts +189 -0
- package/lib/types/src/wiring/fs.d.ts +13 -0
- package/lib/types/src/wiring/helpers.d.ts +144 -0
- package/lib/types/src/wiring/install-memo.d.ts +32 -0
- package/lib/types/src/wiring/legacy-dirs.d.ts +62 -0
- package/lib/types/src/wiring/package-paths.d.ts +35 -0
- package/lib/types/src/wiring/roots.d.ts +21 -0
- package/lib/types/src/wiring/state-dir.d.ts +29 -0
- package/lib/types/src/wiring/tools.d.ts +35 -0
- package/package.json +120 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "only the newest answer may land" gate.
|
|
3
|
+
*
|
|
4
|
+
* Why a module instead of a `let token = 0` in each caller: this repository's client tests **cannot render a
|
|
5
|
+
* React component** (no `react-dom`, see [`portal.ts`](./portal.ts)), so a generation counter written inside a
|
|
6
|
+
* component's `useCallback` is a decision no test can reach. Extracting it makes "a late answer is dropped"
|
|
7
|
+
* something a test can hold on to, which is the same reason `actionQueryKey` / `rootsQueryKey` /
|
|
8
|
+
* long-poll re-arms exist.
|
|
9
|
+
*
|
|
10
|
+
* The three call sites share one failure mode: two requests are in flight, the **older** one answers last, and
|
|
11
|
+
* its answer overwrites the newer state. That is not a theoretical race here — every one of these surfaces has
|
|
12
|
+
* a button that starts a second request while the first is still open (a refresh during an action, a re-read
|
|
13
|
+
* after a scope change, a poll racing a manual refresh).
|
|
14
|
+
*
|
|
15
|
+
* ```text
|
|
16
|
+
* const gate = createLatestOnly();
|
|
17
|
+
* const mine = gate.begin();
|
|
18
|
+
* const answer = await call();
|
|
19
|
+
* if (!gate.isCurrent(mine)) return; // a newer request owns the surface now
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-over-ssh/client/latest
|
|
23
|
+
*/
|
|
24
|
+
/** A monotonic gate that admits only the most recently issued request. */
|
|
25
|
+
export interface LatestOnly {
|
|
26
|
+
/**
|
|
27
|
+
* Issue a new request id.
|
|
28
|
+
*
|
|
29
|
+
* @returns the id this request must present when it answers.
|
|
30
|
+
*/
|
|
31
|
+
begin(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Whether an issued id is still the newest.
|
|
34
|
+
*
|
|
35
|
+
* @param token - the id `begin` returned to the request that is answering.
|
|
36
|
+
* @returns `true` only for the newest id; every earlier one is stale.
|
|
37
|
+
*/
|
|
38
|
+
isCurrent(token: number): boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a gate.
|
|
42
|
+
*
|
|
43
|
+
* Ids start at 1, so `0` (a value a caller can get from an uninitialised variable) is never "current" by
|
|
44
|
+
* accident.
|
|
45
|
+
*
|
|
46
|
+
* @returns the gate.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createLatestOnly(): LatestOnly;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-half copy for the `over-ssh` namespace.
|
|
3
|
+
*
|
|
4
|
+
* Registration is the **typed** form of `ctx.locale.register`: the namespace is merged into
|
|
5
|
+
* `LocaleNamespaceMap`, so a missing key in either shipped locale is a compile error, and the runtime
|
|
6
|
+
* additionally requires both locales at registration (bilingual balance). Keeping the dictionaries in a
|
|
7
|
+
* pure module is what makes them testable without a browser.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-over-ssh/client/locale
|
|
10
|
+
*/
|
|
11
|
+
/** Namespace owned by this plugin's client half. */
|
|
12
|
+
export declare const OVER_SSH_NS = "over-ssh";
|
|
13
|
+
/**
|
|
14
|
+
* Keys of this namespace.
|
|
15
|
+
*
|
|
16
|
+
* `LocaleNamespaceMap` maps a namespace to its **key union** (not to a value interface): the runtime then
|
|
17
|
+
* derives `Record<Key, string>` per locale, which is exactly what makes a missing translation a compile
|
|
18
|
+
* error.
|
|
19
|
+
*/
|
|
20
|
+
export type OverSshKey = 'sectionTitle' | 'sectionDescription' | 'noHostData'
|
|
21
|
+
/** The connection list's own heading / empty state / unknown-transport badge (audit ui P2-6, P2-7). */
|
|
22
|
+
| 'connectionsHeading' | 'connectionsEmpty' | 'connectionsTransportUnknown' | 'close' | 'rootsTitle' | 'rootsDescription' | 'rootsAdvancedSummary' | 'rootsAdvancedNote' | 'rootsEmpty' | 'rootsPending' | 'rootsAuthPassword' | 'rootsPasswordLabel' | 'rootsTestConnection' | 'rootsTesting' | 'rootsTestOk' | 'sessionPanelTitle' | 'sessionPanelDescription' | 'sessionPanelUnavailable' | 'sessionPanelLoading' | 'sessionPanelLoadFailed' | 'sessionPanelSessionsEmpty' | 'sessionPanelSessions' | 'sessionPanelBin' | 'sessionPanelBinEmpty' | 'sessionPanelSelectAll' | 'sessionPanelClear' | 'sessionPanelDeleteRecycle' | 'sessionPanelDeletePermanent' | 'sessionPanelRestore' | 'sessionPanelPurge' | 'sessionPanelSweep' | 'sessionPanelRefresh' | 'sessionPanelClose' | 'sessionPanelLive' | 'sessionPanelSubsession' | 'sessionPanelUnreadable' | 'sessionPanelConfirmTitle' | 'sessionPanelConfirmBody' | 'sessionPanelConfirmPurgeBody' | 'sessionPanelConfirmCancel' | 'sessionPanelConfirmAccept' | 'sessionPanelDeleteWorkspace' | 'sessionPanelConfirmWorkspaceBody' | 'sessionPanelSubsessionsHidden' | 'sessionPanelTotals' | 'sessionPanelOpen' | 'sessionPanelLiveWarning' | 'sessionRowRecycle' | 'sessionRowDelete' | 'sessionRowRecycleBody' | 'sessionRowDeleteBody' | 'sessionRowConfirmRecycle' | 'sessionRowConfirmDelete' | 'sessionRowCancel' | 'sessionRowClose' | 'sessionRowLoading' | 'sessionRowBlockedRunning' | 'sessionRowBlockedSubsession' | 'sessionRowBlockedUnreadable' | 'sessionRowMissing' | 'sessionRowUnavailable' | 'rootsTestDegraded' | 'rootsTestFailed' | 'rootsAddConnection' | 'rootsRemove' | 'rootsSave' | 'rootsEdit' | 'rootsEditTitle' | 'rootsEditSave' | 'rootsEditCancel' | 'rootsEditPasswordHint' | 'rootsEditRenameHint' | 'rootsConfirmRemoveTitle' | 'rootsConfirmRemoveBody' | 'rootsConfirmRemoveAccept' | 'rootsConfirmRemoveCancel' | 'rootsConnection' | 'rootsConnectionPlaceholder' | 'rootsRemotePathLabel' | 'rootsIdLabel' | 'rootsHostLabel' | 'rootsUserLabel' | 'rootsAuthLabel' | 'rootsAuthKey' | 'rootsKeyPathLabel' | 'rootsPortLabel' | 'rootsBadgeLocal' | 'rootsBadgeRemote' | 'rootsErrorPathRequired' | 'rootsErrorRemotePath' | 'rootsErrorConnectionRequired' | 'rootsErrorLocalPath' | 'rootsErrorDuplicate' | 'rootsErrorIdRequired' | 'rootsErrorIdInvalid' | 'rootsErrorIdDuplicate' | 'rootsErrorHostRequired' | 'rootsErrorUserRequired' | 'rootsErrorPort' | 'rootsImportTitle' | 'rootsImportDescription' | 'rootsImportOpen' | 'rootsImportEmpty' | 'rootsImportRefresh' | 'rootsImportSelectAll' | 'rootsImportClear' | 'rootsImportAccept' | 'rootsImportCancel' | 'rootsImportBusy' | 'rootsImportDone' | 'rootsImportIdHint' | 'rootsImportSkippedTitle' | 'rootsImportSkipWildcard' | 'rootsImportSkipDuplicate' | 'rootsImportSkipTaken' | 'rootsImportSkipUnusableId' | 'rootsImportSkipNotImportable' | 'rootsImportSkipUnknown' | 'rootsImportHasKey' | 'rootsImportHasJump' | 'transferTitle' | 'transferDescription' | 'transferEmpty' | 'transferLoading' | 'transferRefresh' | 'transferClear' | 'transferFailedTitle' | 'transferDone' | 'transferCancelled' | 'transferUploadLabel' | 'transferDownloadLabel' | 'transferFailed' | 'transferFailedHttp' | 'transferFailedHost' | 'transferFailureLine' | 'transferErrorCodeUnknown' | 'remoteFilesHint' | 'rootsImportFailed' | 'rootsErrorKeyPathRequired' | 'rootsErrorPasswordRequired' | 'rootsNoticeUnavailable' | 'rootsNoticeWarnings' | 'rootsNoticeSavedConnection' | 'rootsNoticeSavedEdit' | 'flowTitle' | 'flowDescription' | 'flowLocalHeading' | 'flowLocalHint' | 'flowPickFailed' | 'flowRemoteHeading' | 'flowRemoteHint' | 'flowConfirmRemote' | 'flowCancel' | 'flowPending' | 'flowBusy' | 'flowNoConnections' | 'flowConnectionsLoading' | 'flowNoAnchor' | 'flowUnavailable' | 'browseHeading' | 'browseUp' | 'browseHome' | 'browseCurrent' | 'browseEmpty' | 'browseLoading' | 'browseRetry' | 'browseFailed' | 'browseUnavailable' | 'browseNeedConnection' | 'browseTruncated'
|
|
23
|
+
/** The remote world's connection list: reading, and failed-with-reason (audit 2026-11-01b ui P2-4). */
|
|
24
|
+
| 'browseConnectionsLoading' | 'browseConnectionsFailed' | 'browseKindFile' | 'browseKindSymlink' | 'browseKindOther' | 'browseLocalMode' | 'browsePickNative' | 'browseRemoteMode' | 'browseOpenLocal' | 'browseOpenRemote' | 'browseChoose' | 'sessionAddOpen' | 'sessionAddTitle' | 'sessionAddHint' | 'sessionAddWorkspace' | 'sessionAddWorkspaceUnknown' | 'sessionAddPick' | 'sessionBrowse' | 'sessionBrowseBack' | 'sessionRootsTitle' | 'sessionRootsEmpty'
|
|
25
|
+
/** The session entry's card while it reads its directory list (the empty state must not be shown then). */
|
|
26
|
+
| 'sessionRootsLoading' | 'sessionRootsHint' | 'sessionRootsFailed' | 'sessionAddDone' | 'sessionAddFailed' | 'sessionResultNoSelection' | 'sessionResultMissingCwd' | 'sessionResultMissingEntryId' | 'sessionResultHttpFailed' | 'sessionResultHostRefused' | 'sessionResultActionFailed' | 'sessionResultUnreachable' | 'sessionResultDeletedPermanent' | 'sessionResultDeleteNone' | 'sessionResultRecycled' | 'sessionResultWorkspaceEmpty' | 'sessionResultWorkspacePurged' | 'sessionResultWorkspacePurgedRefused' | 'sessionResultWorkspaceRecycled' | 'sessionResultWorkspaceRecycledRefused' | 'sessionResultRestored' | 'sessionResultPurged' | 'sessionResultSweepNone' | 'sessionResultSweepDone' | 'sessionWorkspaceUnknown' | 'connectionTestNoAnswer' | 'connectionTestFailed' | 'connectionTestRefused'
|
|
27
|
+
/** The probe ran out of the client's own deadline (no host sentence exists for it). */
|
|
28
|
+
| 'connectionTestTimeout' | 'transferSummaryRunning' | 'transferSummaryDone' | 'transferRemaining' | 'transferRemainingUnknown' | 'transferDurationUnknown' | 'rootsImportSummary' | 'rootsImportRefusedTitle' | 'rootsImportRefusedLine';
|
|
29
|
+
/** Merge this plugin's namespace into the shared locale map, so `t('over-ssh.…')` is typed. */
|
|
30
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
31
|
+
interface LocaleNamespaceMap {
|
|
32
|
+
'over-ssh': OverSshKey;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Every shipped locale's dictionary. Both `zh` and `en` are required by the runtime. */
|
|
36
|
+
export declare const OVER_SSH_DICTS: Record<'zh' | 'en', Record<OverSshKey, string>>;
|
|
37
|
+
/** Dictionary keys, so a completeness test never has to restate them. */
|
|
38
|
+
export declare function overSshKeys(): readonly OverSshKey[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred copy: a dictionary key plus its parameters, and the one resolver for it.
|
|
3
|
+
*
|
|
4
|
+
* Why this is `core/` and not `roots/`: three modules need the same two lines — the roots panel model, the
|
|
5
|
+
* transfer panel and (since the audit) the session panel's result sentences — and two of them are **pure
|
|
6
|
+
* models** that must not import React. `roots/roots-panel-state.ts` used to own the resolver, which meant every
|
|
7
|
+
* pure model either imported a React-bearing module or (what actually happened) duplicated the six lines with a
|
|
8
|
+
* comment apologising for the duplication (`core/panel-model.ts`). One implementation, no React.
|
|
9
|
+
*
|
|
10
|
+
* The contract a dictionary keeps: a **missing** key renders as the key itself. That is deliberate — a missing
|
|
11
|
+
* translation must be visible in the UI, never silently blank (see `roots-panel-state.ts`'s original note).
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-over-ssh/client/message
|
|
14
|
+
*/
|
|
15
|
+
/** A message is a key plus its parameters; a component resolves it against the active dictionary. */
|
|
16
|
+
export interface MessageKey {
|
|
17
|
+
/** The dictionary key (an entry of `OverSshKey` for this plugin's own copy). */
|
|
18
|
+
readonly key: string;
|
|
19
|
+
/** Values for the template's `{name}` placeholders. */
|
|
20
|
+
readonly params?: Readonly<Record<string, string | number>>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Substitute `{name}` placeholders in one template.
|
|
24
|
+
*
|
|
25
|
+
* @param template - the dictionary string.
|
|
26
|
+
* @param params - the values to substitute; an absent value leaves the placeholder in place.
|
|
27
|
+
* @returns the rendered text.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveTemplate(template: string, params: Readonly<Record<string, string | number>> | undefined): string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve one message against a dictionary.
|
|
32
|
+
*
|
|
33
|
+
* @param copy - the active locale's dictionary (a plain string map).
|
|
34
|
+
* @param message - the key plus optional parameters.
|
|
35
|
+
* @returns the rendered text, or `message.key` itself when the dictionary has no such key.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveMessage(copy: Readonly<Record<string, string>>, message: MessageKey): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings panel's pure logic and copy wiring.
|
|
3
|
+
*
|
|
4
|
+
* Two rules that keep a client component testable without a browser:
|
|
5
|
+
*
|
|
6
|
+
* - **no `ctx` in components** (the platform contract): everything a component shows arrives through props;
|
|
7
|
+
* - consequently all decision-making lives here, where Node can run it. The component is a template.
|
|
8
|
+
*
|
|
9
|
+
* The host data channel arrives as **untrusted wire data** (`unknown`), so the conversion below whitelists
|
|
10
|
+
* fields one by one: a leaked `keyPath` / `password` / `credentialKey` cannot reach the panel even if the host
|
|
11
|
+
* ever started sending one. That is the client half's half of the "no credentials over the channel" rule the
|
|
12
|
+
* host face also enforces.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-over-ssh/client/panel-model
|
|
15
|
+
*/
|
|
16
|
+
import type { OverSshKey } from './locale.ts';
|
|
17
|
+
/**
|
|
18
|
+
* Transports a connection row can name; `unknown` means "the host did not say".
|
|
19
|
+
*
|
|
20
|
+
* Declared once, in the roots model the wire parser lives in, and re-exported here so both halves of the panel
|
|
21
|
+
* vocabulary agree (a second identical union is how `unknown` ended up rendered as a raw English word).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How a connection reaches the host: `alias`/`ssh2` are Tier 1 (no agent), `agent` is Tier 2, `unknown` is
|
|
25
|
+
* "the host did not say".
|
|
26
|
+
*
|
|
27
|
+
* Defined **here**, in `core`, and re-exported by `roots/roots-panel.ts`, not the other way round: the
|
|
28
|
+
* dependency rule for this half is `core → roots → session → flows → entry`, and importing it from `roots`
|
|
29
|
+
* was a reverse edge that `tests/architecture.test.ts` caught (the panel model is what a caller without the
|
|
30
|
+
* editor imports, so it must not reach into a feature folder for a type it owns).
|
|
31
|
+
*/
|
|
32
|
+
export type ConnectionTransport = 'alias' | 'ssh2' | 'agent' | 'unknown';
|
|
33
|
+
/** One connection line: what the user needs to know about a single saved connection. */
|
|
34
|
+
export interface ConnectionRow {
|
|
35
|
+
/** Connection id (the profile key the user sees in `ssh://` paths). */
|
|
36
|
+
readonly id: string;
|
|
37
|
+
/** Host name as configured. */
|
|
38
|
+
readonly host: string;
|
|
39
|
+
/** Transport currently in force, or `unknown` before the first connection. */
|
|
40
|
+
readonly transport: ConnectionTransport;
|
|
41
|
+
/** Whether the current transport is a degraded substitute. */
|
|
42
|
+
readonly degraded: boolean;
|
|
43
|
+
/** Why: the degradation reason, else the decision's explanation, else "not connected yet". */
|
|
44
|
+
readonly note: string;
|
|
45
|
+
/**
|
|
46
|
+
* The row's badge text, already localized by whoever built the row.
|
|
47
|
+
*
|
|
48
|
+
* The template used to render {@link ConnectionRow.transport} raw, so every row carried a badge reading the
|
|
49
|
+
* English wire word `unknown` in the Chinese UI as well. `alias`/`ssh2`/`agent` are protocol names and stay as
|
|
50
|
+
* they are; only the "the host did not say" case needs a word, and it now arrives from the dictionary
|
|
51
|
+
* (audit 2026-11-01b ui P2-6).
|
|
52
|
+
*/
|
|
53
|
+
readonly transportLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The 「测试」 button's state for this row, already localized; absent when the panel has no tester.
|
|
56
|
+
*
|
|
57
|
+
* The template stays copy-free (`panel.ts` renders text it is handed), and the strings are rebuilt on every
|
|
58
|
+
* re-registration, so a locale change re-renders them without any subscription here.
|
|
59
|
+
*/
|
|
60
|
+
readonly test?: {
|
|
61
|
+
readonly status: 'idle' | 'running' | 'ok' | 'failed';
|
|
62
|
+
/** Button label: 「测试」, or 「测试中…」 while the probe runs. */
|
|
63
|
+
readonly label: string;
|
|
64
|
+
/** Result line; empty while running. */
|
|
65
|
+
readonly detail: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The row's own edit/delete labels, already localized; absent when the panel has no editor.
|
|
69
|
+
*
|
|
70
|
+
* Localized here rather than in the component for the same reason as {@link ConnectionRow.test}: the
|
|
71
|
+
* template renders strings it is handed and never looks up copy itself.
|
|
72
|
+
*/
|
|
73
|
+
readonly actions?: {
|
|
74
|
+
readonly editLabel: string;
|
|
75
|
+
readonly removeLabel: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** What the panel renders. */
|
|
79
|
+
export interface PanelModel {
|
|
80
|
+
readonly title: string;
|
|
81
|
+
readonly description: string;
|
|
82
|
+
/** One line per connection; empty either before the host answered or when no connection is configured. */
|
|
83
|
+
readonly connections: readonly ConnectionRow[];
|
|
84
|
+
/** Heading of the connection list (it had none: the rows started right under the description). */
|
|
85
|
+
readonly connectionsHeading: string;
|
|
86
|
+
/** What an answered-but-empty list says (it used to render nothing at all). */
|
|
87
|
+
readonly connectionsEmpty: string;
|
|
88
|
+
/**
|
|
89
|
+
* Notice about the missing host data channel; empty once the host answered. An answered-but-empty list
|
|
90
|
+
* clears it too — "you configured no connection" and "the channel is not wired" are different facts, and
|
|
91
|
+
* the panel must not claim the second one when the first is true.
|
|
92
|
+
*/
|
|
93
|
+
readonly notice: string;
|
|
94
|
+
readonly closeLabel: string;
|
|
95
|
+
/**
|
|
96
|
+
* The confirmation card's text for deleting one connection, or absent when the panel cannot delete.
|
|
97
|
+
*
|
|
98
|
+
* A function of the row because the sentence names the connection **and** how many remote roots go with it
|
|
99
|
+
* - the fact that makes the confirmation necessary at all.
|
|
100
|
+
*/
|
|
101
|
+
readonly confirmRemove?: {
|
|
102
|
+
readonly title: string;
|
|
103
|
+
readonly cancelLabel: string;
|
|
104
|
+
readonly acceptLabel: string;
|
|
105
|
+
readonly body: (row: ConnectionRow, remoteRoots: number) => string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convert the host's connection status payload into panel rows.
|
|
110
|
+
*
|
|
111
|
+
* Every field is read individually and type-checked; unknown fields (including any credential the host might
|
|
112
|
+
* ever add) are dropped rather than forwarded. A row without a usable `id` and `host` is skipped: an
|
|
113
|
+
* unidentifiable line is worse than a missing one.
|
|
114
|
+
*
|
|
115
|
+
* @param value - the remote call's value, exactly as it arrived over the wire.
|
|
116
|
+
* @returns the rows the panel may render, in the host's order.
|
|
117
|
+
*/
|
|
118
|
+
export declare function toConnectionRows(value: unknown): readonly ConnectionRow[];
|
|
119
|
+
/**
|
|
120
|
+
* Build the panel model from one locale's dictionary.
|
|
121
|
+
*
|
|
122
|
+
* @param copy - the dictionary the caller resolved (`zh` or `en`, chosen by the locale runtime).
|
|
123
|
+
* @param options - `rows` are the connection lines the host channel produced; **omitted** means the channel
|
|
124
|
+
* never answered, which is the only case that shows the notice.
|
|
125
|
+
* @returns everything the component needs, with no further lookups.
|
|
126
|
+
*/
|
|
127
|
+
export declare function buildPanelModel(copy: Record<OverSshKey, string>, options?: {
|
|
128
|
+
readonly rows?: readonly ConnectionRow[];
|
|
129
|
+
/**
|
|
130
|
+
* Whether the panel has an editor behind it. When true, rows carry their edit/delete labels and the model
|
|
131
|
+
* carries the removal confirmation; when false (the default) the page is read-only.
|
|
132
|
+
*/
|
|
133
|
+
readonly editable?: boolean;
|
|
134
|
+
}): PanelModel;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **body-level portal**: the one place this plugin's cards are mounted outside the host's own layout.
|
|
3
|
+
*
|
|
4
|
+
* Why it has to exist, and why the previous "inline, narrow-first" plan was wrong: the
|
|
5
|
+
* `sidebar.workspaces.directoryFlow` hole renders **inside the sidebar's title row**, which is a flex row
|
|
6
|
+
* that clips its overflow. A card rendered in place there is a flex item, and a flex item is *shrunk*
|
|
7
|
+
* (`flex-shrink: 1`) before it ever overflows - `max-width: 100%` says nothing about shrinking, and the row
|
|
8
|
+
* does not wrap. The real screenshot after the last release showed exactly that: two or three vertical lines
|
|
9
|
+
* with a clipped "选择连接并填写". That is a fact about the real DOM, not a hypothesis, so the card has to
|
|
10
|
+
* leave the row: one container per mount, appended to the **injected** `document.body` while the interaction
|
|
11
|
+
* is open, removed again when it closes, and `position: fixed` inside it.
|
|
12
|
+
*
|
|
13
|
+
* Two rules this module keeps:
|
|
14
|
+
*
|
|
15
|
+
* - **The DOM is injected.** {@link createCardPortal} takes the document; nothing here touches `document` at
|
|
16
|
+
* module scope, so importing this module off-browser (Node's test runner, the host half) does nothing, and
|
|
17
|
+
* every branch is driven by a fake in the offline tests.
|
|
18
|
+
* - **One container per mount.** `mount()` and `unmount()` are idempotent: a re-render while the card is open
|
|
19
|
+
* must not append a second container, and a double close must not remove the same node twice.
|
|
20
|
+
*
|
|
21
|
+
* `createPortal` comes from `react-dom`, which is **not** a dependency of this repository but *is* a client
|
|
22
|
+
* baseline external: `PLATFORM_MODULES` in `packages/client/web/src/platform.ts` names it among the modules
|
|
23
|
+
* the shell seeds into the frozen module table, and the loader calls every client factory as
|
|
24
|
+
* `factory(require)` with a `require` that looks a specifier up in that seed table first
|
|
25
|
+
* (`ClientModuleSystem.makeRequire`). The lookup below therefore goes through that same `require` instead of
|
|
26
|
+
* a static import, for one concrete reason: a static `import 'react-dom'` is evaluated when Node's test
|
|
27
|
+
* runner loads this module, and `react-dom` is not installed here - every offline client test would fail at
|
|
28
|
+
* import time. The lookup is lazy, and a runtime that cannot supply `react-dom` renders the card in place
|
|
29
|
+
* (the pre-portal behaviour) instead of failing, with one warning so the degradation is visible in a log.
|
|
30
|
+
*
|
|
31
|
+
* @module dsh-over-ssh/client/portal
|
|
32
|
+
*/
|
|
33
|
+
import type { ReactNode } from 'react';
|
|
34
|
+
/** The part of `document` this module uses. Structural, so a fake document is a full citizen. */
|
|
35
|
+
export interface PortalDocument {
|
|
36
|
+
/** The element a card container is attached to while the card is open. */
|
|
37
|
+
readonly body?: PortalContainer | null | undefined;
|
|
38
|
+
/** Whatever held focus when the card opened; the card gives focus back to it when it closes. */
|
|
39
|
+
readonly activeElement?: FocusTarget | null | undefined;
|
|
40
|
+
createElement(tag: string): PortalElement;
|
|
41
|
+
/**
|
|
42
|
+
* Present on a real document; the shell hangs its global key listener here.
|
|
43
|
+
*
|
|
44
|
+
* `options` exists for one caller: the settings-hosted card traps Tab in the **capture** phase. The platform's
|
|
45
|
+
* own modal layer listens on `document` in the bubble phase and, seeing focus outside its panel, pulls it back
|
|
46
|
+
* — a bubble listener of ours would run *after* it (registration order) and could no longer tell which field
|
|
47
|
+
* Tab came from. Capture runs first, so the card keeps the key it owns.
|
|
48
|
+
*/
|
|
49
|
+
addEventListener?(type: 'keydown', listener: (event: PortalKeyEvent) => void, options?: boolean): void;
|
|
50
|
+
/** The other half of {@link addEventListener}. */
|
|
51
|
+
removeEventListener?(type: 'keydown', listener: (event: PortalKeyEvent) => void, options?: boolean): void;
|
|
52
|
+
}
|
|
53
|
+
/** One element the portal created. Only what the portal itself does with it is typed here. */
|
|
54
|
+
export interface PortalElement {
|
|
55
|
+
/** The class attribute; the port has already been given its vocabulary classes when it is attached. */
|
|
56
|
+
className: string;
|
|
57
|
+
/** Present on a real element; used to move focus into the card. */
|
|
58
|
+
querySelector?(selector: string): FocusTarget | null | undefined;
|
|
59
|
+
/** Present on a real element; used by the Tab trap to list the card's controls in order. */
|
|
60
|
+
querySelectorAll?(selector: string): ArrayLike<FocusTarget> | null | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The part of a keyboard event the portal reads.
|
|
64
|
+
*
|
|
65
|
+
* Structural, like {@link KeyEventLike} in `./controls.ts`: the pure helpers are testable offline, and a real
|
|
66
|
+
* `KeyboardEvent` is assignable to it.
|
|
67
|
+
*/
|
|
68
|
+
export interface PortalKeyEvent {
|
|
69
|
+
/** The key name, e.g. `'Escape'` or `'Tab'`. */
|
|
70
|
+
readonly key?: string;
|
|
71
|
+
/** True while Shift is held: the Tab trap walks backwards. */
|
|
72
|
+
readonly shiftKey?: boolean;
|
|
73
|
+
/** Cancel the browser's own focus move when the trap takes over. */
|
|
74
|
+
preventDefault?(): void;
|
|
75
|
+
/** Stop the event before a modal layer behind the card sees it (see {@link trapCardTab}). */
|
|
76
|
+
stopPropagation?(): void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The **one** shell every confirmation card is portalled into.
|
|
80
|
+
*
|
|
81
|
+
* A confirmation is the same object wherever it is raised: a sentence naming what will be destroyed, and two
|
|
82
|
+
* buttons. Three surfaces used to compose that object three ways (audit 2026-11-01b ui P2-8 / task-12 B1); this
|
|
83
|
+
* constant is the whole fix, together with {@link CONFIRM_BACKDROP_CLASS}.
|
|
84
|
+
*/
|
|
85
|
+
export declare const CONFIRM_PORTAL_CLASS: string;
|
|
86
|
+
/**
|
|
87
|
+
* The confirmation's click sink.
|
|
88
|
+
*
|
|
89
|
+
* It uses the settings tier (1001) because the settings panel is one of the three raisers and its modal uses
|
|
90
|
+
* 900/1001; the overlay is above everything and sits at the same tier, so one constant is right for both.
|
|
91
|
+
*/
|
|
92
|
+
export declare const CONFIRM_BACKDROP_CLASS: string;
|
|
93
|
+
/** The two operations the portal needs from `document.body`. */
|
|
94
|
+
export interface PortalContainer {
|
|
95
|
+
appendChild(node: PortalElement): unknown;
|
|
96
|
+
removeChild(node: PortalElement): unknown;
|
|
97
|
+
}
|
|
98
|
+
/** Anything that can take focus; `document.activeElement` is one. */
|
|
99
|
+
export interface FocusTarget {
|
|
100
|
+
focus?(): void;
|
|
101
|
+
readonly isConnected?: boolean;
|
|
102
|
+
}
|
|
103
|
+
/** Render a card somewhere else. The browser passes a function built on `react-dom`'s `createPortal`. */
|
|
104
|
+
export type PortalRenderer = (children: ReactNode, container: unknown) => ReactNode;
|
|
105
|
+
/**
|
|
106
|
+
* What a template needs to put its card in a portal: the container element (created from the injected
|
|
107
|
+
* document) and the renderer that fills it.
|
|
108
|
+
*
|
|
109
|
+
* The template never creates either of them - it only decides whether it has one. That keeps the templates
|
|
110
|
+
* pure element trees the offline tests walk, and makes "the card is not in the host container" an assertion
|
|
111
|
+
* about an injected fake rather than about React.
|
|
112
|
+
*/
|
|
113
|
+
export interface CardPortalTarget {
|
|
114
|
+
/** The body-level element the card is rendered into. */
|
|
115
|
+
readonly container: unknown;
|
|
116
|
+
/** Render the card into {@link container}. */
|
|
117
|
+
readonly render: PortalRenderer;
|
|
118
|
+
}
|
|
119
|
+
/** One mount's body-level container, plus its attach/detach lifecycle. */
|
|
120
|
+
export interface CardPortal extends CardPortalTarget {
|
|
121
|
+
/**
|
|
122
|
+
* The click sink that sits **under** the card and counts as "the page behind the dialog".
|
|
123
|
+
*
|
|
124
|
+
* `undefined` when the caller asked for no backdrop, when there is no document, or when the runtime cannot
|
|
125
|
+
* portal at all (an in-place card has no modal geometry to protect). See {@link CreateCardPortalOptions.backdropClassName}.
|
|
126
|
+
*/
|
|
127
|
+
readonly backdrop: PortalElement | undefined;
|
|
128
|
+
/** True while the container is attached to `document.body`. */
|
|
129
|
+
readonly mounted: boolean;
|
|
130
|
+
/** Attach the container; a second call while attached does nothing. */
|
|
131
|
+
mount(): void;
|
|
132
|
+
/** Detach the container; a second call while detached does nothing. */
|
|
133
|
+
unmount(): void;
|
|
134
|
+
/** Move focus to the card's first control. `false` when there is nothing focusable (or no DOM). */
|
|
135
|
+
focusCard(): boolean;
|
|
136
|
+
}
|
|
137
|
+
/** What {@link createCardPortal} needs: the injected document, the container's classes and the renderer. */
|
|
138
|
+
export interface CreateCardPortalOptions {
|
|
139
|
+
/** The page's `document`, or `undefined` off-browser, where the whole portal is a no-op. */
|
|
140
|
+
readonly document: PortalDocument | undefined;
|
|
141
|
+
/** Classes of the body-level container (`CLASS.portal` plus one placement class). */
|
|
142
|
+
readonly className: string;
|
|
143
|
+
/** How the card is rendered into the container; the shells pass {@link renderInPortal}. */
|
|
144
|
+
readonly render: PortalRenderer;
|
|
145
|
+
/**
|
|
146
|
+
* Classes of the **backdrop**: the element this portal inserts under the card to swallow pointer input.
|
|
147
|
+
*
|
|
148
|
+
* Why it exists at all: the card claims `aria-modal="true"` and traps Tab, but the page behind it stayed
|
|
149
|
+
* *clickable* — the portal only ever created the card's own container, which hugs the card, so everywhere else
|
|
150
|
+
* the pointer reached the surface underneath. With the connection editor open that meant a click could land on
|
|
151
|
+
* the 「保存」 of the form behind it (and, before the drafts were split, submit the dialog's values as a new
|
|
152
|
+
* connection). A backdrop is the pointer half of the modal promise; the Tab trap is the keyboard half
|
|
153
|
+
* (audit 2026-11-01b ui P2-1).
|
|
154
|
+
*
|
|
155
|
+
* Optional so a caller that renders a **non-modal** card keeps the old behaviour, and so the offline portal
|
|
156
|
+
* tests can keep asserting the container lifecycle alone.
|
|
157
|
+
*/
|
|
158
|
+
readonly backdropClassName?: string;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The selector of the first thing in a card that can take focus.
|
|
162
|
+
*
|
|
163
|
+
* Disabled controls are excluded, and so is a `tabindex="-1"` element (a programmatic focus target, not a
|
|
164
|
+
* user one): the point of the initial focus is that the next Tab goes somewhere sensible.
|
|
165
|
+
*/
|
|
166
|
+
export declare const CARD_FOCUS_SELECTOR = "button:not([disabled]), input:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex=\"-1\"])";
|
|
167
|
+
/**
|
|
168
|
+
* Create one mount's card portal.
|
|
169
|
+
*
|
|
170
|
+
* The container is created **eagerly** (a React portal needs its container during the commit that renders the
|
|
171
|
+
* card) but attached **lazily**: nothing is in the body until `mount()` runs, which the shell does from a
|
|
172
|
+
* layout effect on the open edge, so a closed surface costs no DOM at all.
|
|
173
|
+
*
|
|
174
|
+
* @param options - the injected document, the container's classes and the card renderer.
|
|
175
|
+
* @returns the portal handle; every method is safe with no document and safe to call twice.
|
|
176
|
+
*/
|
|
177
|
+
export declare function createCardPortal(options: CreateCardPortalOptions): CardPortal;
|
|
178
|
+
/**
|
|
179
|
+
* Move focus to the first control inside a container.
|
|
180
|
+
*
|
|
181
|
+
* @param container - the card's container, or `undefined` when there is no DOM.
|
|
182
|
+
* @returns whether something actually took focus.
|
|
183
|
+
*/
|
|
184
|
+
export declare function focusFirstControl(container: PortalElement | undefined): boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Every control inside the card that Tab can reach, in DOM order.
|
|
187
|
+
*
|
|
188
|
+
* @param container - the card's container, or `undefined` when there is no DOM.
|
|
189
|
+
* @returns the focusable elements; `[]` when the container or `querySelectorAll` is absent.
|
|
190
|
+
*/
|
|
191
|
+
export declare function focusableWithin(container: PortalElement | undefined): readonly FocusTarget[];
|
|
192
|
+
/**
|
|
193
|
+
* Keep Tab inside the open card.
|
|
194
|
+
*
|
|
195
|
+
* A dialog that lets Tab walk into the page behind it is a dialog the keyboard user can leave without
|
|
196
|
+
* dismissing; trapping the two ends is the minimum the modal promise requires. `active` is passed in (rather
|
|
197
|
+
* than read from the document) so the rule is a pure function of its arguments.
|
|
198
|
+
*
|
|
199
|
+
* @param event - the keydown event, structurally.
|
|
200
|
+
* @param container - the card's container.
|
|
201
|
+
* @param active - the element that currently holds focus.
|
|
202
|
+
* @returns whether the trap handled the key (and already called `preventDefault`).
|
|
203
|
+
*/
|
|
204
|
+
export declare function trapTab(event: {
|
|
205
|
+
readonly shiftKey?: boolean;
|
|
206
|
+
preventDefault?(): void;
|
|
207
|
+
}, container: PortalElement | undefined, active: FocusTarget | undefined): boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Keep Tab inside a card that is hosted **inside another modal**.
|
|
210
|
+
*
|
|
211
|
+
* The settings page is a platform modal with its own focus trap (`useModalLayer`): it listens for Tab on
|
|
212
|
+
* `document` and, when focus is not inside its panel, moves focus to that panel's first control. This card is
|
|
213
|
+
* deliberately *outside* that panel (it is portalled to the body above the modal layers, audit 0088), so without
|
|
214
|
+
* this the platform would pull the caret out of the dialog on the first Tab.
|
|
215
|
+
*
|
|
216
|
+
* Two things have to happen, in this order:
|
|
217
|
+
*
|
|
218
|
+
* 1. `stopPropagation` — the platform's listener is on `document` in the **bubble** phase, so the card's own
|
|
219
|
+
* listener must be registered in the capture phase ({@link PortalDocument.addEventListener}'s `options`) and
|
|
220
|
+
* consume the event before it bubbles back up;
|
|
221
|
+
* 2. delegate to {@link trapTab} for the wrap at both ends. **In the middle it returns `false` and does not
|
|
222
|
+
* preventDefault** — the browser's own Tab then walks the card's fields in order, which is exactly right.
|
|
223
|
+
*
|
|
224
|
+
* @param event - the keydown event, structurally.
|
|
225
|
+
* @param container - the card's container.
|
|
226
|
+
* @param active - the element that currently holds focus.
|
|
227
|
+
* @returns whether Tab was consumed (the trap already called `preventDefault`).
|
|
228
|
+
*/
|
|
229
|
+
export declare function trapCardTab(event: PortalKeyEvent, container: PortalElement | undefined, active: FocusTarget | undefined): boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Remember what held focus before the card opened.
|
|
232
|
+
*
|
|
233
|
+
* @param doc - the page's `document`, or `undefined` off-browser.
|
|
234
|
+
* @returns the focused element, or `undefined` when nothing was focused.
|
|
235
|
+
*/
|
|
236
|
+
export declare function captureFocusOrigin(doc: PortalDocument | undefined): FocusTarget | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Give focus back to the element {@link captureFocusOrigin} remembered.
|
|
239
|
+
*
|
|
240
|
+
* @param origin - the remembered element.
|
|
241
|
+
* @returns whether focus was handed back; `false` when the trigger is gone (the owner re-rendered it away).
|
|
242
|
+
*/
|
|
243
|
+
export declare function restoreFocusOrigin(origin: FocusTarget | undefined): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* True when a card can actually be portalled here: this runtime supplies `react-dom`.
|
|
246
|
+
*
|
|
247
|
+
* Without it the shell must render the card in place and skip the container lifecycle entirely (an empty
|
|
248
|
+
* body-level node would just be appended and removed for nothing).
|
|
249
|
+
*
|
|
250
|
+
* @returns whether {@link renderInPortal} has a `createPortal` to work with.
|
|
251
|
+
*/
|
|
252
|
+
export declare function canRenderInPortal(): boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Render a card into the portal container, with `react-dom`'s `createPortal`.
|
|
255
|
+
*
|
|
256
|
+
* @param children - the card element the template built.
|
|
257
|
+
* @param container - the body-level element from {@link createCardPortal}.
|
|
258
|
+
* @returns the portal element, or `children` unchanged when this runtime supplies no `react-dom`.
|
|
259
|
+
*/
|
|
260
|
+
export declare function renderInPortal(children: ReactNode, container: unknown): ReactNode;
|
|
261
|
+
/**
|
|
262
|
+
* The page's `document`, when this module runs in a browser.
|
|
263
|
+
*
|
|
264
|
+
* @returns the structural document view, or `undefined` off-browser.
|
|
265
|
+
*/
|
|
266
|
+
export declare function browserPortalDocument(): PortalDocument | undefined;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client half's host data channel: mount the `sshdev` namespace and read connection status.
|
|
3
|
+
*
|
|
4
|
+
* Three facts decide the shape of this module, all read from 0.2.0-rc.2 (see
|
|
5
|
+
* [`doc/02-platform/remote-data-channel.md`](../../doc/02-platform/remote-data-channel.md)):
|
|
6
|
+
*
|
|
7
|
+
* 1. **The namespace is mounted by us, not by `api-remotes`.** That core assembly lists its packages
|
|
8
|
+
* explicitly, so a third-party plugin mounts its own `TypertRemoteContribution` through
|
|
9
|
+
* `ctx.remote.$mount`.
|
|
10
|
+
* 2. **`remote.<ns>` must NOT be in the plugin's `inject`.** We create the namespace ourselves inside
|
|
11
|
+
* `apply`; declaring it as a dependency would wait forever for a service only we can provide.
|
|
12
|
+
* 3. **The descriptors are hand-written and zero-argument.** `$mount` rejects any descriptor whose
|
|
13
|
+
* parameter/uplink/Context codec is not strict, and only the Typert build pipeline produces strict
|
|
14
|
+
* codecs. A method with **no parameters** triggers none of those checks, which is why the slice stops
|
|
15
|
+
* at one zero-argument read; anything with arguments needs generated `lib/typert.remote-client.js`.
|
|
16
|
+
*
|
|
17
|
+
* The wire contract (`sshdev/connections` → an array of `{id, host, transport, degraded, note}`) is mirrored
|
|
18
|
+
* in `src/host/integration/remote-face.ts`; the two halves are separate tsconfig programs and cannot import
|
|
19
|
+
* each other, so the constants below are a hand-synchronized pair.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-over-ssh/client/remote
|
|
22
|
+
*/
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import type { InvocationDescriptor, TypertDisposer, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
|
|
25
|
+
import { type ConnectionRow } from './panel-model.ts';
|
|
26
|
+
/** Wire namespace; the host face declares the same string. */
|
|
27
|
+
export declare const SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
28
|
+
/** Endpoint method name. */
|
|
29
|
+
export declare const SSH_DEV_REMOTE_METHOD = "connections";
|
|
30
|
+
/** Host Cordis service key the descriptor dispatches to (must match the host face's service name). */
|
|
31
|
+
export declare const SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
32
|
+
/** Contribution identity; `$mount` rejects a repeat of the same package name. */
|
|
33
|
+
export declare const SSH_DEV_REMOTE_PACKAGE = "dsh-over-ssh";
|
|
34
|
+
/**
|
|
35
|
+
* The one descriptor this slice mounts.
|
|
36
|
+
*
|
|
37
|
+
* `result: { mode: 'src-json' }` is the honest declaration: no strict result codec exists for this endpoint in
|
|
38
|
+
* this repository, and the Client Remote only consults `result.decode` when the mode is `strict`
|
|
39
|
+
* (`packages/api/gateway/src/client/index.ts:449-451`), so the raw JSON value passes through and is narrowed
|
|
40
|
+
* by {@link toConnectionRows} instead.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SSH_DEV_CONNECTIONS_DESCRIPTOR: InvocationDescriptor;
|
|
43
|
+
/** The mountable contribution. */
|
|
44
|
+
export declare const SSH_DEV_REMOTE_CONTRIBUTION: TypertRemoteContribution;
|
|
45
|
+
/**
|
|
46
|
+
* Mount the `sshdev` namespace in the caller's fiber.
|
|
47
|
+
*
|
|
48
|
+
* The returned disposer withdraws the namespace and its concrete method; `$mount` also registers everything
|
|
49
|
+
* as an effect of the calling context, so unloading the plugin tears the channel down even if the caller
|
|
50
|
+
* never keeps the disposer.
|
|
51
|
+
*
|
|
52
|
+
* @param ctx - the plugin's client context (needs `remote`).
|
|
53
|
+
* @returns the mount disposer.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mountConnectionChannel(ctx: Context): Promise<TypertDisposer>;
|
|
56
|
+
/**
|
|
57
|
+
* Read the host's connection status once.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately non-throwing: a channel that is absent, unmounted, offline, answered with a failure, or that
|
|
60
|
+
* rejects outright yields `undefined`, and the panel keeps its "no host data" notice.
|
|
61
|
+
*
|
|
62
|
+
* @param ctx - the plugin's client context; the namespace must already be mounted.
|
|
63
|
+
* @returns the panel rows, or `undefined` when no usable answer arrived.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fetchConnectionRows(ctx: Context): Promise<readonly ConnectionRow[] | undefined>;
|
|
66
|
+
/** Where {@link openConnectionChannel} reports its outcome. */
|
|
67
|
+
export interface ConnectionChannelObserver {
|
|
68
|
+
/** Called once with the first rows the host answered — including an empty list (no connection configured). */
|
|
69
|
+
readonly onRows: (rows: readonly ConnectionRow[]) => void;
|
|
70
|
+
/** Called when the channel could not be mounted or answered; the panel keeps its notice. */
|
|
71
|
+
readonly onUnavailable?: (reason: unknown) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Mount the channel, read it once, and hand the rows to `observer.onRows`.
|
|
75
|
+
*
|
|
76
|
+
* One read, no polling and no subscription: the panel is a snapshot of the moment the settings page (or the
|
|
77
|
+
* plugin) came up. Live refresh needs an observable source on the inject face — see the limitations section of
|
|
78
|
+
* `doc/02-platform/remote-data-channel.md`.
|
|
79
|
+
*
|
|
80
|
+
* @param ctx - the plugin's client context (needs `remote`).
|
|
81
|
+
* @param observer - row sink plus an optional unavailable-reason sink.
|
|
82
|
+
* @returns the mount disposer, or `undefined` when the channel never mounted.
|
|
83
|
+
*/
|
|
84
|
+
export declare function openConnectionChannel(ctx: Context, observer: ConnectionChannelObserver): Promise<TypertDisposer | undefined>;
|