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,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **directory browser**: pick a directory by looking at it, on this machine or on a connection.
|
|
3
|
+
*
|
|
4
|
+
* Why a browser at all: the platform's own chooser (`ctx.uiWorkspace.pickDirectory()`) is a **native**
|
|
5
|
+
* dialog, and the panel's remote form asked for a path typed from memory. The client asked for browsing
|
|
6
|
+
* ("添加工作区应该要添加浏览目录的功能"), and **both** worlds now come from this plugin's own read-only route:
|
|
7
|
+
*
|
|
8
|
+
* - **local** listings come from `POST { action: 'list-local', path? }`, which the host answers with the
|
|
9
|
+
* absolute, canonical path it resolved, the breadcrumb chain to it, and `{ name, kind }` rows. The
|
|
10
|
+
* platform's `uiWorkspace.listDirectory()` is **not** used: its Host half needs the directory-picker's
|
|
11
|
+
* `browse` capability, and a profile whose composed picker serves only the native dialog fails the call
|
|
12
|
+
* outright ("directoryPicker.list needs the browse capability; the composed picker serves \"native\"").
|
|
13
|
+
* The host process always has `node:fs`, so browsing this machine is one action of our own route;
|
|
14
|
+
* - **remote** listings come from `POST { action: 'list-remote', connectionId, path }`, answered with
|
|
15
|
+
* `{ name, kind }` rows only. The client never composes an SSH command or a shell script: a browser that
|
|
16
|
+
* built its own `ls` would be a second, unaudited path to the remote host, and it would be the client that
|
|
17
|
+
* decides what a "directory" is.
|
|
18
|
+
*
|
|
19
|
+
* The platform chooser is still a second way in, but it is a **different surface**: the dialog around this
|
|
20
|
+
* browser keeps its own "pick a folder" button (`ctx.uiWorkspace.pickDirectory()`), and this module never
|
|
21
|
+
* touches it.
|
|
22
|
+
*
|
|
23
|
+
* Three layers, the same split every other surface here uses (`workspace-directory-flow.ts`):
|
|
24
|
+
*
|
|
25
|
+
* 1. {@link directoryBrowserReducer} - pure state machine over one listing level.
|
|
26
|
+
* 2. {@link createDirectoryBrowser} - the same state plus the injected calls and a request token, so an
|
|
27
|
+
* answer for a directory the user already left cannot overwrite the current one.
|
|
28
|
+
* 3. {@link DirectoryBrowserSection} (pure template) + {@link DirectoryBrowserContainer} (the React
|
|
29
|
+
* shell), so a test walks the element tree without a renderer.
|
|
30
|
+
*
|
|
31
|
+
* The browser is **narrow-friendly by construction**: one column, wrapping rows, and no declaration that
|
|
32
|
+
* could exceed its container (see `styles.ts`; the widths are percentages, and the only fixed lengths are
|
|
33
|
+
* radii and hairlines). The dialog it is embedded in is what decides between one and two columns.
|
|
34
|
+
*
|
|
35
|
+
* **Unverified** (see `doc/02-platform/workspace-directory-flow.md`): this tree has never been rendered or
|
|
36
|
+
* clicked in a real GUI. A local listing now needs nothing from the composition but the host route itself;
|
|
37
|
+
* a host without the `listLocalDirectory` seam answers `500 capability-missing`, which the browser reports
|
|
38
|
+
* instead of pretending the machine is empty.
|
|
39
|
+
*
|
|
40
|
+
* @module dsh-over-ssh/client/directory-browser
|
|
41
|
+
*/
|
|
42
|
+
import { type ReactNode, type RefObject } from 'react';
|
|
43
|
+
import type { OverSshKey } from '../core/locale.ts';
|
|
44
|
+
import { type ApiResult, type ConnectionSummary, type FetchLike } from '../roots/roots-panel.ts';
|
|
45
|
+
import { type RootsPanelCopy } from '../roots/roots-panel-view.ts';
|
|
46
|
+
/** Which world a listing level belongs to. */
|
|
47
|
+
export type DirectoryBrowserMode = 'local' | 'remote';
|
|
48
|
+
/**
|
|
49
|
+
* What one row is, in either world.
|
|
50
|
+
*
|
|
51
|
+
* The remote action classifies a row as a directory, a file or a symlink; the local one uses the narrower
|
|
52
|
+
* vocabulary `directory | file | other`, because a local link can only be told from a file or a directory by
|
|
53
|
+
* **following** it, which the host never does (see `doc/02-platform/roots-http-route.md` §2).
|
|
54
|
+
*/
|
|
55
|
+
export type DirectoryBrowserKind = 'directory' | 'file' | 'symlink' | 'other';
|
|
56
|
+
/** One **local** row as this plugin's route answers it: **a name and a kind, nothing else**. */
|
|
57
|
+
export interface LocalDirectoryEntry {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly kind: 'directory' | 'file' | 'other';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One local listing level, exactly as `list-local` answers it.
|
|
63
|
+
*
|
|
64
|
+
* Every path here is the **host's own**: `path` is the absolute canonical path it resolved, and `crumbs` is
|
|
65
|
+
* the absolute chain from the filesystem root down to it. The browser reads no other field and derives no
|
|
66
|
+
* path of its own ("up" is `crumbs[len - 2]`, "home" is a request without a `path`), which is what keeps
|
|
67
|
+
* drive letters, UNC shares and separators out of the client.
|
|
68
|
+
*/
|
|
69
|
+
export interface LocalDirectoryListing {
|
|
70
|
+
/** Absolute, host-resolved path of the listed directory. */
|
|
71
|
+
readonly path: string;
|
|
72
|
+
/** Absolute ancestor chain of {@link path}: root first, `path` last. */
|
|
73
|
+
readonly crumbs: readonly string[];
|
|
74
|
+
/** Direct children of {@link path}, as the host classified them. */
|
|
75
|
+
readonly entries: readonly LocalDirectoryEntry[];
|
|
76
|
+
/** True when the host cut the list at its own bound. */
|
|
77
|
+
readonly truncated: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** The kinds the **remote** action can report; a remote listing never answers `other`. */
|
|
80
|
+
export type RemoteDirectoryKind = 'directory' | 'file' | 'symlink';
|
|
81
|
+
/** One remote row as this plugin's route answers it: **a name and a kind, nothing else**. */
|
|
82
|
+
export interface RemoteDirectoryEntry {
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly kind: RemoteDirectoryKind;
|
|
85
|
+
}
|
|
86
|
+
/** One remote listing level. */
|
|
87
|
+
export interface RemoteDirectoryListing {
|
|
88
|
+
/**
|
|
89
|
+
* Absolute path the host actually listed.
|
|
90
|
+
*
|
|
91
|
+
* Carried back because the host may have resolved it: an empty request path means "this connection's start
|
|
92
|
+
* directory", which the host answers with the login user's home (on this site `/home1/<user>`, not
|
|
93
|
+
* `/home/<user>`). The browser reads this instead of assuming what it asked for.
|
|
94
|
+
*/
|
|
95
|
+
readonly path: string;
|
|
96
|
+
readonly entries: readonly RemoteDirectoryEntry[];
|
|
97
|
+
/** True when the host cut the list at its own bound. */
|
|
98
|
+
readonly truncated: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The route the host serves git badges on. **Duplicated here on purpose**: the client bundle cannot import host
|
|
102
|
+
* modules (architecture: `client` may not depend on `host`), and the value is asserted equal in the host's own
|
|
103
|
+
* test so the two cannot drift silently.
|
|
104
|
+
*/
|
|
105
|
+
export declare const GIT_STATUS_API_PATH = "/api/over-ssh/git-status";
|
|
106
|
+
/** One normalized status; the host and the agent only ever send these six. */
|
|
107
|
+
export type GitChangeStatus = 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'conflicted';
|
|
108
|
+
/** One changed path, relative to the git work-tree root the host resolved. */
|
|
109
|
+
export interface GitStatusChange {
|
|
110
|
+
readonly path: string;
|
|
111
|
+
readonly status: GitChangeStatus;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* What the host answers for one browsed directory.
|
|
115
|
+
*
|
|
116
|
+
* `available: false` is normal (Tier 1, an agent without `git.status`, a directory outside any work tree): the
|
|
117
|
+
* browser simply draws no badges. `directory` is the browsed path **relative to the work-tree root**, which is
|
|
118
|
+
* what lets the client match a row to its change without knowing any path rules.
|
|
119
|
+
*/
|
|
120
|
+
export interface GitStatusView {
|
|
121
|
+
readonly available: boolean;
|
|
122
|
+
readonly root?: string;
|
|
123
|
+
readonly directory?: string;
|
|
124
|
+
readonly branch?: string;
|
|
125
|
+
readonly changes: readonly GitStatusChange[];
|
|
126
|
+
readonly truncated: boolean;
|
|
127
|
+
readonly reason?: string;
|
|
128
|
+
}
|
|
129
|
+
/** Badge letter per status. Language-neutral on purpose (no locale key is added for them). */
|
|
130
|
+
export declare const GIT_STATUS_BADGE: Readonly<Record<GitChangeStatus, string>>;
|
|
131
|
+
/** Narrow one host answer; unknown statuses and empty paths are dropped. */
|
|
132
|
+
export declare function parseGitStatusView(value: Record<string, unknown>): GitStatusView;
|
|
133
|
+
/** The calls the browser makes. All of them are injected, so the state machine is testable offline. */
|
|
134
|
+
export interface DirectoryBrowserIo {
|
|
135
|
+
/** The connections the remote mode offers (this plugin's own `GET`). */
|
|
136
|
+
loadConnections(): Promise<ApiResult<readonly ConnectionSummary[]>>;
|
|
137
|
+
/** One local directory level, through this plugin's own route (`list-local`); no `path` means the host's home. */
|
|
138
|
+
listLocal(path?: string): Promise<ApiResult<LocalDirectoryListing>>;
|
|
139
|
+
/** One remote directory level, through this plugin's own route. */
|
|
140
|
+
listRemote(input: {
|
|
141
|
+
readonly connectionId: string;
|
|
142
|
+
readonly path: string;
|
|
143
|
+
}): Promise<ApiResult<RemoteDirectoryListing>>;
|
|
144
|
+
/**
|
|
145
|
+
* Remote git badges for one listed level, through this plugin's own route.
|
|
146
|
+
*
|
|
147
|
+
* Optional so a surface (or an older composition) that does not serve the route keeps compiling: absent means
|
|
148
|
+
* no badges, never a failed listing.
|
|
149
|
+
*/
|
|
150
|
+
loadGitStatus?(input: {
|
|
151
|
+
readonly connectionId: string;
|
|
152
|
+
readonly path: string;
|
|
153
|
+
}): Promise<ApiResult<GitStatusView>>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The host's machine-readable reason for "this composition cannot list a local directory at all".
|
|
157
|
+
*
|
|
158
|
+
* The route answers it as a **500** and never as an empty listing: a machine nobody can read and a machine
|
|
159
|
+
* with nothing in it are two different facts. The browser renders it through `browseUnavailable`.
|
|
160
|
+
*/
|
|
161
|
+
export declare const NO_LOCAL_CAPABILITY_REASON = "capability-missing";
|
|
162
|
+
/** The error reported when a remote listing is asked for without a connection. */
|
|
163
|
+
export declare const NO_CONNECTION_ERROR = "no-connection";
|
|
164
|
+
/**
|
|
165
|
+
* Prefix of the reason a failed **native chooser** reports.
|
|
166
|
+
*
|
|
167
|
+
* The chooser is not a listing: when the platform dialog is missing (`directory-picker/unavailable`) or it
|
|
168
|
+
* rejects, the rows on screen are still the ones the host listed, and telling the user "listing failed" would
|
|
169
|
+
* describe the wrong thing. The prefix is what lets the failure text pick the right sentence without a second
|
|
170
|
+
* error field in the state.
|
|
171
|
+
*/
|
|
172
|
+
export declare const PICK_ERROR_PREFIX = "pick:";
|
|
173
|
+
/**
|
|
174
|
+
* The text one refused `POST` reports.
|
|
175
|
+
*
|
|
176
|
+
* Order, and the reason for it: the host's `reason` is the **machine-readable** field (it names the case, so
|
|
177
|
+
* a surface can branch on it), its `error` is the prose behind it, and `transportFailureReason(status, path)`
|
|
178
|
+
* is what is left when the body named nothing at all - the path is part of that text on purpose, because
|
|
179
|
+
* `http-404` alone cannot distinguish "the route is not registered" from "the request never reached the host"
|
|
180
|
+
* (see `doc/02-platform/roots-http-route.md` §2).
|
|
181
|
+
*
|
|
182
|
+
* @param parsed - the response body, when it parsed as an object.
|
|
183
|
+
* @param status - the HTTP status.
|
|
184
|
+
* @param path - the exact URL that was requested.
|
|
185
|
+
* @returns the reason the surface shows.
|
|
186
|
+
*/
|
|
187
|
+
export declare function postFailureReason(parsed: Record<string, unknown> | undefined, status: number, path: string): string;
|
|
188
|
+
/**
|
|
189
|
+
* POST one action to this plugin's route and answer the **raw** JSON object.
|
|
190
|
+
*
|
|
191
|
+
* Deliberately not `createRootsApi().send`: that path projects the answer through `parseRootsSnapshot`, which
|
|
192
|
+
* is right for the panel's snapshot and wrong for an action that answers something else (`anchorPath` for
|
|
193
|
+
* `add-root`, `entries` for `list-remote` and `list-local`). The failure vocabulary is still shared - the
|
|
194
|
+
* host's machine-readable `reason` wins, then its prose, and otherwise `transportFailureReason(status, path)`
|
|
195
|
+
* names the request that failed.
|
|
196
|
+
*
|
|
197
|
+
* @param fetchImpl - the page's `fetch`, or a stub in tests.
|
|
198
|
+
* @param path - the route to post to; `ROOTS_API_PATH` for every caller in this plugin.
|
|
199
|
+
* @param body - the action and its whitelisted parameters.
|
|
200
|
+
* @returns the parsed object, or the reason it could not be produced.
|
|
201
|
+
*/
|
|
202
|
+
export declare function postRootsJson(fetchImpl: FetchLike, path: string, body: Record<string, unknown>): Promise<ApiResult<Record<string, unknown>>>;
|
|
203
|
+
/**
|
|
204
|
+
* Bind the browser's three calls to this plugin's own route.
|
|
205
|
+
*
|
|
206
|
+
* There is **no platform directory service in this face**: `uiWorkspace.listDirectory()` needs the
|
|
207
|
+
* directory-picker's `browse` capability, and a profile whose composed picker serves the native dialog fails
|
|
208
|
+
* that call outright. Both listings are therefore POSTs to the same relative route, with the same failure
|
|
209
|
+
* vocabulary, and the composition cannot take local browsing away.
|
|
210
|
+
*
|
|
211
|
+
* @param options - `fetch` and an optional route override (tests).
|
|
212
|
+
* @returns the calls the browser uses.
|
|
213
|
+
*/
|
|
214
|
+
export declare function createDirectoryBrowserIo(options: {
|
|
215
|
+
readonly fetchImpl: FetchLike;
|
|
216
|
+
readonly path?: string;
|
|
217
|
+
}): DirectoryBrowserIo;
|
|
218
|
+
/** One rendered row. */
|
|
219
|
+
export interface DirectoryBrowserRow {
|
|
220
|
+
/** Stable identity inside the current listing (the child's path for local rows, name for remote ones). */
|
|
221
|
+
readonly key: string;
|
|
222
|
+
readonly name: string;
|
|
223
|
+
/** Absolute path: for a local row the host's path plus its name, for a remote row the joined remote path. */
|
|
224
|
+
readonly path: string;
|
|
225
|
+
readonly kind: DirectoryBrowserKind;
|
|
226
|
+
readonly hidden: boolean;
|
|
227
|
+
}
|
|
228
|
+
/** Everything the browser renders from. */
|
|
229
|
+
export interface DirectoryBrowserState {
|
|
230
|
+
/** Which world the current level belongs to. */
|
|
231
|
+
readonly mode: DirectoryBrowserMode;
|
|
232
|
+
/** True while a listing call is in flight. */
|
|
233
|
+
readonly pending: boolean;
|
|
234
|
+
/** The listed directory; `''` before the first answer. Always the path the **host** answered with. */
|
|
235
|
+
readonly path: string;
|
|
236
|
+
/** Ancestor chain of {@link path}, root first and `path` last (local listings only; `[]` for remote). */
|
|
237
|
+
readonly crumbs: readonly string[];
|
|
238
|
+
/** Child rows of {@link path}. */
|
|
239
|
+
readonly rows: readonly DirectoryBrowserRow[];
|
|
240
|
+
/** True when the listing was cut at the host's bound. */
|
|
241
|
+
readonly truncated: boolean;
|
|
242
|
+
/** The connection remote listings go through. */
|
|
243
|
+
readonly connectionId: string;
|
|
244
|
+
/** The connections remote mode offers. */
|
|
245
|
+
readonly connections: readonly ConnectionSummary[];
|
|
246
|
+
/** The last connection-list failure, kept so the surface can say why the list is empty. */
|
|
247
|
+
readonly connectionsError?: string;
|
|
248
|
+
/**
|
|
249
|
+
* True while the remote world's connection list is being read.
|
|
250
|
+
*
|
|
251
|
+
* Without it the browser rendered 「还没有连接:请先在『SSH 远程开发』里新增一个连接。」 during the read — telling the
|
|
252
|
+
* user to add a connection they already had (audit 2026-11-01b ui P2-4).
|
|
253
|
+
*/
|
|
254
|
+
readonly connectionsPending: boolean;
|
|
255
|
+
/** The last listing failure, as the host reported it. */
|
|
256
|
+
readonly error?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Remote git badges for {@link path}.
|
|
259
|
+
*
|
|
260
|
+
* Absent means "no badges": local mode, a failed/slow git read, or a connection without git support. Its own
|
|
261
|
+
* optional field rather than more booleans, so "we asked and got nothing" and "we never asked" render the same
|
|
262
|
+
* (no badges) and the listing cannot be affected by it.
|
|
263
|
+
*/
|
|
264
|
+
readonly git?: DirectoryBrowserGitState;
|
|
265
|
+
}
|
|
266
|
+
/** The badge data of one listed level. */
|
|
267
|
+
export interface DirectoryBrowserGitState {
|
|
268
|
+
/** The work-tree root the host resolved; `''` when the answer carried none. */
|
|
269
|
+
readonly root: string;
|
|
270
|
+
/** The browsed directory relative to {@link root} ('' when the browsed path *is* the root). */
|
|
271
|
+
readonly directory: string;
|
|
272
|
+
/** Branch name; absent for a detached HEAD (never substituted). */
|
|
273
|
+
readonly branch?: string;
|
|
274
|
+
/** Root-relative path → status. */
|
|
275
|
+
readonly changes: Readonly<Record<string, GitChangeStatus>>;
|
|
276
|
+
readonly truncated: boolean;
|
|
277
|
+
}
|
|
278
|
+
/** What the reducer accepts. */
|
|
279
|
+
export type DirectoryBrowserAction =
|
|
280
|
+
/** The user switched world; the current level is dropped, not carried over. */
|
|
281
|
+
{
|
|
282
|
+
readonly type: 'mode-changed';
|
|
283
|
+
readonly mode: DirectoryBrowserMode;
|
|
284
|
+
}
|
|
285
|
+
/** The user chose another connection. */
|
|
286
|
+
| {
|
|
287
|
+
readonly type: 'connection-changed';
|
|
288
|
+
readonly connectionId: string;
|
|
289
|
+
}
|
|
290
|
+
/** The connection list is being read (also clears a previous failure, so the retry shows "reading"). */
|
|
291
|
+
| {
|
|
292
|
+
readonly type: 'connections-loading';
|
|
293
|
+
}
|
|
294
|
+
/** The connection list arrived. */
|
|
295
|
+
| {
|
|
296
|
+
readonly type: 'connections-loaded';
|
|
297
|
+
readonly connections: readonly ConnectionSummary[];
|
|
298
|
+
}
|
|
299
|
+
/** The connection list failed. */
|
|
300
|
+
| {
|
|
301
|
+
readonly type: 'connections-failed';
|
|
302
|
+
readonly error: string;
|
|
303
|
+
}
|
|
304
|
+
/** A level is being read: the rows are cleared so a stale level is never shown under a new path. */
|
|
305
|
+
| {
|
|
306
|
+
readonly type: 'target';
|
|
307
|
+
readonly path: string;
|
|
308
|
+
}
|
|
309
|
+
/** A local level arrived. */
|
|
310
|
+
| {
|
|
311
|
+
readonly type: 'local-loaded';
|
|
312
|
+
readonly listing: LocalDirectoryListing;
|
|
313
|
+
}
|
|
314
|
+
/** A remote level arrived. */
|
|
315
|
+
| {
|
|
316
|
+
readonly type: 'remote-loaded';
|
|
317
|
+
readonly path: string;
|
|
318
|
+
readonly listing: RemoteDirectoryListing;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The remote git badges for the current level arrived.
|
|
322
|
+
*
|
|
323
|
+
* Its own action rather than a field of `remote-loaded`: the git read and the listing are independent calls,
|
|
324
|
+
* and a level must render (and stay interactive) while the badge read is still in flight or has failed.
|
|
325
|
+
*/
|
|
326
|
+
| {
|
|
327
|
+
readonly type: 'git-loaded';
|
|
328
|
+
readonly view: GitStatusView;
|
|
329
|
+
}
|
|
330
|
+
/** Reading the level failed. */
|
|
331
|
+
| {
|
|
332
|
+
readonly type: 'failed';
|
|
333
|
+
readonly error: string;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The native chooser round trip failed.
|
|
337
|
+
*
|
|
338
|
+
* Its own action rather than `failed`: the listing is untouched, so only the failure line changes.
|
|
339
|
+
*/
|
|
340
|
+
| {
|
|
341
|
+
readonly type: 'pick-failed';
|
|
342
|
+
readonly error: string;
|
|
343
|
+
};
|
|
344
|
+
/** @returns the state a freshly mounted browser holds. */
|
|
345
|
+
export declare function initialDirectoryBrowserState(mode?: DirectoryBrowserMode): DirectoryBrowserState;
|
|
346
|
+
/**
|
|
347
|
+
* The browser's state machine. Pure: an action that changes nothing returns the **same object**.
|
|
348
|
+
*
|
|
349
|
+
* @param state - the current state.
|
|
350
|
+
* @param action - what happened.
|
|
351
|
+
* @returns the next state.
|
|
352
|
+
*/
|
|
353
|
+
export declare function directoryBrowserReducer(state: DirectoryBrowserState, action: DirectoryBrowserAction): DirectoryBrowserState;
|
|
354
|
+
/** The parent of one absolute POSIX path, or `undefined` at the root (and for a path never listed). */
|
|
355
|
+
export declare function parentRemotePath(path: string): string | undefined;
|
|
356
|
+
/** One child of a remote directory. Only ever called with a name the host reported (no separators). */
|
|
357
|
+
export declare function remoteChildPath(base: string, name: string): string;
|
|
358
|
+
/**
|
|
359
|
+
* The path to ask for when entering one child of a **local** listing.
|
|
360
|
+
*
|
|
361
|
+
* This is the browser's only path composition, and it is deliberately the smallest one that can work: the
|
|
362
|
+
* route accepts absolute paths only, so "enter" has to name the child, and the host's row carries the name
|
|
363
|
+
* alone. Everything else stays host-owned - the **base is always the path the host answered with** (never a
|
|
364
|
+
* `dirname` of anything), "up" is a crumb the host sent, and "home" is a request with no path at all.
|
|
365
|
+
*
|
|
366
|
+
* `/` is the separator appended, because it is the one both dialects accept: `node:path.isAbsolute` and
|
|
367
|
+
* `node:path.resolve` take `D:/work/name` on Windows (and canonicalise it back to `D:\work\name`), while on
|
|
368
|
+
* POSIX a backslash can be part of a real file name, so `/` is the only correct choice there. Trailing
|
|
369
|
+
* separators are dropped first so the filesystem root (`/`, `D:\`, `\\server\share\`) does not yield `//name`.
|
|
370
|
+
*
|
|
371
|
+
* @param base - the host's own absolute path of the listed directory.
|
|
372
|
+
* @param name - a name the host reported (by contract it carries no separator).
|
|
373
|
+
* @returns the absolute path to list for that child.
|
|
374
|
+
*/
|
|
375
|
+
export declare function localChildPath(base: string, name: string): string;
|
|
376
|
+
/** The complete render input. */
|
|
377
|
+
export interface DirectoryBrowserView {
|
|
378
|
+
/** True while a listing is in flight. */
|
|
379
|
+
readonly pending: boolean;
|
|
380
|
+
/** The level being listed: the last answer's path, or the one being read now. */
|
|
381
|
+
readonly path: string;
|
|
382
|
+
readonly rows: readonly DirectoryBrowserRow[];
|
|
383
|
+
readonly connections: readonly ConnectionSummary[];
|
|
384
|
+
readonly connectionId: string;
|
|
385
|
+
/** May "go up" be pressed, and where does it go? */
|
|
386
|
+
readonly canGoUp: boolean;
|
|
387
|
+
/** Where "up" goes; absent at the top of the tree. */
|
|
388
|
+
readonly upPath?: string;
|
|
389
|
+
/** May the current level be committed? */
|
|
390
|
+
readonly canCommit: boolean;
|
|
391
|
+
/** Remote mode with no connection chosen: nothing can be listed yet. */
|
|
392
|
+
readonly needsConnection: boolean;
|
|
393
|
+
/** The level was read and holds no child. */
|
|
394
|
+
readonly empty: boolean;
|
|
395
|
+
/** The level was read and cut at the service's bound. */
|
|
396
|
+
readonly truncated: boolean;
|
|
397
|
+
/** The last listing failure. */
|
|
398
|
+
readonly error?: string;
|
|
399
|
+
/** The last connection-list failure. */
|
|
400
|
+
readonly connectionsError?: string;
|
|
401
|
+
/** True while the connection list is being read (remote world, nothing chosen yet). */
|
|
402
|
+
readonly connectionsPending: boolean;
|
|
403
|
+
/** Branch badge at the top of the level; absent when unknown (detached HEAD, no git, read failed). */
|
|
404
|
+
readonly gitBranch?: string;
|
|
405
|
+
/** Root-relative path → status, for {@link gitBadgeForRow}. Empty when no badges apply. */
|
|
406
|
+
readonly gitChanges: Readonly<Record<string, GitChangeStatus>>;
|
|
407
|
+
/** The browsed directory relative to the git root ('' when browsing the root itself or no badge data). */
|
|
408
|
+
readonly gitDirectory: string;
|
|
409
|
+
/** The badge list was cut (agent/service cap): the UI must not claim it is complete. */
|
|
410
|
+
readonly gitTruncated: boolean;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Turn the state into render input. The single place the browser's gates are decided.
|
|
414
|
+
*
|
|
415
|
+
* @param state - the browser state.
|
|
416
|
+
* @returns the flags, the path, the rows and the two failure texts.
|
|
417
|
+
*/
|
|
418
|
+
export declare function selectDirectoryBrowserView(state: DirectoryBrowserState): DirectoryBrowserView;
|
|
419
|
+
/**
|
|
420
|
+
* The badge of one row: the status of the row's **own path**, or `undefined`.
|
|
421
|
+
*
|
|
422
|
+
* Two deliberate rules:
|
|
423
|
+
* - only a **direct** change marks a row: a modified file two levels down does not paint its ancestor directory
|
|
424
|
+
* (an aggregate status would be a guess; the user can enter the directory and see it);
|
|
425
|
+
* - directory rows get no badge at all — `git status` reports paths, not directories, and inventing `M` for a
|
|
426
|
+
* directory that merely *contains* changes is exactly the "unknown status guessed into a badge" this task
|
|
427
|
+
* forbids.
|
|
428
|
+
*
|
|
429
|
+
* @param view - the rendered level.
|
|
430
|
+
* @param row - one row of that level.
|
|
431
|
+
* @returns the status to draw, or `undefined` (no badge).
|
|
432
|
+
*/
|
|
433
|
+
export declare function gitBadgeForRow(view: DirectoryBrowserView, row: DirectoryBrowserRow): GitChangeStatus | undefined;
|
|
434
|
+
/** What the controller needs: the injected calls, the active dictionary and its mount-time world. */
|
|
435
|
+
export interface DirectoryBrowserDeps {
|
|
436
|
+
readonly io: DirectoryBrowserIo;
|
|
437
|
+
/** The dictionary of the active locale; a thunk, because the locale can change under a mounted browser. */
|
|
438
|
+
readonly copy: () => RootsPanelCopy;
|
|
439
|
+
/** The world this occurrence browses; a mount-time input (changing it remounts). */
|
|
440
|
+
readonly mode?: DirectoryBrowserMode;
|
|
441
|
+
/** A connection chosen before the first listing (the embedding surface may already know one). */
|
|
442
|
+
readonly connectionId?: string;
|
|
443
|
+
/**
|
|
444
|
+
* The connection list the embedding surface already read, when it has one.
|
|
445
|
+
*
|
|
446
|
+
* The workspace directory flow reads it when it opens; the embedded remote browser used to read the **whole
|
|
447
|
+
* roots snapshot** again just to get the same list, i.e. one extra full GET per "open browse" (audit
|
|
448
|
+
* 2026-11-01b ui P3-8). Seeding it here removes the duplicate, and `ensureConnections` still reads for a
|
|
449
|
+
* surface that opens the browser on its own (the session entry).
|
|
450
|
+
*/
|
|
451
|
+
readonly connections?: readonly ConnectionSummary[];
|
|
452
|
+
}
|
|
453
|
+
/** The calls the container makes. */
|
|
454
|
+
export interface DirectoryBrowserController {
|
|
455
|
+
readonly getState: () => DirectoryBrowserState;
|
|
456
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
457
|
+
/** Read the first level (and, in remote mode, the connection list) once. */
|
|
458
|
+
readonly start: (seedPath?: string) => void;
|
|
459
|
+
readonly setMode: (mode: DirectoryBrowserMode) => void;
|
|
460
|
+
readonly setConnection: (connectionId: string) => void;
|
|
461
|
+
/** Enter a child directory. */
|
|
462
|
+
readonly enter: (path: string) => void;
|
|
463
|
+
/** Go one level up, when the view allows it. */
|
|
464
|
+
readonly up: () => void;
|
|
465
|
+
/** Jump to this side's home directory: the local host's, or the remote login user's (resolved by the host). */
|
|
466
|
+
readonly home: () => void;
|
|
467
|
+
/** Re-read the current level. */
|
|
468
|
+
readonly reload: () => void;
|
|
469
|
+
/** Re-read the connection list after its own failure (the remote world's retry). */
|
|
470
|
+
readonly reloadConnections: () => void;
|
|
471
|
+
/** Report a failed native-chooser round trip; the listing is not touched. */
|
|
472
|
+
readonly failPick: (reason: string) => void;
|
|
473
|
+
/**
|
|
474
|
+
* Re-arm after a mount.
|
|
475
|
+
*
|
|
476
|
+
* React's development-mode replay runs the effect's cleanup once before the real lifetime, so a controller
|
|
477
|
+
* that stayed disposed would never load anything (or, for the session entry, never write). The flow's
|
|
478
|
+
* controller has the same method for the same reason.
|
|
479
|
+
*/
|
|
480
|
+
readonly activate: () => void;
|
|
481
|
+
readonly dispose: () => void;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Build the stateful half of the browser.
|
|
485
|
+
*
|
|
486
|
+
* Every listing is addressed by a monotonically increasing token: a late answer for a directory the user
|
|
487
|
+
* already left is dropped rather than rendered, which is the one race a browsing UI really has (click
|
|
488
|
+
* through a slow host, then go up).
|
|
489
|
+
*
|
|
490
|
+
* @param deps - the injected calls, the dictionary and the mount-time world.
|
|
491
|
+
* @returns the controller the container subscribes to.
|
|
492
|
+
*/
|
|
493
|
+
export declare function createDirectoryBrowser(deps: DirectoryBrowserDeps): DirectoryBrowserController;
|
|
494
|
+
/** What the browser's controls call. All of them are no-ops when the gate is closed. */
|
|
495
|
+
export interface DirectoryBrowserActions {
|
|
496
|
+
readonly onModeChange: (mode: DirectoryBrowserMode) => void;
|
|
497
|
+
readonly onConnectionChange: (connectionId: string) => void;
|
|
498
|
+
readonly onEnter: (path: string) => void;
|
|
499
|
+
readonly onUp: () => void;
|
|
500
|
+
/** Jump to the host account's home directory (local mode only). */
|
|
501
|
+
readonly onHome: () => void;
|
|
502
|
+
readonly onReload: () => void;
|
|
503
|
+
/** The connection list's own retry (its failure is inline, in the remote block). */
|
|
504
|
+
readonly onReloadConnections: () => void;
|
|
505
|
+
readonly onCommit: () => void;
|
|
506
|
+
/**
|
|
507
|
+
* 用宿主系统的原生目录选择框挑一个目录(本机模式、且宿主提供了这个能力时才有)。
|
|
508
|
+
*
|
|
509
|
+
* 为什么要有:内置浏览器能到的地方有限,而本机用户通常已经知道路径,或者想从「最近使用」里挑。
|
|
510
|
+
* 平台为此提供了 `uiWorkspace.pickDirectory()`(桌面壳里是 `__DSH_DIRECTORY_PICKER__`),
|
|
511
|
+
* 直接弹系统框比一层层点进去快得多。
|
|
512
|
+
*/
|
|
513
|
+
readonly onPickNative?: (() => void) | undefined;
|
|
514
|
+
}
|
|
515
|
+
/** The default `id` prefix when the embedding surface does not pass one (direct template calls, tests). */
|
|
516
|
+
export declare const DEFAULT_DIRECTORY_BROWSER_ID_PREFIX = "dsh-rw-browse";
|
|
517
|
+
/** Props of the pure template: state in, callbacks out, no hooks and no store access. */
|
|
518
|
+
export interface DirectoryBrowserSectionProps {
|
|
519
|
+
readonly copy: RootsPanelCopy;
|
|
520
|
+
readonly state: DirectoryBrowserState;
|
|
521
|
+
readonly actions: DirectoryBrowserActions;
|
|
522
|
+
/** Label of the committing action ("use this directory"), supplied by the surface that embedded it. */
|
|
523
|
+
readonly commitLabel: string;
|
|
524
|
+
/** True while the embedding surface cannot accept a pick (owner busy, request in flight). */
|
|
525
|
+
readonly disabled?: boolean | undefined;
|
|
526
|
+
/** Hide the mode switch when the surface already decided which world this browser is for. */
|
|
527
|
+
readonly fixedMode?: boolean | undefined;
|
|
528
|
+
/** `id` prefix of this occurrence's controls, so two embedded browsers cannot produce duplicate ids. */
|
|
529
|
+
readonly idPrefix?: string | undefined;
|
|
530
|
+
/**
|
|
531
|
+
* The level list, so the container can move focus into it after a navigation.
|
|
532
|
+
*
|
|
533
|
+
* The row the user activated is **unmounted** by that navigation (the level is replaced), so without this the
|
|
534
|
+
* focus falls to `<body>` and every keyboard step starts over (audit 2026-10-31 client.md §2.3).
|
|
535
|
+
*/
|
|
536
|
+
readonly listRef?: RefObject<HTMLUListElement | null> | undefined;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Render one directory level. Pure: the same props yield the same tree.
|
|
540
|
+
*
|
|
541
|
+
* @param props - copy, state, callbacks and the surface's own gate.
|
|
542
|
+
* @returns the browser element.
|
|
543
|
+
*/
|
|
544
|
+
export declare function DirectoryBrowserSection({ copy, state, actions, commitLabel, disabled, fixedMode, idPrefix, listRef, }: DirectoryBrowserSectionProps): ReactNode;
|
|
545
|
+
/** Props of the React shell: the injected calls, the mount-time world and the surface's callback. */
|
|
546
|
+
export interface DirectoryBrowserContainerProps {
|
|
547
|
+
/** The active locale's dictionary. */
|
|
548
|
+
readonly copy: RootsPanelCopy;
|
|
549
|
+
/** The injected calls; created once by the entry, never by the component. */
|
|
550
|
+
readonly io: DirectoryBrowserIo;
|
|
551
|
+
/** Which world this browser is for; changing it is expressed by remounting (the caller sets `key`). */
|
|
552
|
+
readonly mode: DirectoryBrowserMode;
|
|
553
|
+
/** Where a local listing starts; absent means the host account's home. */
|
|
554
|
+
readonly seedPath?: string | undefined;
|
|
555
|
+
/** Pre-selected connection for remote mode. */
|
|
556
|
+
readonly initialConnectionId?: string | undefined;
|
|
557
|
+
/**
|
|
558
|
+
* The connection list the embedding surface already has (see {@link DirectoryBrowserDeps.connections}).
|
|
559
|
+
*
|
|
560
|
+
* Absent means "read it yourself", which is what the composer entry does.
|
|
561
|
+
*/
|
|
562
|
+
readonly initialConnections?: readonly ConnectionSummary[] | undefined;
|
|
563
|
+
readonly disabled?: boolean | undefined;
|
|
564
|
+
/** Hide the local/remote switch (the embedding surface already decided). */
|
|
565
|
+
readonly fixedMode?: boolean | undefined;
|
|
566
|
+
/**
|
|
567
|
+
* 宿主系统的原生目录选择框;`null` 表示用户取消。
|
|
568
|
+
*
|
|
569
|
+
* 缺席就完全不画那颗按钮——「没有这个能力」和「有这个能力但调用会失败」是两件事,
|
|
570
|
+
* 界面上只应该出现前者对应的样子。
|
|
571
|
+
*/
|
|
572
|
+
readonly pickNative?: (() => Promise<string | null>) | undefined;
|
|
573
|
+
/** `id` prefix of this occurrence's controls; defaults to {@link DEFAULT_DIRECTORY_BROWSER_ID_PREFIX}. */
|
|
574
|
+
readonly idPrefix?: string | undefined;
|
|
575
|
+
readonly commitLabel: string;
|
|
576
|
+
/** The operator picked the current directory. */
|
|
577
|
+
readonly onCommit: (path: string, connectionId: string | undefined) => void;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Subscribe to a per-mount browser and render {@link DirectoryBrowserSection}.
|
|
581
|
+
*
|
|
582
|
+
* @param props - the injected calls, the world and the commit callback.
|
|
583
|
+
* @returns the browser element.
|
|
584
|
+
*/
|
|
585
|
+
export declare function DirectoryBrowserContainer(props: DirectoryBrowserContainerProps): ReactNode;
|
|
586
|
+
/** The dictionary keys this module renders, so a test can assert the two locales carry them without a second list. */
|
|
587
|
+
export type DirectoryBrowserKey = Extract<OverSshKey, 'browseHeading' | 'browseUp' | 'browseCurrent' | 'browseEmpty' | 'browseLoading' | 'browseFailed' | 'browseUnavailable' | 'browseNeedConnection' | 'browseTruncated' | 'browseKindFile' | 'browseKindSymlink' | 'browseKindOther' | 'browseRetry' | 'browseLocalMode' | 'browseRemoteMode' | 'browseHome' | 'browseChoose' | 'flowNoConnections' | 'rootsConnection' | 'rootsConnectionPlaceholder'>;
|