dsh-over-ssh 0.1.0 → 0.2.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/README.md +19 -17
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +422 -337
- package/lib/file-references.js +92 -35
- package/lib/fs.js +295 -113
- package/lib/index.js +6912 -4330
- package/lib/terminal-wrapper.js +9 -4
- package/lib/types/src/config.d.ts +156 -9
- package/lib/types/src/file-references.d.ts +0 -1
- package/lib/types/src/host/app/backend.d.ts +44 -3
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +0 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +0 -1
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +12 -1
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-selector.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-service.d.ts +0 -1
- package/lib/types/src/host/domain/capability.d.ts +0 -1
- package/lib/types/src/host/domain/errors.d.ts +0 -1
- package/lib/types/src/host/domain/roots/connection.d.ts +0 -1
- package/lib/types/src/host/domain/roots/root.d.ts +24 -9
- package/lib/types/src/host/domain/roots/session-roots.d.ts +23 -1
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +0 -1
- package/lib/types/src/host/domain/session/session-admin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/transfer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/forward.d.ts +75 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +5 -1
- package/lib/types/src/host/infra/transport/transport-base.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport.d.ts +42 -2
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +0 -1
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +0 -1
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +18 -3
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +0 -1
- package/lib/types/src/index.d.ts +23 -1
- package/lib/types/src/local-bridge-refs.d.ts +0 -1
- package/lib/types/src/local-bridge.d.ts +0 -1
- package/lib/types/src/shared/root-keys.d.ts +120 -0
- package/lib/types/src/wiring/helpers.d.ts +0 -1
- package/package.json +5 -3
- package/lib/types/src/client/controls.d.ts +0 -131
- package/lib/types/src/client/core/card-focus-trap.d.ts +0 -45
- package/lib/types/src/client/core/controls.d.ts +0 -164
- package/lib/types/src/client/core/latest.d.ts +0 -48
- package/lib/types/src/client/core/locale.d.ts +0 -38
- package/lib/types/src/client/core/message.d.ts +0 -37
- package/lib/types/src/client/core/panel-model.d.ts +0 -134
- package/lib/types/src/client/core/portal.d.ts +0 -266
- package/lib/types/src/client/core/remote.d.ts +0 -84
- package/lib/types/src/client/core/styles.d.ts +0 -458
- package/lib/types/src/client/file-tree-io.d.ts +0 -19
- package/lib/types/src/client/file-tree.d.ts +0 -271
- package/lib/types/src/client/flows/directory-browser.d.ts +0 -587
- package/lib/types/src/client/flows/session-directory.d.ts +0 -415
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +0 -480
- package/lib/types/src/client/locale.d.ts +0 -31
- package/lib/types/src/client/panel-model.d.ts +0 -37
- package/lib/types/src/client/panel.d.ts +0 -77
- package/lib/types/src/client/roots/connection-test.d.ts +0 -69
- package/lib/types/src/client/roots/roots-panel-state.d.ts +0 -307
- package/lib/types/src/client/roots/roots-panel-view.d.ts +0 -280
- package/lib/types/src/client/roots/roots-panel.d.ts +0 -364
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +0 -35
- package/lib/types/src/client/roots/transfer-panel.d.ts +0 -241
- package/lib/types/src/client/roots/transfer-progress.d.ts +0 -67
- package/lib/types/src/client/roots-panel-view.d.ts +0 -236
- package/lib/types/src/client/roots-panel.d.ts +0 -347
- package/lib/types/src/client/session/session-panel-view.d.ts +0 -336
- package/lib/types/src/client/session/session-panel.d.ts +0 -322
- package/lib/types/src/client/session/session-remote-mark.d.ts +0 -110
- package/lib/types/src/client/session/session-row-actions.d.ts +0 -398
- package/lib/types/src/client/session-panel.d.ts +0 -152
- package/lib/types/src/client/styles.d.ts +0 -252
- package/lib/types/src/client/workspace-directory-flow.d.ts +0 -342
- package/lib/types/src/fs.d.ts +0 -46
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +0 -38
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +0 -215
- package/lib/types/src/host/app/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin-service.d.ts +0 -45
- package/lib/types/src/host/app/recycle-bin-world.d.ts +0 -35
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +0 -369
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +0 -130
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +0 -26
- package/lib/types/src/host/app/roots/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/roots/user-roots.d.ts +0 -552
- package/lib/types/src/host/app/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots-api-contract.d.ts +0 -339
- package/lib/types/src/host/app/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots-api-support.d.ts +0 -182
- package/lib/types/src/host/app/roots-api-transfers.d.ts +0 -13
- package/lib/types/src/host/app/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/routed-fs.d.ts +0 -239
- package/lib/types/src/host/app/serialize.d.ts +0 -37
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin-contract.d.ts +0 -214
- package/lib/types/src/host/app/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-service.d.ts +0 -38
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +0 -227
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +0 -55
- package/lib/types/src/host/app/transport-selector.d.ts +0 -98
- package/lib/types/src/host/app/transport-service.d.ts +0 -75
- package/lib/types/src/host/app/user-roots.d.ts +0 -461
- package/lib/types/src/host/domain/bin-location.d.ts +0 -79
- package/lib/types/src/host/domain/connection.d.ts +0 -95
- package/lib/types/src/host/domain/glob-pattern.d.ts +0 -62
- package/lib/types/src/host/domain/literal-text.d.ts +0 -63
- package/lib/types/src/host/domain/path-grammar.d.ts +0 -84
- package/lib/types/src/host/domain/recycle-bin.d.ts +0 -337
- package/lib/types/src/host/domain/root.d.ts +0 -74
- package/lib/types/src/host/domain/roots/bin-location.d.ts +0 -91
- package/lib/types/src/host/domain/roots/route-table.d.ts +0 -102
- package/lib/types/src/host/domain/route-table.d.ts +0 -80
- package/lib/types/src/host/domain/session/terminal-target.d.ts +0 -136
- package/lib/types/src/host/domain/session-admin.d.ts +0 -330
- package/lib/types/src/host/domain/shell.d.ts +0 -47
- package/lib/types/src/host/domain/ssh-config.d.ts +0 -73
- package/lib/types/src/host/domain/terminal-target.d.ts +0 -132
- package/lib/types/src/host/domain/tool-requests.d.ts +0 -369
- package/lib/types/src/host/domain/transfer.d.ts +0 -260
- package/lib/types/src/host/infra/backend/local-backend.d.ts +0 -84
- package/lib/types/src/host/infra/backend/local-version.d.ts +0 -43
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +0 -89
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +0 -239
- package/lib/types/src/host/infra/cache/lru.d.ts +0 -52
- package/lib/types/src/host/infra/cache/versioned.d.ts +0 -77
- package/lib/types/src/host/infra/temp-name.d.ts +0 -19
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +0 -132
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +0 -140
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +0 -564
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +0 -418
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +0 -121
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +0 -123
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +0 -131
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +0 -509
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +0 -353
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/command-pool.d.ts +0 -61
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +0 -140
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +0 -63
- package/lib/types/src/host/infra/transport/git-status.d.ts +0 -101
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +0 -26
- package/lib/types/src/host/infra/transport/session-window.d.ts +0 -51
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +0 -59
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +0 -62
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +0 -302
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +0 -66
- package/lib/types/src/host/integration/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent-preflight.d.ts +0 -56
- package/lib/types/src/host/integration/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/askpass-registry.d.ts +0 -59
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/file-reference-provider.d.ts +0 -115
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +0 -21
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +0 -94
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +0 -369
- package/lib/types/src/host/integration/fs/remote-index.d.ts +0 -177
- package/lib/types/src/host/integration/fs-provider.d.ts +0 -226
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +0 -38
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +0 -67
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +0 -130
- package/lib/types/src/host/integration/management-tools.d.ts +0 -64
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +0 -247
- package/lib/types/src/host/integration/remote/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/remote/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +0 -108
- package/lib/types/src/host/integration/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote-file-tools.d.ts +0 -241
- package/lib/types/src/host/integration/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/remote-index.d.ts +0 -165
- package/lib/types/src/host/integration/remote-job-tools.d.ts +0 -295
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +0 -97
- package/lib/types/src/host/integration/remote-tools.d.ts +0 -206
- package/lib/types/src/host/integration/root-registry.d.ts +0 -115
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/roots/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/roots/root-registry.d.ts +0 -144
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +0 -219
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +0 -52
- package/lib/types/src/host/integration/roots-api.d.ts +0 -407
- package/lib/types/src/host/integration/roots-wiring.d.ts +0 -211
- package/lib/types/src/host/integration/session/session-api.d.ts +0 -88
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session/session-header.d.ts +0 -151
- package/lib/types/src/host/integration/session/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-admin-service.d.ts +0 -236
- package/lib/types/src/host/integration/session-api.d.ts +0 -81
- package/lib/types/src/host/integration/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session-header.d.ts +0 -118
- package/lib/types/src/host/integration/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-scope.d.ts +0 -113
- package/lib/types/src/host/integration/session-tools.d.ts +0 -44
- package/lib/types/src/host/integration/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +0 -62
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +0 -60
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +0 -167
- package/lib/types/src/host/integration/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/tools/management-tools.d.ts +0 -245
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +0 -316
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +0 -43
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +0 -236
- package/lib/types/src/host/integration/tools/session-target.d.ts +0 -52
- package/lib/types/src/host/integration/tools/session-tools.d.ts +0 -46
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +0 -66
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +0 -48
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/transfer-adapters.d.ts +0 -44
- package/lib/types/src/host/integration/transfer-service.d.ts +0 -164
- package/lib/types/src/host/integration/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/user-roots.d.ts +0 -461
- package/lib/types/src/wiring/connections.d.ts +0 -189
- package/lib/types/src/wiring/fs.d.ts +0 -13
- package/lib/types/src/wiring/install-memo.d.ts +0 -32
- package/lib/types/src/wiring/legacy-dirs.d.ts +0 -62
- package/lib/types/src/wiring/package-paths.d.ts +0 -35
- package/lib/types/src/wiring/roots.d.ts +0 -21
- package/lib/types/src/wiring/state-dir.d.ts +0 -29
- package/lib/types/src/wiring/tools.d.ts +0 -35
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The 「测试」 button's state: one connection at a time, answer or failure, and nothing else.
|
|
3
|
-
*
|
|
4
|
-
* Kept out of the component (which is a pure template) and out of the roots editor (which owns the *file*):
|
|
5
|
-
* this is transient UI state whose only job is to hold "what did the last probe say" long enough to render it.
|
|
6
|
-
*
|
|
7
|
-
* The probe itself runs on the host (a real connect plus one command); this module only carries the request and
|
|
8
|
-
* narrows the answer, so the four fields it reads are the four the host whitelists - an answer that grew a
|
|
9
|
-
* field cannot reach the panel.
|
|
10
|
-
*
|
|
11
|
-
* @module dsh-over-ssh/client/connection-test
|
|
12
|
-
*/
|
|
13
|
-
import type { MessageKey } from '../core/message.ts';
|
|
14
|
-
import type { FetchLike } from './roots-panel.ts';
|
|
15
|
-
/** Where one connection's probe stands. */
|
|
16
|
-
/**
|
|
17
|
-
* Where one connection's probe stands.
|
|
18
|
-
*
|
|
19
|
-
* `idle` is the state a row is in **before** anyone clicked - the button must already be there (it is what the
|
|
20
|
-
* user clicks), so "no answer yet" is a status, not a missing field.
|
|
21
|
-
*/
|
|
22
|
-
export type ConnectionTestStatus = 'idle' | 'running' | 'ok' | 'failed';
|
|
23
|
-
/** The last answer for one connection. */
|
|
24
|
-
export interface ConnectionTestState {
|
|
25
|
-
readonly status: ConnectionTestStatus;
|
|
26
|
-
/** Transport the host actually used (`alias` / `ssh2` / `agent`), once it answered. */
|
|
27
|
-
readonly transport?: string;
|
|
28
|
-
/** Whether that transport was a degraded substitute. */
|
|
29
|
-
readonly degraded?: boolean;
|
|
30
|
-
/** Machine-readable failure code (`AUTH_FAILED`, `TIMEOUT`, …). */
|
|
31
|
-
readonly code?: string;
|
|
32
|
-
/** The host's own sentence about the failure (or the carrier's, e.g. `Failed to fetch`). */
|
|
33
|
-
readonly message?: string;
|
|
34
|
-
/**
|
|
35
|
-
* Our own sentence, when the failure was ours.
|
|
36
|
-
*
|
|
37
|
-
* A key rather than text: this module has no dictionary, and the three sentences it used to spell were Chinese
|
|
38
|
-
* in both locales (`roots-panel-view` renders them through `resolveMessage` with the active dictionary).
|
|
39
|
-
*/
|
|
40
|
-
readonly messageKey?: MessageKey;
|
|
41
|
-
/** Round trip in milliseconds, either way. */
|
|
42
|
-
readonly ms?: number;
|
|
43
|
-
}
|
|
44
|
-
/** The tester the panel drives. */
|
|
45
|
-
export interface ConnectionTester {
|
|
46
|
-
/** Current answers by connection id; a new map after every change (safe for a snapshot comparison). */
|
|
47
|
-
readonly getState: () => ReadonlyMap<string, ConnectionTestState>;
|
|
48
|
-
/** Probe one connection; resolves when the answer (or the failure) has been recorded. */
|
|
49
|
-
readonly test: (id: string) => Promise<void>;
|
|
50
|
-
/** Watch for changes; returns the disposer. */
|
|
51
|
-
readonly subscribe: (listener: () => void) => () => void;
|
|
52
|
-
/** Drop listeners; an in-flight answer is discarded rather than delivered. */
|
|
53
|
-
readonly dispose: () => void;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* How long a probe may stay 「测试中…」 before the client gives up on it.
|
|
57
|
-
*
|
|
58
|
-
* A real connect plus one command can take seconds, but it can also hang forever: the host's own connect has no
|
|
59
|
-
* deadline the client can see, and a `fetch` with no signal keeps its promise (and the row's button) in limbo.
|
|
60
|
-
* Without this the 「测试」 button stayed disabled and read 「测试中…」 **forever**, with no cancel and no way to try
|
|
61
|
-
* again short of reloading the page (audit 2026-11-01b ui P2-11).
|
|
62
|
-
*/
|
|
63
|
-
export declare const CONNECTION_TEST_TIMEOUT_MS = 15000;
|
|
64
|
-
export declare function createConnectionTester(options: {
|
|
65
|
-
readonly fetchImpl: FetchLike;
|
|
66
|
-
readonly path?: string;
|
|
67
|
-
/** Override for {@link CONNECTION_TEST_TIMEOUT_MS}; a test drives the timeout instead of waiting 15 s. */
|
|
68
|
-
readonly timeoutMs?: number;
|
|
69
|
-
}): ConnectionTester;
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 根面板的**状态层**:草稿、状态机、reducer、选择器与文案解析。
|
|
3
|
-
*
|
|
4
|
-
* 从 `roots-panel-view.ts` 抽出(审计 0076):这一层是纯函数,不碰 React,
|
|
5
|
-
* 也是测试最密集的部分;拆开后 view 只负责渲染与接线。
|
|
6
|
-
*
|
|
7
|
-
* @module dsh-over-ssh/client/roots-panel-state
|
|
8
|
-
*/
|
|
9
|
-
import type { OverSshKey } from '../core/locale.ts';
|
|
10
|
-
import { type MessageKey, type RootsSnapshot, type RootsView, type SshConfigListing } from './roots-panel.ts';
|
|
11
|
-
export type RootsPanelCopy = Record<OverSshKey, string>;
|
|
12
|
-
/** The three forms' contents. Kept flat so a patch is one spread and a test can name a single field. */
|
|
13
|
-
export interface RootsPanelDrafts {
|
|
14
|
-
/** Local root path (form 1). */
|
|
15
|
-
readonly localPath: string;
|
|
16
|
-
/** Remote root path (form 2). */
|
|
17
|
-
readonly remotePath: string;
|
|
18
|
-
/** Connection chosen for the remote root (form 2). */
|
|
19
|
-
readonly remoteConnectionId: string;
|
|
20
|
-
/** Optional label for the remote root (form 2). */
|
|
21
|
-
readonly remoteLabel: string;
|
|
22
|
-
/** New connection: id. */
|
|
23
|
-
readonly newId: string;
|
|
24
|
-
/** New connection: host. */
|
|
25
|
-
readonly newHost: string;
|
|
26
|
-
/** New connection: user. */
|
|
27
|
-
readonly newUser: string;
|
|
28
|
-
/** New connection: authentication mode. */
|
|
29
|
-
readonly newAuthKind: 'agent' | 'key' | 'password';
|
|
30
|
-
/** New connection: key path, only meaningful for `newAuthKind === 'key'`. */
|
|
31
|
-
readonly newKeyPath: string;
|
|
32
|
-
/**
|
|
33
|
-
* New connection: password, only meaningful for `newAuthKind === 'password'`.
|
|
34
|
-
*
|
|
35
|
-
* Held only between typing and the POST that follows it — the drafts are reset once the request settles, and
|
|
36
|
-
* no answer ever carries it back (the host stores it in `ctx.credentials`).
|
|
37
|
-
*/
|
|
38
|
-
readonly newPassword: string;
|
|
39
|
-
/** New connection: port as typed; parsed by the controller so a bad value can be refused, not coerced. */
|
|
40
|
-
readonly newPort: string;
|
|
41
|
-
}
|
|
42
|
-
/** A change to the drafts; every field is optional so one handler can serve every input. */
|
|
43
|
-
export type RootsPanelDraftPatch = Partial<RootsPanelDrafts>;
|
|
44
|
-
/**
|
|
45
|
-
* The **edit dialog's** own fields, kept apart from {@link RootsPanelDrafts}.
|
|
46
|
-
*
|
|
47
|
-
* The two forms used to share one flat draft set, and that is exactly what went wrong (audit 2026-11-01b ui
|
|
48
|
-
* P1-4 — three symptoms, one cause):
|
|
49
|
-
*
|
|
50
|
-
* - `edit-started` overwrote the add form's drafts with the row being edited, so opening 「编辑」 to look at a
|
|
51
|
-
* record threw away whatever the operator had already typed into 「新增连接」;
|
|
52
|
-
* - `edit-ended` reset **all** drafts, so even cancelling the dialog wiped the add form;
|
|
53
|
-
* - both forms read the same fields, so submitting the add form (reachable while the dialog is up, in a
|
|
54
|
-
* composition where the card has no backdrop) posted the edit dialog's values as a *new* connection.
|
|
55
|
-
*
|
|
56
|
-
* The password box belongs here, not in {@link RootsPanelDrafts}: it is always empty on render, and empty means
|
|
57
|
-
* 「keep the stored password」 — a rule about the edit path only (「新增连接」 has its own required-password rule).
|
|
58
|
-
*/
|
|
59
|
-
export interface RootsPanelEditDrafts {
|
|
60
|
-
/** The connection's id. Editable: a change is a rename that re-keys its remote roots. */
|
|
61
|
-
readonly id: string;
|
|
62
|
-
readonly host: string;
|
|
63
|
-
readonly user: string;
|
|
64
|
-
readonly authKind: 'agent' | 'key' | 'password';
|
|
65
|
-
readonly keyPath: string;
|
|
66
|
-
/** Always empty on render; empty means 「keep the stored password」. */
|
|
67
|
-
readonly password: string;
|
|
68
|
-
/** Port as typed; parsed by the controller so a bad value can be refused, not coerced. */
|
|
69
|
-
readonly port: string;
|
|
70
|
-
}
|
|
71
|
-
/** A change to the edit dialog's fields. */
|
|
72
|
-
export type RootsPanelEditDraftPatch = Partial<RootsPanelEditDrafts>;
|
|
73
|
-
/** @returns the edit dialog's fields in their fresh state. */
|
|
74
|
-
export declare function emptyRootsPanelEditDrafts(): RootsPanelEditDrafts;
|
|
75
|
-
/** Where an inline form message belongs. */
|
|
76
|
-
export type RootsPanelFormScope = 'roots' | 'connection' | 'edit';
|
|
77
|
-
/** A refusal or a failed submit, shown next to the form it belongs to. */
|
|
78
|
-
export interface RootsPanelFormNotice {
|
|
79
|
-
readonly scope: RootsPanelFormScope;
|
|
80
|
-
readonly message: MessageKey;
|
|
81
|
-
}
|
|
82
|
-
/** Everything the section renders from. */
|
|
83
|
-
export interface RootsPanelState {
|
|
84
|
-
/** True once an initial `load()` has been asked for, so a remount does not re-fetch. */
|
|
85
|
-
readonly started: boolean;
|
|
86
|
-
/** True while a request is in flight; the edit gate is `buildRootsView`'s `canEdit`. */
|
|
87
|
-
readonly pending: boolean;
|
|
88
|
-
/** True once a `load()` answered (ok or not). */
|
|
89
|
-
readonly loaded: boolean;
|
|
90
|
-
/** The last host answer, if any. */
|
|
91
|
-
readonly snapshot?: RootsSnapshot;
|
|
92
|
-
/** The last transport-level failure, if any. */
|
|
93
|
-
readonly error?: string;
|
|
94
|
-
/** The forms' contents. */
|
|
95
|
-
readonly drafts: RootsPanelDrafts;
|
|
96
|
-
/**
|
|
97
|
-
* The **edit dialog's** fields (see {@link RootsPanelEditDrafts}).
|
|
98
|
-
*
|
|
99
|
-
* Always present, empty while no dialog is open: an optional field would make "the dialog is closed" and "the
|
|
100
|
-
* value was dropped" the same state, which is how the two forms ended up sharing one draft set.
|
|
101
|
-
*/
|
|
102
|
-
readonly editDrafts: RootsPanelEditDrafts;
|
|
103
|
-
/**
|
|
104
|
-
* The connection currently open in the edit form, if any.
|
|
105
|
-
*
|
|
106
|
-
* Only the **id** is remembered: every other value lives in the drafts, so the form cannot show a stale copy
|
|
107
|
-
* of a record the host has since changed. `undefined` means the add form is the one on screen.
|
|
108
|
-
*/
|
|
109
|
-
readonly editingId?: string;
|
|
110
|
-
/**
|
|
111
|
-
* The connection whose removal is awaiting confirmation.
|
|
112
|
-
*
|
|
113
|
-
* Deleting a connection also deletes its roots (the host does that), which is not obvious from a row that
|
|
114
|
-
* shows only `id · host` - hence the confirmation, the same shape the session panel uses for its
|
|
115
|
-
* irreversible actions.
|
|
116
|
-
*/
|
|
117
|
-
readonly confirmingRemove?: string;
|
|
118
|
-
/** The last validation refusal or submit failure, with the form it belongs to. */
|
|
119
|
-
readonly formNotice?: RootsPanelFormNotice;
|
|
120
|
-
/**
|
|
121
|
-
* The last **successful** submit, with the form it belongs to.
|
|
122
|
-
*
|
|
123
|
-
* Adding a root, adding a connection or saving an edit used to end silently: the form emptied itself and the
|
|
124
|
-
* only evidence was the list changing somewhere else on the page. The success line reuses the same
|
|
125
|
-
* per-form slot mechanism as {@link RootsPanelState.formNotice}, with the opposite tone
|
|
126
|
-
* (audit 2026-11-01b ui P3-5).
|
|
127
|
-
*/
|
|
128
|
-
readonly formSuccess?: RootsPanelFormNotice;
|
|
129
|
-
/**
|
|
130
|
-
* True while the `~/.ssh/config` import dialog is open (FR-2.1.4).
|
|
131
|
-
*
|
|
132
|
-
* The dialog is state in this controller rather than local `useState` in the view for the same reason the
|
|
133
|
-
* forms are: the controller owns every request, and an import is a request. Keeping the listing here also
|
|
134
|
-
* means reopening the dialog does not re-read the file unless the user asks.
|
|
135
|
-
*/
|
|
136
|
-
readonly importOpen?: boolean;
|
|
137
|
-
/** The aliases the host offered, once `list-ssh-config` answered. */
|
|
138
|
-
readonly importListing?: SshConfigListing;
|
|
139
|
-
/** Aliases ticked in the dialog, in the order they were ticked. */
|
|
140
|
-
readonly importSelected?: readonly string[];
|
|
141
|
-
/** True while `list-ssh-config` or `import-ssh-config` is in flight. */
|
|
142
|
-
readonly importBusy?: boolean;
|
|
143
|
-
/** The last import's report, shown inside the dialog (ids imported, aliases refused). */
|
|
144
|
-
readonly importReport?: {
|
|
145
|
-
readonly imported: readonly string[];
|
|
146
|
-
readonly refused: readonly {
|
|
147
|
-
readonly alias: string;
|
|
148
|
-
readonly reason: string;
|
|
149
|
-
}[];
|
|
150
|
-
};
|
|
151
|
-
/** Why the last import attempt failed, shown inside the dialog. */
|
|
152
|
-
readonly importError?: string;
|
|
153
|
-
}
|
|
154
|
-
/** What the reducer accepts. */
|
|
155
|
-
export type RootsPanelAction =
|
|
156
|
-
/** A request is being sent; also marks the panel as started. */
|
|
157
|
-
{
|
|
158
|
-
readonly type: 'load';
|
|
159
|
-
} | {
|
|
160
|
-
readonly type: 'loaded';
|
|
161
|
-
readonly snapshot: RootsSnapshot;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* A request failed. With a `scope` it is a submit failure and is shown next to that form; without one it
|
|
165
|
-
* is the initial load failing, which the view's own `notice` already reports - showing both would say the
|
|
166
|
-
* same thing twice.
|
|
167
|
-
*/
|
|
168
|
-
| {
|
|
169
|
-
readonly type: 'failed';
|
|
170
|
-
readonly error: string;
|
|
171
|
-
readonly scope?: RootsPanelFormScope;
|
|
172
|
-
}
|
|
173
|
-
/** One form field changed: clears the inline message, because the user is fixing it. */
|
|
174
|
-
| {
|
|
175
|
-
readonly type: 'draft-changed';
|
|
176
|
-
readonly patch: RootsPanelDraftPatch;
|
|
177
|
-
}
|
|
178
|
-
/** One field of the **edit** dialog changed; the add form's drafts are untouched. */
|
|
179
|
-
| {
|
|
180
|
-
readonly type: 'edit-draft-changed';
|
|
181
|
-
readonly patch: RootsPanelEditDraftPatch;
|
|
182
|
-
}
|
|
183
|
-
/** A submit is in flight. */
|
|
184
|
-
| {
|
|
185
|
-
readonly type: 'submitted';
|
|
186
|
-
}
|
|
187
|
-
/** A draft was refused before any request; nothing was sent. */
|
|
188
|
-
| {
|
|
189
|
-
readonly type: 'refused';
|
|
190
|
-
readonly scope: RootsPanelFormScope;
|
|
191
|
-
readonly message: MessageKey;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* A request succeeded. `resetDrafts` clears the forms (an add did); a removal keeps them.
|
|
195
|
-
*
|
|
196
|
-
* `scope` decides which form gets the "saved" line — a removal passes none, because its evidence is the row
|
|
197
|
-
* leaving the list, not a sentence.
|
|
198
|
-
*/
|
|
199
|
-
| {
|
|
200
|
-
readonly type: 'succeeded';
|
|
201
|
-
readonly resetDrafts?: boolean;
|
|
202
|
-
readonly scope?: RootsPanelFormScope;
|
|
203
|
-
}
|
|
204
|
-
/** A removal is in flight; the reducer itself refuses a row that is not removable. */
|
|
205
|
-
| {
|
|
206
|
-
readonly type: 'remove';
|
|
207
|
-
readonly key: string;
|
|
208
|
-
}
|
|
209
|
-
/** Open the edit form for one connection, prefilled from the last answer. */
|
|
210
|
-
| {
|
|
211
|
-
readonly type: 'edit-started';
|
|
212
|
-
readonly id: string;
|
|
213
|
-
}
|
|
214
|
-
/** Close the edit form (cancelled, or the edit settled). */
|
|
215
|
-
| {
|
|
216
|
-
readonly type: 'edit-ended';
|
|
217
|
-
}
|
|
218
|
-
/** Ask for confirmation before removing a connection (it takes its roots with it). */
|
|
219
|
-
| {
|
|
220
|
-
readonly type: 'remove-connection-asked';
|
|
221
|
-
readonly id: string;
|
|
222
|
-
}
|
|
223
|
-
/** Dismiss the confirmation without removing anything. */
|
|
224
|
-
| {
|
|
225
|
-
readonly type: 'remove-connection-dismissed';
|
|
226
|
-
}
|
|
227
|
-
/** Open the ssh-config import dialog (FR-2.1.4). */
|
|
228
|
-
| {
|
|
229
|
-
readonly type: 'import-opened';
|
|
230
|
-
}
|
|
231
|
-
/** Close it, keeping nothing but the panel's own state. */
|
|
232
|
-
| {
|
|
233
|
-
readonly type: 'import-closed';
|
|
234
|
-
}
|
|
235
|
-
/** A read or an import is in flight. */
|
|
236
|
-
| {
|
|
237
|
-
readonly type: 'import-busy';
|
|
238
|
-
}
|
|
239
|
-
/** The host's candidate list arrived. */
|
|
240
|
-
| {
|
|
241
|
-
readonly type: 'import-loaded';
|
|
242
|
-
readonly listing: SshConfigListing;
|
|
243
|
-
}
|
|
244
|
-
/** One alias was ticked or unticked. */
|
|
245
|
-
| {
|
|
246
|
-
readonly type: 'import-toggled';
|
|
247
|
-
readonly alias: string;
|
|
248
|
-
}
|
|
249
|
-
/** Tick every candidate. */
|
|
250
|
-
| {
|
|
251
|
-
readonly type: 'import-all';
|
|
252
|
-
}
|
|
253
|
-
/** Untick everything. */
|
|
254
|
-
| {
|
|
255
|
-
readonly type: 'import-none';
|
|
256
|
-
}
|
|
257
|
-
/** The import settled: what went in, and what was refused with its reason. */
|
|
258
|
-
| {
|
|
259
|
-
readonly type: 'import-settled';
|
|
260
|
-
readonly imported: readonly string[];
|
|
261
|
-
readonly refused: readonly {
|
|
262
|
-
readonly alias: string;
|
|
263
|
-
readonly reason: string;
|
|
264
|
-
}[];
|
|
265
|
-
}
|
|
266
|
-
/** The read or the import failed. */
|
|
267
|
-
| {
|
|
268
|
-
readonly type: 'import-failed';
|
|
269
|
-
readonly error: string;
|
|
270
|
-
} | {
|
|
271
|
-
readonly type: 'import-failed';
|
|
272
|
-
readonly error: string;
|
|
273
|
-
};
|
|
274
|
-
/** The starting state: nothing loaded, no drafts, no queue. */
|
|
275
|
-
export declare function emptyRootsPanelDrafts(): RootsPanelDrafts;
|
|
276
|
-
/** @returns the state a freshly created controller holds. */
|
|
277
|
-
export declare function initialRootsPanelState(): RootsPanelState;
|
|
278
|
-
/**
|
|
279
|
-
* The panel's state machine. Pure: same state + same action => same result, and an action that changes
|
|
280
|
-
* nothing returns the **same object**, so a subscriber is never woken for a no-op.
|
|
281
|
-
*
|
|
282
|
-
* @param state - the current state.
|
|
283
|
-
* @param action - what happened.
|
|
284
|
-
* @returns the next state.
|
|
285
|
-
*/
|
|
286
|
-
export declare function rootsPanelReducer(state: RootsPanelState, action: RootsPanelAction): RootsPanelState;
|
|
287
|
-
/**
|
|
288
|
-
* The view model for a state.
|
|
289
|
-
*
|
|
290
|
-
* @param state - the panel state.
|
|
291
|
-
* @returns exactly what `buildRootsView` says: rows with `removable`, connections, notice and the edit gate.
|
|
292
|
-
*/
|
|
293
|
-
export declare function selectRootsView(state: RootsPanelState): RootsView;
|
|
294
|
-
/** True when the panel may accept input right now: an answer arrived and no request is in flight. */
|
|
295
|
-
export declare function canEdit(state: RootsPanelState): boolean;
|
|
296
|
-
/**
|
|
297
|
-
* Turn one `MessageKey` into text, with its parameters substituted.
|
|
298
|
-
*
|
|
299
|
-
* The implementation is `core/message.ts`'s (the session panel and the transfer panel need the same two lines and
|
|
300
|
-
* must not import this React-bearing module); what stays here is the roots panel's own convenience: no message
|
|
301
|
-
* renders as `''`, because every caller in this file is a template slot that simply renders nothing.
|
|
302
|
-
*
|
|
303
|
-
* @param copy - the dictionary of the active locale.
|
|
304
|
-
* @param message - the key plus optional `{name}` parameters.
|
|
305
|
-
* @returns the rendered text, or `''` when there is no message.
|
|
306
|
-
*/
|
|
307
|
-
export declare function resolveMessage(copy: RootsPanelCopy, message: MessageKey | undefined): string;
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The **editable** remote-workspaces section of the settings panel.
|
|
3
|
-
*
|
|
4
|
-
* Three layers, deliberately separated so the interesting parts can be tested without a browser:
|
|
5
|
-
*
|
|
6
|
-
* 1. {@link rootsPanelReducer} - a pure state machine over the panel's drafts, the last host answer and
|
|
7
|
-
* whether a request is in flight. No DOM, no I/O, no React: `tests/client-roots-view.test.ts` drives it
|
|
8
|
-
* directly.
|
|
9
|
-
* 2. {@link createRootsPanelController} - the same state plus the two calls from
|
|
10
|
-
* `./roots-panel.ts` (`load` / `send`). Everything that is *not* pure lives here: validation gate,
|
|
11
|
-
* double-submit gate, the refresh after a success, and dropping answers that a newer request superseded.
|
|
12
|
-
* 3. {@link RootsPanelSection} - a pure template (`createElement`, never JSX: Node's native type stripping
|
|
13
|
-
* cannot transform JSX) and {@link RootsPanelContainer}, the thin React shell that subscribes to the
|
|
14
|
-
* controller.
|
|
15
|
-
*
|
|
16
|
-
* Rules this file is responsible for, and where they are enforced:
|
|
17
|
-
*
|
|
18
|
-
* - **A `config` root never gets a delete button.** The button is rendered from
|
|
19
|
-
* {@link RootRowView.removable} and from nothing else; the controller refuses such a key before a POST.
|
|
20
|
-
* - **Nothing is submitted before validation.** `validateRootDraft` / `validateConnectionDraft` are the only
|
|
21
|
-
* way a draft becomes a request body, and their `MessageKey` refusal is rendered as text through the
|
|
22
|
-
* locale dictionaries.
|
|
23
|
-
* - **No double submit.** `canEdit === false` (no answer yet, or a request in flight) disables every control
|
|
24
|
-
* and stops every handler.
|
|
25
|
-
* - **No credentials.** This file can only send the fields its drafts hold; the password field does not
|
|
26
|
-
* exist, and `validateConnectionDraft` refuses `authKind: 'password'` outright.
|
|
27
|
-
*
|
|
28
|
-
* @module dsh-over-ssh/client/roots-panel-view
|
|
29
|
-
*/
|
|
30
|
-
import { type ReactNode } from 'react';
|
|
31
|
-
import { type CardPortal } from '../core/portal.ts';
|
|
32
|
-
import { type RootLocation, type RootsApi } from './roots-panel.ts';
|
|
33
|
-
import type { RootsPanelCopy, RootsPanelDraftPatch, RootsPanelEditDraftPatch, RootsPanelState } from './roots-panel-state.ts';
|
|
34
|
-
export * from './roots-panel-state.ts';
|
|
35
|
-
export * from './transfer-panel-view.ts';
|
|
36
|
-
/**
|
|
37
|
-
* Placement of the connection **edit dialog**'s portal.
|
|
38
|
-
*
|
|
39
|
-
* `CLASS.portal` is the fixed body-level container; `CLASS.portalTop` is the wider placement the other two
|
|
40
|
-
* dialogs share, which this form needs because it carries five fields plus the rename note.
|
|
41
|
-
*/
|
|
42
|
-
export declare const ROOTS_EDIT_PORTAL_CLASS: string;
|
|
43
|
-
/**
|
|
44
|
-
* The click sink for that card: the settings modal's own layers are 900/1001, so the sink sits at 1001 and the
|
|
45
|
-
* card above it (audit 2026-11-01b ui P2-1). The edit dialog is the case where a click *through* the gap used to
|
|
46
|
-
* be worst: the 「新增连接」 form behind it shares the screen, and its 保存 was reachable.
|
|
47
|
-
*/
|
|
48
|
-
export declare const ROOTS_EDIT_BACKDROP_CLASS: string;
|
|
49
|
-
/** The calls the section makes. `roots-panel.ts` owns how they are implemented. */
|
|
50
|
-
export interface RootsPanelController {
|
|
51
|
-
/** The current state; a stable reference until something changes (safe for `useSyncExternalStore`). */
|
|
52
|
-
readonly getState: () => RootsPanelState;
|
|
53
|
-
/** Watch for changes; returns the disposer. */
|
|
54
|
-
readonly subscribe: (listener: () => void) => () => void;
|
|
55
|
-
/** Ask for the first answer once; a remount does not re-fetch. */
|
|
56
|
-
readonly start: () => void;
|
|
57
|
-
/** Reload unconditionally (used after a successful change). */
|
|
58
|
-
readonly reload: () => Promise<void>;
|
|
59
|
-
/** Change one field of the **add connection** form. */
|
|
60
|
-
readonly changeDraft: (patch: RootsPanelDraftPatch) => void;
|
|
61
|
-
/**
|
|
62
|
-
* Change one field of the **edit dialog**.
|
|
63
|
-
*
|
|
64
|
-
* A separate writer because the two forms have separate drafts (`RootsPanelEditDrafts`): one set made opening
|
|
65
|
-
* the dialog overwrite the add form and cancelling it wipe it (audit 2026-11-01b ui P1-4).
|
|
66
|
-
*/
|
|
67
|
-
readonly changeEditDraft: (patch: RootsPanelEditDraftPatch) => void;
|
|
68
|
-
/** Validate and add a root of the given location. */
|
|
69
|
-
readonly submitRoot: (location: RootLocation) => Promise<void>;
|
|
70
|
-
/** Validate and add a connection. */
|
|
71
|
-
readonly submitConnection: () => Promise<void>;
|
|
72
|
-
/** Open the edit form for one connection, prefilled from the last host answer. */
|
|
73
|
-
readonly startEdit: (id: string) => void;
|
|
74
|
-
/** Close the edit form without saving. */
|
|
75
|
-
readonly cancelEdit: () => void;
|
|
76
|
-
/** Validate and save the edit form (a rename included). */
|
|
77
|
-
readonly submitEdit: () => Promise<void>;
|
|
78
|
-
/** Ask for confirmation before removing a connection (it takes its remote roots with it). */
|
|
79
|
-
readonly askRemoveConnection: (id: string) => void;
|
|
80
|
-
/** Dismiss the confirmation. */
|
|
81
|
-
readonly cancelRemoveConnection: () => void;
|
|
82
|
-
/** Remove a connection, once the confirmation was accepted. */
|
|
83
|
-
readonly confirmRemoveConnection: (id: string) => Promise<void>;
|
|
84
|
-
/** Remove one row, if the view allows it. */
|
|
85
|
-
readonly remove: (key: string) => Promise<void>;
|
|
86
|
-
/** Open the ssh-config import dialog and read the file (FR-2.1.4). */
|
|
87
|
-
readonly openImport: () => Promise<void>;
|
|
88
|
-
/** Re-read `~/.ssh/config`, replacing the candidate list. */
|
|
89
|
-
readonly reloadImport: () => Promise<void>;
|
|
90
|
-
/** Close the dialog. */
|
|
91
|
-
readonly closeImport: () => void;
|
|
92
|
-
/** Tick or untick one alias. */
|
|
93
|
-
readonly toggleImport: (alias: string) => void;
|
|
94
|
-
/** Tick every candidate. */
|
|
95
|
-
readonly selectAllImports: () => void;
|
|
96
|
-
/** Untick everything. */
|
|
97
|
-
readonly clearImports: () => void;
|
|
98
|
-
/** Import the ticked aliases. */
|
|
99
|
-
readonly submitImport: () => Promise<void>;
|
|
100
|
-
/**
|
|
101
|
-
* Tell the controller the settings surface is on screen again.
|
|
102
|
-
*
|
|
103
|
-
* Cancels a pending {@link RootsPanelController.surfaceDetached} — see there for why unmounting is not by
|
|
104
|
-
* itself "the page was closed".
|
|
105
|
-
*/
|
|
106
|
-
readonly surfaceAttached: () => void;
|
|
107
|
-
/**
|
|
108
|
-
* Tell the controller the settings surface went away.
|
|
109
|
-
*
|
|
110
|
-
* **Not** "close the editor": the platform re-registers this section whenever its inject face changes, and
|
|
111
|
-
* opening the editor is such a change, so the component unmounts and remounts as a normal part of opening
|
|
112
|
-
* it. Closing the settings page looks the same except that **nothing remounts**. The two are told apart in
|
|
113
|
-
* time: the clear is deferred by one task, and a remount cancels it (audit 0091).
|
|
114
|
-
*/
|
|
115
|
-
readonly surfaceDetached: () => void;
|
|
116
|
-
/** Stop accepting results and drop listeners. */
|
|
117
|
-
readonly dispose: () => void;
|
|
118
|
-
}
|
|
119
|
-
/** What a controller needs: the one call, plus the one scheduling choice. */
|
|
120
|
-
export interface RootsPanelControllerOptions {
|
|
121
|
-
readonly api: RootsApi;
|
|
122
|
-
/**
|
|
123
|
-
* Defer one task and return its canceller.
|
|
124
|
-
*
|
|
125
|
-
* Injectable so {@link RootsPanelController.surfaceDetached} is testable without timers: the policy it
|
|
126
|
-
* encodes ("did anything remount before this ran?") is the whole point, and a test that has to wait on a
|
|
127
|
-
* real `setTimeout` would be both slow and flaky.
|
|
128
|
-
*/
|
|
129
|
-
readonly defer?: (run: () => void) => () => void;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Build the stateful half of the panel.
|
|
133
|
-
*
|
|
134
|
-
* The refresh-after-success is the reason this exists at all: `POST` answers with the *persisted* state,
|
|
135
|
-
* whose `roots` are the user's roots - the same field name `GET` uses for the *config* roots. Rendering that
|
|
136
|
-
* answer directly would show a config root as a user root (or the reverse), so a success always ends with a
|
|
137
|
-
* fresh `GET` and the POST body is used only as a signal.
|
|
138
|
-
*
|
|
139
|
-
* @param options - the injected API.
|
|
140
|
-
* @returns the controller the container subscribes to.
|
|
141
|
-
*/
|
|
142
|
-
export declare function createRootsPanelController(options: RootsPanelControllerOptions): RootsPanelController;
|
|
143
|
-
/**
|
|
144
|
-
* The section's callbacks, derived from the controller.
|
|
145
|
-
*
|
|
146
|
-
* A factory rather than an inline object literal so the shell can `useMemo(() => rootsPanelSectionActions(controller),
|
|
147
|
-
* [controller])` — stable props are the precondition for ever putting `memo` on the two child components — **and**
|
|
148
|
-
* so "each callback really drives the controller" is a decision a test can hold on to (audit 2026-11-01b ui P3-7).
|
|
149
|
-
*
|
|
150
|
-
* @param controller - the panel's controller.
|
|
151
|
-
* @returns the callbacks the pure template calls.
|
|
152
|
-
*/
|
|
153
|
-
export declare function rootsPanelSectionActions(controller: RootsPanelController): RootsPanelSectionActions;
|
|
154
|
-
/** What the section's controls call. All of them are no-ops when the gate is closed. */
|
|
155
|
-
export interface RootsPanelSectionActions {
|
|
156
|
-
readonly onDraftChange: (patch: RootsPanelDraftPatch) => void;
|
|
157
|
-
/** One field of the edit dialog changed (its own draft set). */
|
|
158
|
-
readonly onEditDraftChange: (patch: RootsPanelEditDraftPatch) => void;
|
|
159
|
-
readonly onSubmitRoot: (location: RootLocation) => void;
|
|
160
|
-
readonly onSubmitConnection: () => void;
|
|
161
|
-
readonly onRemove: (key: string) => void;
|
|
162
|
-
readonly onSubmitEdit: () => void;
|
|
163
|
-
readonly onCancelEdit: () => void;
|
|
164
|
-
/** FR-2.1.4: the ssh-config import dialog's controls. */
|
|
165
|
-
readonly onImportOpen: () => void;
|
|
166
|
-
readonly onImportClose: () => void;
|
|
167
|
-
readonly onImportReload: () => void;
|
|
168
|
-
readonly onImportToggle: (alias: string) => void;
|
|
169
|
-
readonly onImportAll: () => void;
|
|
170
|
-
readonly onImportNone: () => void;
|
|
171
|
-
readonly onImportSubmit: () => void;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* The `~/.ssh/config` import dialog (FR-2.1.4).
|
|
175
|
-
*
|
|
176
|
-
* Three things it deliberately does:
|
|
177
|
-
*
|
|
178
|
-
* - **It shows the refusals.** Every alias the host would not offer is listed with the reason, because the
|
|
179
|
-
* user is looking at their own config file and can see the entry that is missing from the list. A silent
|
|
180
|
-
* subset reads as a broken feature.
|
|
181
|
-
* - **It shows the id each alias will become.** `deploy@bastion` is a valid alias and not a valid id, so the
|
|
182
|
-
* dialog says what the id will be instead of letting `add-connection` refuse it later.
|
|
183
|
-
* - **It never shows an identity path.** The host answers `hasIdentityFile` as a boolean; the path itself is
|
|
184
|
-
* not on the wire (the route's invariant 1), so nothing here can leak one.
|
|
185
|
-
*
|
|
186
|
-
* @param props - copy, state and callbacks.
|
|
187
|
-
* @returns the dialog, or nothing when it is closed.
|
|
188
|
-
*/
|
|
189
|
-
export declare function SshConfigImportDialog({ copy, state, actions, idPrefix }: RootsPanelSectionProps): ReactNode;
|
|
190
|
-
/**
|
|
191
|
-
* The **edit** form for one saved connection.
|
|
192
|
-
*
|
|
193
|
-
* It exists as its own component because it is the one form whose fields are prefilled from a host answer: the
|
|
194
|
-
* panel's top list renders the row buttons that open it, and this renders what those buttons open. Splitting
|
|
195
|
-
* them keeps `RootsPanelSection` about the roots it owns and this about one connection.
|
|
196
|
-
*
|
|
197
|
-
* Two rules it inherits and one it adds:
|
|
198
|
-
*
|
|
199
|
-
* - **Nothing is submitted before validation** ({@link validateConnectionEdit} is the only way in);
|
|
200
|
-
* - **no credential is ever rendered** - the password box is always empty, because no answer carries a
|
|
201
|
-
* secret back, and an empty box means "keep the stored password" (see the key's own note);
|
|
202
|
-
* - **the id is editable, and the rename is explained**: a rename re-keys the connection's remote roots and
|
|
203
|
-
* is therefore not a cosmetic field, so the form says so rather than leaving it to be discovered.
|
|
204
|
-
*
|
|
205
|
-
* @param props - copy, state and callbacks.
|
|
206
|
-
* @returns the form, or nothing when no connection is open for editing.
|
|
207
|
-
*/
|
|
208
|
-
export declare function ConnectionEditForm({ copy, state, actions, idPrefix }: RootsPanelSectionProps): ReactNode;
|
|
209
|
-
/** Props of the pure template: state in, callbacks out, no hooks and no store access. */
|
|
210
|
-
/**
|
|
211
|
-
* The `id` prefix the pure templates use when the shell does not pass one (direct template calls, tests).
|
|
212
|
-
*
|
|
213
|
-
* The shell always passes `useId()`'s value instead, so the ids are **per mount** — see
|
|
214
|
-
* {@link RootsPanelSectionProps.idPrefix}.
|
|
215
|
-
*/
|
|
216
|
-
export declare const DEFAULT_ROOTS_ID_PREFIX = "dsh-rw-roots";
|
|
217
|
-
export interface RootsPanelSectionProps {
|
|
218
|
-
readonly copy: RootsPanelCopy;
|
|
219
|
-
readonly state: RootsPanelState;
|
|
220
|
-
readonly actions: RootsPanelSectionActions;
|
|
221
|
-
/**
|
|
222
|
-
* `id` prefix for this mount's form controls (`{prefix}-edit-host`, `{prefix}-import-<alias>`, …).
|
|
223
|
-
*
|
|
224
|
-
* A module constant was enough while there was exactly one section on one page, and it stopped being enough
|
|
225
|
-
* the moment the platform started re-registering the section on every inject-face change: for one commit the
|
|
226
|
-
* old mount and the new one are both in the DOM, so two inputs share an `id` and every `<label for>` in the
|
|
227
|
-
* overlap points at whichever one the browser found first. `useId()` in the container makes that impossible.
|
|
228
|
-
*
|
|
229
|
-
* Optional only so the offline view tests can call these templates directly (audit 2026-10-31 client.md §2.4).
|
|
230
|
-
*/
|
|
231
|
-
readonly idPrefix?: string;
|
|
232
|
-
/**
|
|
233
|
-
* The body-level portal the connection **edit dialog** is rendered into.
|
|
234
|
-
*
|
|
235
|
-
* Optional on purpose: the pure template is called directly by the offline view tests (and by any caller
|
|
236
|
-
* without a DOM), where the card is rendered in place. The shell always passes one, so the real UI gets a
|
|
237
|
-
* card that floats over the settings page instead of being pushed below the "新增连接" form (audit 0088).
|
|
238
|
-
*/
|
|
239
|
-
readonly portal?: CardPortal;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Props of the whole settings section.
|
|
243
|
-
*
|
|
244
|
-
* It is {@link RootsPanelSectionProps} plus the one element that needs the container: the transfer progress
|
|
245
|
-
* panel. The remote file **tree** is not here at all: it was deleted (audit 0085) because remote paths are
|
|
246
|
-
* reached through the conversation, and because this function is called **directly** by the view tests,
|
|
247
|
-
* outside any React render, where a hook throws `Invalid hook call` (the constraint documented in
|
|
248
|
-
* `controls.ts`).
|
|
249
|
-
*/
|
|
250
|
-
export interface RootsPanelSectionViewProps extends RootsPanelSectionProps {
|
|
251
|
-
/** The transfer progress panel element, or `undefined` in a template-only render. */
|
|
252
|
-
readonly transfers?: ReactNode;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Render the editable section. Pure: calling it twice with the same props yields the same tree.
|
|
256
|
-
*
|
|
257
|
-
* @param props - copy, state and callbacks.
|
|
258
|
-
* @returns the section element.
|
|
259
|
-
*/
|
|
260
|
-
export declare function RootsPanelSection({ copy, state, actions, transfers, portal, idPrefix, }: RootsPanelSectionViewProps): ReactNode;
|
|
261
|
-
/** Props of the React shell: the controller owns the state, the shell only subscribes. */
|
|
262
|
-
export interface RootsPanelContainerProps {
|
|
263
|
-
readonly copy: RootsPanelCopy;
|
|
264
|
-
readonly controller: RootsPanelController;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Subscribe to a controller and render {@link RootsPanelSection}.
|
|
268
|
-
*
|
|
269
|
-
* `useSyncExternalStore` is the whole reason this is a component and not a function call: the slot's inject
|
|
270
|
-
* face is a synchronous snapshot, so the section has to be re-rendered by the store it reads.
|
|
271
|
-
*
|
|
272
|
-
* The right-pane entry point is the one piece of state this shell owns itself: whether the composition
|
|
273
|
-
* publishes the right sidebar is not a fact the controller's store carries (it is answered live by the entry
|
|
274
|
-
* point), and an open that failed after the button was enabled must say so somewhere. Both are re-read on
|
|
275
|
-
* every render, which is exactly when a `ctx.get` seam can have changed.
|
|
276
|
-
*
|
|
277
|
-
* @param props - the dictionary and the controller.
|
|
278
|
-
* @returns the section element.
|
|
279
|
-
*/
|
|
280
|
-
export declare function RootsPanelContainer({ copy, controller }: RootsPanelContainerProps): ReactNode;
|