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,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **directory-flow occupant**: "add a local or remote directory to a workspace".
|
|
3
|
+
*
|
|
4
|
+
* Both workspace surfaces of the platform declare one `single` hole - `sidebar.workspaces.directoryFlow` and
|
|
5
|
+
* `conversation.hero.workspace.directoryFlow` - and each renders its "add workspace" action **only while that
|
|
6
|
+
* hole is occupied** (a composition without a picker has no such action at all). This module fills both holes
|
|
7
|
+
* with one dialog offering two entries:
|
|
8
|
+
*
|
|
9
|
+
* - **local**: the platform's own chooser (`ctx.uiWorkspace.pickDirectory()`); the absolute path it answers
|
|
10
|
+
* is handed straight back through `onPicked`;
|
|
11
|
+
* - **remote**: a connection plus a remote path, POSTed to this plugin's own route (`add-root`); the host
|
|
12
|
+
* answers with the **anchor path** it registered for that root, and *that* path is what goes back through
|
|
13
|
+
* `onPicked`. The platform creates a workspace on a **host path**, and the router resolves a path under
|
|
14
|
+
* the anchor back to the remote root (`doc/02-platform/roots-http-route.md`), so the anchor - not the
|
|
15
|
+
* remote path - is the only answer that puts a remote directory in a workspace.
|
|
16
|
+
*
|
|
17
|
+
* Three layers, mirroring `roots-panel-view.ts`:
|
|
18
|
+
*
|
|
19
|
+
* 1. {@link workspaceDirectoryFlowReducer} - a pure state machine over the dialog's drafts, the connection
|
|
20
|
+
* list and the owner conversation. No DOM, no I/O, no React.
|
|
21
|
+
* 2. {@link createWorkspaceDirectoryFlow} - the same state plus the injected calls, the owner conversation
|
|
22
|
+
* and the **one result per open** gate.
|
|
23
|
+
* 3. {@link WorkspaceDirectoryFlowSection} - a pure template (`createElement`, never JSX: Node's native type
|
|
24
|
+
* stripping cannot transform JSX) and {@link WorkspaceDirectoryFlowContainer}, the thin React shell.
|
|
25
|
+
*
|
|
26
|
+
* Each entry also embeds the shared **directory browser** (`./directory-browser.ts`), so a path can be found
|
|
27
|
+
* by looking instead of typed from memory: both worlds come from this plugin's own read-only route
|
|
28
|
+
* (`list-local` / `list-remote`), never from the platform's `uiWorkspace.listDirectory()` — that call needs
|
|
29
|
+
* the directory-picker's `browse` capability and fails outright in a composition whose picker serves only the
|
|
30
|
+
* native dialog (see `doc/02-platform/roots-http-route.md` §2). `browse` in the state says which of the two
|
|
31
|
+
* browsers is open (never both: two listing panels in a ~260px column would each get half the height for no
|
|
32
|
+
* gain).
|
|
33
|
+
*
|
|
34
|
+
* Rules this file is responsible for:
|
|
35
|
+
*
|
|
36
|
+
* - **Exactly one outcome per open.** The owner contract says so; {@link canReport} is the single gate, and
|
|
37
|
+
* a settlement that arrives after the owner withdrew `open` (or after an earlier settlement) is dropped
|
|
38
|
+
* rather than reported.
|
|
39
|
+
* - **No submission while `busy`.** The owner is adopting a path; every commit affordance is disabled and
|
|
40
|
+
* every handler re-checks the gate, so a double submit is impossible even if a caller bypasses the view.
|
|
41
|
+
* - **Nothing is POSTed before validation.** `validateRootDraft` (the same authority the settings panel
|
|
42
|
+
* uses) is the only way a draft becomes a request, and its refusal is rendered through the dictionaries.
|
|
43
|
+
* - **No credentials.** The connection list comes from `parseRootsSnapshot`, which copies field by field.
|
|
44
|
+
* - **The card is portalled, and it has to be.** `sidebar.workspaces.directoryFlow` renders *inside the
|
|
45
|
+
* sidebar's title row* - a flex row that clips its overflow - so a card rendered in place is a flex item
|
|
46
|
+
* that gets shrunk to almost nothing (`max-width: 100%` does not stop flex shrinking and the row never
|
|
47
|
+
* wraps). The release before this one shipped exactly that and the user photographed the result: a
|
|
48
|
+
* vertical strip with a clipped hint. `./portal.ts` therefore attaches one container to `document.body`
|
|
49
|
+
* while the interaction is open, and the card lives in it with `position: fixed`. The **internals** stay
|
|
50
|
+
* narrow-first: one column by default, every cell capped at the container, committing buttons on their own
|
|
51
|
+
* row, no pixel widths, and the two-column shape only ever added by the card's own `@container` query.
|
|
52
|
+
*
|
|
53
|
+
* **Unverified** (see `doc/02-platform/workspace-directory-flow.md`): the portal has not been looked at in a
|
|
54
|
+
* real GUI, and it is not verified that the host accepts an anchor path as a workspace path.
|
|
55
|
+
*
|
|
56
|
+
* @module dsh-over-ssh/client/workspace-directory-flow
|
|
57
|
+
*/
|
|
58
|
+
import { type ReactNode } from 'react';
|
|
59
|
+
import { type DirectoryBrowserIo } from './directory-browser.ts';
|
|
60
|
+
import type { OverSshKey } from '../core/locale.ts';
|
|
61
|
+
import { type CardPortalTarget } from '../core/portal.ts';
|
|
62
|
+
import { type ApiResult, type ConnectionSummary, type FetchLike, type MessageKey } from '../roots/roots-panel.ts';
|
|
63
|
+
import { type RootsPanelCopy } from '../roots/roots-panel-view.ts';
|
|
64
|
+
/**
|
|
65
|
+
* The owner share of both directory-flow holes, quoted from the platform (`DirectoryFlowOwnerProps` in
|
|
66
|
+
* `@deepseek-ai/dsh-client-ui-workspace/client`, `packages/client/ui-workspace/src/client/contract/slots.ts`).
|
|
67
|
+
*
|
|
68
|
+
* Mirrored instead of imported: that package is not a dependency of this repository (its types are only in
|
|
69
|
+
* the DSH checkout), and the merge below has to compile here.
|
|
70
|
+
*/
|
|
71
|
+
export interface DirectoryFlowOwnerProps {
|
|
72
|
+
/** True while a picking interaction is requested; flipping back to false withdraws the request. */
|
|
73
|
+
open: boolean;
|
|
74
|
+
/** True while the owner adopts a picked path (`createWorkspace` in flight); occupants disable their commit affordances. */
|
|
75
|
+
busy: boolean;
|
|
76
|
+
/** The operator picked a directory (absolute host path); the owner adopts it. */
|
|
77
|
+
onPicked: (path: string) => void;
|
|
78
|
+
/** The operator dismissed the interaction; the owner just closes the flow. */
|
|
79
|
+
onCancel: () => void;
|
|
80
|
+
/** The interaction itself failed (chooser missing, listing denied); the owner shows its error surface. */
|
|
81
|
+
onError: (message: string) => void;
|
|
82
|
+
}
|
|
83
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
84
|
+
interface SlotMap {
|
|
85
|
+
/** Directory-flow hole under the conversation empty-state picker. */
|
|
86
|
+
'conversation.hero.workspace.directoryFlow': {
|
|
87
|
+
kind: 'single';
|
|
88
|
+
scope: 'root';
|
|
89
|
+
owner: DirectoryFlowOwnerProps;
|
|
90
|
+
};
|
|
91
|
+
/** Directory-flow hole under the sidebar browsing region. */
|
|
92
|
+
'sidebar.workspaces.directoryFlow': {
|
|
93
|
+
kind: 'single';
|
|
94
|
+
scope: 'root';
|
|
95
|
+
owner: DirectoryFlowOwnerProps;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The two holes, in registration order; one occupant fills both. */
|
|
100
|
+
export declare const WORKSPACE_DIRECTORY_FLOW_SLOTS: readonly ["sidebar.workspaces.directoryFlow", "conversation.hero.workspace.directoryFlow"];
|
|
101
|
+
/** One of the two holes. */
|
|
102
|
+
export type WorkspaceDirectoryFlowSlotName = (typeof WORKSPACE_DIRECTORY_FLOW_SLOTS)[number];
|
|
103
|
+
/**
|
|
104
|
+
* Shadowing rank of this occupant inside the single-cell hole.
|
|
105
|
+
*
|
|
106
|
+
* A `single` slot holds one entry **per priority**: a second registration at the same priority throws
|
|
107
|
+
* (`SlotCore.register`), while a different priority shadows. The platform ships a picker for this hole
|
|
108
|
+
* (`ui-directory-picker-native` / `-browse`) whose profile row mounts it at the default priority 0, so a
|
|
109
|
+
* plain registration would throw - or, worse, fail the whole client half - in exactly the compositions that
|
|
110
|
+
* already have a picker. `-1` renders lower (ascending: lowest renders) and therefore replaces the shipped
|
|
111
|
+
* picker instead of colliding with it. The local entry below still reaches the same platform chooser.
|
|
112
|
+
*/
|
|
113
|
+
export declare const WORKSPACE_DIRECTORY_FLOW_PRIORITY = -1;
|
|
114
|
+
/** The error {@link WorkspaceDirectoryFlowIo.addRemoteRoot} answers when the host did not report an anchor. */
|
|
115
|
+
export declare const NO_ANCHOR_ERROR = "no-anchor-path";
|
|
116
|
+
/**
|
|
117
|
+
* Classes of the body-level container this dialog's card is portalled into: the floating shell
|
|
118
|
+
* (`CLASS.portal`) plus the dialog's placement (`CLASS.portalTop`, centred below the top of the window).
|
|
119
|
+
*
|
|
120
|
+
* The container is created by {@link WorkspaceDirectoryFlowContainer} from `./portal.ts`; the template only
|
|
121
|
+
* receives it. Its width (`min(26rem, calc(100vw - 2rem))`) is what the card's own `@container` query measures.
|
|
122
|
+
*/
|
|
123
|
+
export declare const WORKSPACE_DIRECTORY_FLOW_PORTAL_CLASS: string;
|
|
124
|
+
/** The dialog's click sink: the sidebar dialog is modal, so the title row behind it is not clickable through the gap. */
|
|
125
|
+
export declare const WORKSPACE_DIRECTORY_FLOW_BACKDROP_CLASS: "dsh-rw-backdrop";
|
|
126
|
+
/** The dialog heading's `id` when the shell did not pass a per-mount one (direct template calls, tests). */
|
|
127
|
+
export declare const WORKSPACE_DIRECTORY_FLOW_TITLE_ID = "dsh-rw-flow-title";
|
|
128
|
+
/** One shipped dictionary of this plugin's namespace, as the template consumes it. */
|
|
129
|
+
export type WorkspaceDirectoryFlowCopy = RootsPanelCopy;
|
|
130
|
+
/** The remote form's contents. Flat, so a patch is one spread and a test can name a single field. */
|
|
131
|
+
export interface WorkspaceDirectoryFlowDrafts {
|
|
132
|
+
/** Connection the remote root belongs to. */
|
|
133
|
+
readonly connectionId: string;
|
|
134
|
+
/** Remote path as typed; validated by the controller so a bad value is refused, not coerced. */
|
|
135
|
+
readonly path: string;
|
|
136
|
+
}
|
|
137
|
+
/** A change to the drafts; every field is optional so one handler can serve every input. */
|
|
138
|
+
export type WorkspaceDirectoryFlowDraftPatch = Partial<WorkspaceDirectoryFlowDrafts>;
|
|
139
|
+
/** Everything the dialog renders from. */
|
|
140
|
+
export interface WorkspaceDirectoryFlowState {
|
|
141
|
+
/** The owner's `open`, as last synced. */
|
|
142
|
+
readonly open: boolean;
|
|
143
|
+
/** The owner's `busy`, as last synced. */
|
|
144
|
+
readonly busy: boolean;
|
|
145
|
+
/** Bumped on every rising `open` edge; every call and every settlement is addressed by it. */
|
|
146
|
+
readonly generation: number;
|
|
147
|
+
/** True once this generation reported its one outcome (or the owner withdrew the request). */
|
|
148
|
+
readonly settled: boolean;
|
|
149
|
+
/** True while one of the flow's own calls is in flight. */
|
|
150
|
+
readonly pending: boolean;
|
|
151
|
+
/** The connections the remote form offers. */
|
|
152
|
+
readonly connections: readonly ConnectionSummary[];
|
|
153
|
+
/** True while the connection list is being read (the first read of an open, or a retry). */
|
|
154
|
+
readonly connectionsLoading: boolean;
|
|
155
|
+
/** The last connection-list failure, kept so the window can be re-opened without a stale list. */
|
|
156
|
+
readonly connectionsError?: string;
|
|
157
|
+
/** The remote form's contents. */
|
|
158
|
+
readonly drafts: WorkspaceDirectoryFlowDrafts;
|
|
159
|
+
/** Which world the operator is adding a directory from. Only that world's block is rendered. */
|
|
160
|
+
readonly mode: WorkspaceDirectoryFlowMode;
|
|
161
|
+
/** The embedded remote browser; `'none'` while it is closed. */
|
|
162
|
+
readonly browse: WorkspaceDirectoryFlowBrowse;
|
|
163
|
+
/** The last refusal or call failure, rendered through the dictionaries. */
|
|
164
|
+
readonly notice?: MessageKey;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Which world this dialog is adding from.
|
|
168
|
+
*
|
|
169
|
+
* The two entries used to be shown together with one commit button each, which read as two competing ways to
|
|
170
|
+
* add a directory. The dialog now asks **first**: the operator picks local or remote, and only that block is
|
|
171
|
+
* rendered. Both blocks still end in the same single owner report (`onPicked`).
|
|
172
|
+
*/
|
|
173
|
+
export type WorkspaceDirectoryFlowMode = 'local' | 'remote';
|
|
174
|
+
/**
|
|
175
|
+
* Which embedded browser is open.
|
|
176
|
+
*
|
|
177
|
+
* Only the **remote** block embeds one: local picking goes through the platform's native chooser (audit 0071),
|
|
178
|
+
* so `'local'` no longer exists. `'none'` is the state a fresh open starts in.
|
|
179
|
+
*/
|
|
180
|
+
export type WorkspaceDirectoryFlowBrowse = 'none' | 'remote';
|
|
181
|
+
/** What the reducer accepts. */
|
|
182
|
+
export type WorkspaceDirectoryFlowAction =
|
|
183
|
+
/** The owner asked for the interaction (an edge: `open` false -> true). */
|
|
184
|
+
{
|
|
185
|
+
readonly type: 'opened';
|
|
186
|
+
}
|
|
187
|
+
/** The owner withdrew the request without an outcome; nothing may be reported for that generation. */
|
|
188
|
+
| {
|
|
189
|
+
readonly type: 'withdrawn';
|
|
190
|
+
}
|
|
191
|
+
/** The owner's adoption flag moved. */
|
|
192
|
+
| {
|
|
193
|
+
readonly type: 'busy-changed';
|
|
194
|
+
readonly busy: boolean;
|
|
195
|
+
}
|
|
196
|
+
/** A connection-list read started (the first read of an open, or a retry). */
|
|
197
|
+
| {
|
|
198
|
+
readonly type: 'connections-loading';
|
|
199
|
+
}
|
|
200
|
+
/** The connection list arrived. */
|
|
201
|
+
| {
|
|
202
|
+
readonly type: 'connections-loaded';
|
|
203
|
+
readonly connections: readonly ConnectionSummary[];
|
|
204
|
+
}
|
|
205
|
+
/** The connection list failed. */
|
|
206
|
+
| {
|
|
207
|
+
readonly type: 'connections-failed';
|
|
208
|
+
readonly error: string;
|
|
209
|
+
}
|
|
210
|
+
/** One draft field changed: clears the inline message, because the user is fixing it. */
|
|
211
|
+
| {
|
|
212
|
+
readonly type: 'draft-changed';
|
|
213
|
+
readonly patch: WorkspaceDirectoryFlowDraftPatch;
|
|
214
|
+
}
|
|
215
|
+
/** The operator switched between adding a local and a remote directory. */
|
|
216
|
+
| {
|
|
217
|
+
readonly type: 'mode-changed';
|
|
218
|
+
readonly mode: WorkspaceDirectoryFlowMode;
|
|
219
|
+
}
|
|
220
|
+
/** The embedded remote browser was opened or closed. */
|
|
221
|
+
| {
|
|
222
|
+
readonly type: 'browse-toggled';
|
|
223
|
+
readonly browse: WorkspaceDirectoryFlowBrowse;
|
|
224
|
+
}
|
|
225
|
+
/** A call is in flight; the reducer itself refuses it when the gate is closed. */
|
|
226
|
+
| {
|
|
227
|
+
readonly type: 'submitted';
|
|
228
|
+
}
|
|
229
|
+
/** A draft was refused before any call; nothing was sent. */
|
|
230
|
+
| {
|
|
231
|
+
readonly type: 'refused';
|
|
232
|
+
readonly message: MessageKey;
|
|
233
|
+
}
|
|
234
|
+
/** One outcome was reported to the owner. */
|
|
235
|
+
| {
|
|
236
|
+
readonly type: 'settled';
|
|
237
|
+
}
|
|
238
|
+
/** The native chooser was dismissed: the dialog stays open and usable, nothing is reported. */
|
|
239
|
+
| {
|
|
240
|
+
readonly type: 'pick-cancelled';
|
|
241
|
+
}
|
|
242
|
+
/** Reporting threw before the owner adopted; the dialog must become usable again. */
|
|
243
|
+
| {
|
|
244
|
+
readonly type: 'report-failed';
|
|
245
|
+
};
|
|
246
|
+
/** The empty remote form. */
|
|
247
|
+
export declare function emptyWorkspaceDirectoryFlowDrafts(): WorkspaceDirectoryFlowDrafts;
|
|
248
|
+
/** @returns the state a freshly created flow holds. */
|
|
249
|
+
export declare function initialWorkspaceDirectoryFlowState(): WorkspaceDirectoryFlowState;
|
|
250
|
+
/**
|
|
251
|
+
* The dialog's state machine. Pure: same state + same action => same result, and an action that changes
|
|
252
|
+
* nothing returns the **same object**, so a subscriber is never woken for a no-op.
|
|
253
|
+
*
|
|
254
|
+
* @param state - the current state.
|
|
255
|
+
* @param action - what happened.
|
|
256
|
+
* @returns the next state.
|
|
257
|
+
*/
|
|
258
|
+
export declare function workspaceDirectoryFlowReducer(state: WorkspaceDirectoryFlowState, action: WorkspaceDirectoryFlowAction): WorkspaceDirectoryFlowState;
|
|
259
|
+
/** The dialog's complete render input. */
|
|
260
|
+
export interface WorkspaceDirectoryFlowView {
|
|
261
|
+
/** True while the owner asks for the interaction. */
|
|
262
|
+
readonly visible: boolean;
|
|
263
|
+
/** True while one of the flow's own calls is in flight. */
|
|
264
|
+
readonly pending: boolean;
|
|
265
|
+
/** True while the owner adopts a picked path. */
|
|
266
|
+
readonly busy: boolean;
|
|
267
|
+
/** True once this generation reported its outcome. */
|
|
268
|
+
readonly settled: boolean;
|
|
269
|
+
/** Which world is being added from; only that block is rendered. */
|
|
270
|
+
readonly mode: WorkspaceDirectoryFlowMode;
|
|
271
|
+
/** May the interaction be driven (mode switch, local browser, remote form)? */
|
|
272
|
+
readonly canPickLocal: boolean;
|
|
273
|
+
/** May the remote form be submitted? */
|
|
274
|
+
readonly canSubmitRemote: boolean;
|
|
275
|
+
/** May the interaction be dismissed? (Never while the owner is adopting.) */
|
|
276
|
+
readonly canCancel: boolean;
|
|
277
|
+
/** The connections the remote form offers. */
|
|
278
|
+
readonly connections: readonly ConnectionSummary[];
|
|
279
|
+
/** True while the connection list is being read. */
|
|
280
|
+
readonly connectionsLoading: boolean;
|
|
281
|
+
/** Which embedded browser is open. */
|
|
282
|
+
readonly browse: WorkspaceDirectoryFlowBrowse;
|
|
283
|
+
/** The last refusal or call failure. */
|
|
284
|
+
readonly notice?: MessageKey;
|
|
285
|
+
/** The last connection-list failure. */
|
|
286
|
+
readonly connectionsError?: string;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Turn the state into render input.
|
|
290
|
+
*
|
|
291
|
+
* @param state - the flow state.
|
|
292
|
+
* @returns the visible flags, the connection list, the browse mode, the messages and the three gates.
|
|
293
|
+
*/
|
|
294
|
+
export declare function selectWorkspaceDirectoryFlowView(state: WorkspaceDirectoryFlowState): WorkspaceDirectoryFlowView;
|
|
295
|
+
/** True when the interaction may still be driven: open, not adopting, no call in flight, nothing reported. */
|
|
296
|
+
export declare function canPickLocal(state: WorkspaceDirectoryFlowState): boolean;
|
|
297
|
+
/** True when the remote form is complete enough to submit (the path rules are checked on submit). */
|
|
298
|
+
export declare function canSubmitRemote(state: WorkspaceDirectoryFlowState): boolean;
|
|
299
|
+
/**
|
|
300
|
+
* True when `cancel()` may report the dismissal.
|
|
301
|
+
*
|
|
302
|
+
* A request in flight (`pending`) is not cancellable either: the host may already have written the root (or
|
|
303
|
+
* the chooser may already have answered), and a dismissal reported at that moment would leave a root the
|
|
304
|
+
* owner never hears about. The late answer is dropped by {@link canReport} instead, so the caller always
|
|
305
|
+
* settles exactly once — which is why the race cannot happen at all rather than being repaired afterwards.
|
|
306
|
+
*/
|
|
307
|
+
export declare function canCancel(state: WorkspaceDirectoryFlowState): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* True when a settlement may still be reported to the owner: this generation is open and nothing was
|
|
310
|
+
* reported for it yet. The one gate behind the owner contract's "exactly one outcome per open".
|
|
311
|
+
*
|
|
312
|
+
* @param state - the flow state.
|
|
313
|
+
* @returns whether a result may be reported now.
|
|
314
|
+
*/
|
|
315
|
+
export declare function canReport(state: WorkspaceDirectoryFlowState): boolean;
|
|
316
|
+
/**
|
|
317
|
+
* The calls the flow makes. All of them are injected, so the state machine is testable offline
|
|
318
|
+
* (红线 3) and no test starts a server or opens a chooser.
|
|
319
|
+
*
|
|
320
|
+
* The browser's calls are part of the same face ({@link DirectoryBrowserIo}): both worlds embed the same
|
|
321
|
+
* browser, and giving it a second injection path would mean two places to keep the route, the credentials
|
|
322
|
+
* filter and the failure vocabulary identical.
|
|
323
|
+
*/
|
|
324
|
+
export interface WorkspaceDirectoryFlowIo extends DirectoryBrowserIo {
|
|
325
|
+
/**
|
|
326
|
+
* Open the platform's **native** directory chooser and answer the absolute path the operator picked, or
|
|
327
|
+
* `null` when they dismissed it. This is the local block's one entry (audit 0071): the OS dialog is what
|
|
328
|
+
* users mean by「浏览目录」, and it can reach drives the embedded browser cannot.
|
|
329
|
+
*/
|
|
330
|
+
pickDirectory(): Promise<string | null>;
|
|
331
|
+
/**
|
|
332
|
+
* Add one remote root and answer the **anchor path** the host registered for it.
|
|
333
|
+
* `{ ok: false, error: NO_ANCHOR_ERROR }` means the host answered without one, which is not a path the
|
|
334
|
+
* owner may create a workspace on.
|
|
335
|
+
*/
|
|
336
|
+
addRemoteRoot(input: {
|
|
337
|
+
readonly connectionId: string;
|
|
338
|
+
readonly path: string;
|
|
339
|
+
}): Promise<ApiResult<string>>;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Bind the flow's calls to the browser and the platform.
|
|
343
|
+
*
|
|
344
|
+
* The connection list and both listings come from {@link createDirectoryBrowserIo}, i.e. from the plugin's
|
|
345
|
+
* own `GET` and its own `list-local` / `list-remote` actions; `addRemoteRoot` posts the same `add-root` body
|
|
346
|
+
* the settings panel posts, but reads the answer's `anchorPath`, which the snapshot projection drops because
|
|
347
|
+
* it belongs to the *live* root, not to the persisted entry.
|
|
348
|
+
*
|
|
349
|
+
* @param options - `fetch` (same-origin in the app) and a route override.
|
|
350
|
+
* @returns the calls the flow uses.
|
|
351
|
+
*/
|
|
352
|
+
export declare function createWorkspaceDirectoryFlowIo(options: {
|
|
353
|
+
readonly fetchImpl: FetchLike;
|
|
354
|
+
readonly path?: string;
|
|
355
|
+
/** The platform's native chooser; the entry (`index.ts`) reads it from `uiWorkspace` lazily. */
|
|
356
|
+
readonly pickDirectory: () => Promise<string | null>;
|
|
357
|
+
}): WorkspaceDirectoryFlowIo;
|
|
358
|
+
/**
|
|
359
|
+
* What the stateful half needs: the injected calls plus the active dictionary, read at report time.
|
|
360
|
+
*
|
|
361
|
+
* The dialog deliberately has **no session-mode seam** (0069): the session this flow starts runs the Host's
|
|
362
|
+
* new-task default, which the platform's own Settings → Agent Preset page controls for every new session. A
|
|
363
|
+
* per-workspace preset choice was a fragile, silently-failing duplicate of that global setting.
|
|
364
|
+
*/
|
|
365
|
+
export interface WorkspaceDirectoryFlowDeps {
|
|
366
|
+
readonly io: WorkspaceDirectoryFlowIo;
|
|
367
|
+
/** The dictionary of the active locale. A thunk, because `onError` needs text and the locale can change. */
|
|
368
|
+
readonly copy: () => WorkspaceDirectoryFlowCopy;
|
|
369
|
+
}
|
|
370
|
+
/** The calls the container makes. `createWorkspaceDirectoryFlow` owns how they are implemented. */
|
|
371
|
+
export interface WorkspaceDirectoryFlowController {
|
|
372
|
+
/** The current state; a stable reference until something changes (safe for `useSyncExternalStore`). */
|
|
373
|
+
readonly getState: () => WorkspaceDirectoryFlowState;
|
|
374
|
+
/** Watch for changes; returns the disposer. */
|
|
375
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
376
|
+
/** Push the owner's current conversation in; called by the shell on every render. */
|
|
377
|
+
readonly syncOwner: (props: DirectoryFlowOwnerProps) => void;
|
|
378
|
+
/** Change one draft field. */
|
|
379
|
+
readonly changeDraft: (patch: WorkspaceDirectoryFlowDraftPatch) => void;
|
|
380
|
+
/** Switch between adding a local and a remote directory (only the selected block is rendered). */
|
|
381
|
+
readonly changeMode: (mode: WorkspaceDirectoryFlowMode) => void;
|
|
382
|
+
/** Validate the remote form, add the root and report its anchor path. */
|
|
383
|
+
readonly submitRemote: () => Promise<void>;
|
|
384
|
+
/** Re-read the connection list while the dialog stays open (the inline retry). */
|
|
385
|
+
readonly reloadConnections: () => void;
|
|
386
|
+
/** Report the operator's dismissal. */
|
|
387
|
+
readonly cancel: () => void;
|
|
388
|
+
/** Open or close the embedded remote browser. */
|
|
389
|
+
readonly toggleBrowse: (browse: WorkspaceDirectoryFlowBrowse) => void;
|
|
390
|
+
/** Open the platform's native chooser; its answer is validated and reported exactly like any other pick. */
|
|
391
|
+
readonly pickLocal: () => Promise<void>;
|
|
392
|
+
/** Adopt an already-absolute local path (the chooser's answer without the chooser: tests and fallbacks). */
|
|
393
|
+
readonly browseLocal: (path: string) => void;
|
|
394
|
+
/** Adopt a directory picked in the remote browser: fill the draft from it, then add the root. */
|
|
395
|
+
readonly browseRemote: (path: string, connectionId: string | undefined) => void;
|
|
396
|
+
/** Re-arm after a mount (React's development replay runs the cleanup once per lifetime). */
|
|
397
|
+
readonly activate: () => void;
|
|
398
|
+
/** Stop accepting results and drop listeners. */
|
|
399
|
+
readonly dispose: () => void;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Build the stateful half of the directory flow.
|
|
403
|
+
*
|
|
404
|
+
* The one gate that matters lives here: {@link canReport} is consulted **before** the owner is told anything,
|
|
405
|
+
* so a call that settles after the owner withdrew `open` - or a second settlement racing the first - is
|
|
406
|
+
* dropped instead of driving `onPicked` / `onCancel` / `onError` twice.
|
|
407
|
+
*
|
|
408
|
+
* @param deps - the injected calls and the active dictionary.
|
|
409
|
+
* @returns the controller the container subscribes to.
|
|
410
|
+
*/
|
|
411
|
+
export declare function createWorkspaceDirectoryFlow(deps: WorkspaceDirectoryFlowDeps): WorkspaceDirectoryFlowController;
|
|
412
|
+
/** What the dialog's controls call. All of them are no-ops when the gate is closed. */
|
|
413
|
+
export interface WorkspaceDirectoryFlowSectionActions {
|
|
414
|
+
readonly onDraftChange: (patch: WorkspaceDirectoryFlowDraftPatch) => void;
|
|
415
|
+
/** The world switch moved (local or remote). */
|
|
416
|
+
readonly onWorldChange: (mode: WorkspaceDirectoryFlowMode) => void;
|
|
417
|
+
readonly onSubmitRemote: () => void;
|
|
418
|
+
/** Re-read the connection list after an inline failure. */
|
|
419
|
+
readonly onReloadConnections: () => void;
|
|
420
|
+
readonly onCancel: () => void;
|
|
421
|
+
/** Open or close the embedded remote browser. */
|
|
422
|
+
readonly onBrowseToggle: (browse: WorkspaceDirectoryFlowBrowse) => void;
|
|
423
|
+
/** Open the platform's native folder chooser; its answer is adopted as the workspace path. */
|
|
424
|
+
readonly onPickLocal: () => void;
|
|
425
|
+
/** A directory was picked in the remote browser: fill the path field, then let the form submit. */
|
|
426
|
+
readonly onBrowseRemote: (path: string, connectionId: string | undefined) => void;
|
|
427
|
+
}
|
|
428
|
+
/** Props of the pure template: state in, callbacks out, no hooks and no store access. */
|
|
429
|
+
export interface WorkspaceDirectoryFlowSectionProps {
|
|
430
|
+
readonly copy: WorkspaceDirectoryFlowCopy;
|
|
431
|
+
readonly state: WorkspaceDirectoryFlowState;
|
|
432
|
+
readonly actions: WorkspaceDirectoryFlowSectionActions;
|
|
433
|
+
/** The browser's injected calls; the same face as {@link WorkspaceDirectoryFlowIo}, narrower. */
|
|
434
|
+
readonly browseIo: DirectoryBrowserIo;
|
|
435
|
+
/**
|
|
436
|
+
* The body-level portal the card is mounted into. Absent renders the card **in place**, which is the
|
|
437
|
+
* DOM-less degradation (SSR, a direct template call): there is no layout to escape in a runtime without a
|
|
438
|
+
* document. The shell always passes it in a browser.
|
|
439
|
+
*/
|
|
440
|
+
readonly portal?: CardPortalTarget | undefined;
|
|
441
|
+
/**
|
|
442
|
+
* `id` prefix of this mount's own controls; the shell passes a per-mount value so two mounted occupants
|
|
443
|
+
* cannot produce duplicate DOM ids (`flow-session-mode`, the remote fields, the embedded browsers).
|
|
444
|
+
*/
|
|
445
|
+
readonly idPrefix: string;
|
|
446
|
+
/** `id` of the dialog's heading, referenced by `aria-labelledby`; the shell passes a per-mount id. */
|
|
447
|
+
readonly titleId?: string | undefined;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Render the directory flow. Pure: calling it twice with the same props yields the same tree, and a closed
|
|
451
|
+
* flow renders nothing (the owner keeps the occupant mounted and drives it through `open`).
|
|
452
|
+
*
|
|
453
|
+
* @param props - copy, state, callbacks, the portal and the heading's id.
|
|
454
|
+
* @returns the dialog element (inside the portal when one is given), or `null` while closed.
|
|
455
|
+
*/
|
|
456
|
+
export declare function WorkspaceDirectoryFlowSection({ copy, state, actions, browseIo, portal, idPrefix, titleId, }: WorkspaceDirectoryFlowSectionProps): ReactNode;
|
|
457
|
+
/** Props of the React shell: the injected face plus the owner's conversation, which arrives as normal props. */
|
|
458
|
+
export interface WorkspaceDirectoryFlowContainerProps extends DirectoryFlowOwnerProps {
|
|
459
|
+
/** The active locale's dictionary; the shell resolves its own dynamic keys through it. */
|
|
460
|
+
readonly copy: WorkspaceDirectoryFlowCopy;
|
|
461
|
+
/** The injected calls. Created once by the entry (`index.ts`), never by the component. */
|
|
462
|
+
readonly io: WorkspaceDirectoryFlowIo;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Subscribe to a per-mount controller and render {@link WorkspaceDirectoryFlowSection}.
|
|
466
|
+
*
|
|
467
|
+
* One controller **per mount**, not per plugin load: the two holes are two independent surfaces with two
|
|
468
|
+
* independent owner conversations, and both can be mounted at the same time.
|
|
469
|
+
*
|
|
470
|
+
* The card is **portalled**: one container per mount (created from the injected document, attached to
|
|
471
|
+
* `document.body` while the interaction is open, removed again when it closes), because the sidebar hole sits
|
|
472
|
+
* in a clipping flex row that shrinks an in-place card to a vertical strip. Opening also moves focus into the
|
|
473
|
+
* card and remembers what held it, so Esc reaches the card and closing hands focus back to the trigger.
|
|
474
|
+
*
|
|
475
|
+
* @param props - the owner conversation plus the injected dictionary and calls.
|
|
476
|
+
* @returns the dialog element, or `null` while the owner has not asked for it.
|
|
477
|
+
*/
|
|
478
|
+
export declare function WorkspaceDirectoryFlowContainer(props: WorkspaceDirectoryFlowContainerProps): ReactNode;
|
|
479
|
+
/** The dictionary keys this module renders, so a test can assert the two locales carry them without a second list. */
|
|
480
|
+
export type WorkspaceDirectoryFlowKey = Extract<OverSshKey, 'flowTitle' | 'flowDescription' | 'flowLocalHeading' | 'flowLocalHint' | 'flowRemoteHeading' | 'flowRemoteHint' | 'flowConfirmRemote' | 'flowCancel' | 'flowPending' | 'flowBusy' | 'flowNoConnections' | 'flowConnectionsLoading' | 'flowNoAnchor' | 'flowUnavailable' | 'flowPickFailed' | 'browseOpenLocal' | 'browseOpenRemote' | 'browseChoose' | 'close' | 'rootsConnection' | 'rootsConnectionPlaceholder' | 'rootsRemotePathLabel'>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web client half: one settings section that explains how this plugin chooses a transport, the connection
|
|
3
|
+
* list once the host answers, the editable remote-workspaces editor (`src/client/roots-panel-view.ts`) that
|
|
4
|
+
* adds and removes roots and connections through the host's same-origin `/api/over-ssh/roots` route,
|
|
5
|
+
* the **remote-files tab** (`src/client/roots/remote-files-tab.ts`) that the editor opens into the sidebar's
|
|
6
|
+
* right column,
|
|
7
|
+
* the **directory-flow occupant** (`src/client/workspace-directory-flow.ts`) that both workspace surfaces
|
|
8
|
+
* render their "add workspace" action from, and the **session entry**
|
|
9
|
+
* (`src/client/session-directory.ts`) that adds a directory to the workspace a session is already in,
|
|
10
|
+
* without creating a workspace.
|
|
11
|
+
*
|
|
12
|
+
* Both browsers of the flow and the session entry are the same component
|
|
13
|
+
* (`src/client/directory-browser.ts`): local levels come from this plugin's own read-only `list-local`
|
|
14
|
+
* action and remote levels from its `list-remote` action, both through the same same-origin route. The
|
|
15
|
+
* platform's `uiWorkspace.listDirectory()` is deliberately **not** used: it needs the directory-picker's
|
|
16
|
+
* `browse` capability, which a profile whose composed picker serves only the native dialog does not have
|
|
17
|
+
* (see `doc/02-platform/roots-http-route.md` §2).
|
|
18
|
+
*
|
|
19
|
+
* Deliberately small, but structurally complete:
|
|
20
|
+
*
|
|
21
|
+
* - dictionaries are registered through the **typed** `ctx.locale.register` (both shipped locales required);
|
|
22
|
+
* - the section is registered through `ctx.slots.register` inside `ctx.effect`, so unloading the plugin
|
|
23
|
+
* removes it;
|
|
24
|
+
* - the directory flow and the session entry go through `ctx.slots.inject` instead, because their holes
|
|
25
|
+
* belong to other entries (the workspace surfaces, the composer), which may activate before or after this
|
|
26
|
+
* plugin and whose absence must not fail the rest of this half;
|
|
27
|
+
* - the panel receives its data through the registration's `inject` face, never through `ctx` (components
|
|
28
|
+
* cannot see `ctx` — platform contract);
|
|
29
|
+
* - the section re-registers on `locale/change`, because registrant options carry localized text and the
|
|
30
|
+
* shell does not subscribe to locale state on the registrant's behalf; the same re-registration is what
|
|
31
|
+
* brings the first host rows onto the screen (the slot's inject face is a synchronous snapshot, so new
|
|
32
|
+
* data needs a fresh registration until an observable source replaces it);
|
|
33
|
+
* - the host channel is mounted here, by this plugin: `api-remotes` only mounts the packages it lists, and a
|
|
34
|
+
* third-party half is not one of them. `remote.<namespace>` is therefore **not** in `inject` — waiting for
|
|
35
|
+
* a service this plugin creates itself would deadlock activation;
|
|
36
|
+
* - the **right sidebar** is an optional seam like `uiWorkspace`: its two services are read through
|
|
37
|
+
* `ctx.get`, never awaited, so a profile without that column loses one tab and nothing else.
|
|
38
|
+
*
|
|
39
|
+
* @module dsh-over-ssh/client
|
|
40
|
+
*/
|
|
41
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
42
|
+
/** Client services this half needs. `remote` is injected; `remote.sshdev` is mounted by `apply` below. */
|
|
43
|
+
export declare const inject: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Install the client half.
|
|
46
|
+
*
|
|
47
|
+
* @param ctx - client plugin context (has `slots`, `locale` and `remote` by injection).
|
|
48
|
+
*/
|
|
49
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-half copy for the `ssh-dev` 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-remote-workspace/client/locale
|
|
10
|
+
*/
|
|
11
|
+
/** Namespace owned by this plugin's client half. */
|
|
12
|
+
export declare const SSH_DEV_NS = "ssh-dev";
|
|
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 SshDevKey = 'sectionTitle' | 'sectionDescription' | 'legendHeading' | 'legendAlias' | 'legendSsh2' | 'legendAgent' | 'noHostData' | 'close' | 'rootsTitle' | 'rootsDescription' | 'rootsEmpty' | 'rootsPending' | 'rootsAddLocal' | 'rootsAddRemote' | 'rootsAddConnection' | 'rootsRemove' | 'rootsSave' | 'rootsConnection' | 'rootsConnectionPlaceholder' | 'rootsPathLabel' | 'rootsRemotePathLabel' | 'rootsLabelLabel' | 'rootsIdLabel' | 'rootsHostLabel' | 'rootsUserLabel' | 'rootsAuthLabel' | 'rootsAuthAgent' | 'rootsAuthKey' | 'rootsKeyPathLabel' | 'rootsPortLabel' | 'rootsBadgeLocal' | 'rootsBadgeRemote' | 'rootsErrorPathRequired' | 'rootsErrorRemotePath' | 'rootsErrorConnectionRequired' | 'rootsErrorLocalPath' | 'rootsErrorDuplicate' | 'rootsErrorIdRequired' | 'rootsErrorIdInvalid' | 'rootsErrorIdDuplicate' | 'rootsErrorHostRequired' | 'rootsErrorUserRequired' | 'rootsErrorPort' | 'rootsErrorKeyPathRequired' | 'rootsErrorPasswordNotAllowed' | 'rootsNoticeUnavailable' | 'rootsNoticeWarnings' | 'flowTitle' | 'flowDescription' | 'flowLocalHeading' | 'flowLocalHint' | 'flowPickLocal' | 'flowRemoteHeading' | 'flowRemoteHint' | 'flowConfirmRemote' | 'flowCancel' | 'flowPending' | 'flowBusy' | 'flowNoConnections' | 'flowNoAnchor' | 'flowUnavailable';
|
|
21
|
+
/** Merge this plugin's namespace into the shared locale map, so `t('ssh-dev.…')` is typed. */
|
|
22
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
23
|
+
interface LocaleNamespaceMap {
|
|
24
|
+
'ssh-dev': SshDevKey;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Every shipped locale's dictionary. Both `zh` and `en` are required by the runtime. */
|
|
28
|
+
export declare const SSH_DEV_DICTS: Record<'zh' | 'en', Record<SshDevKey, string>>;
|
|
29
|
+
/** Dictionary keys, so a completeness test never has to restate them. */
|
|
30
|
+
export declare function sshDevKeys(): readonly SshDevKey[];
|
|
31
|
+
//# sourceMappingURL=locale.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* @module dsh-remote-workspace/client/panel-model
|
|
10
|
+
*/
|
|
11
|
+
import type { SshDevKey } from './locale.ts';
|
|
12
|
+
/** One rendered row: a title and its explanation, already localized. */
|
|
13
|
+
export interface PanelRow {
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly body: string;
|
|
16
|
+
}
|
|
17
|
+
/** What the panel renders. */
|
|
18
|
+
export interface PanelModel {
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly legendHeading: string;
|
|
22
|
+
readonly legend: readonly PanelRow[];
|
|
23
|
+
/** Notice about the not-yet-wired host data channel; empty when host data is available. */
|
|
24
|
+
readonly notice: string;
|
|
25
|
+
readonly closeLabel: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the panel model from one locale's dictionary.
|
|
29
|
+
*
|
|
30
|
+
* @param copy - the dictionary the caller resolved (`zh` or `en`, chosen by the locale runtime).
|
|
31
|
+
* @param options - `hasHostData` flips the notice off once the host channel exists.
|
|
32
|
+
* @returns everything the component needs, with no further lookups.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildPanelModel(copy: Record<SshDevKey, string>, options?: {
|
|
35
|
+
readonly hasHostData?: boolean;
|
|
36
|
+
}): PanelModel;
|
|
37
|
+
//# sourceMappingURL=panel-model.d.ts.map
|