Add @deepseek-ai/dsh-host-directory-picker-auto, a node-half-only chooser that samples the host situation once at boot (bind host via a new httpServer.host getter, SSH markers, platform, DISPLAY/WAYLAND_DISPLAY) and mounts the matching dual-face backend (-native or -browse) as a real Loader entry in the in-memory root tree; the effect disposer removes it. Entry-level mounting keeps the seam's one-row-swaps-both-faces invariant: the client module table discovers the mounted backend's browser half exactly as a config row's. apps/cli now composes -auto as its directory-picker row; composing a backend row directly remains the pin.
4.5 KiB
Agent Note: Adaptive default for the directory-picker interaction
Status: implemented
English | 中文
Problem
The directory-picker seam made the interaction a cordis.yml swap point, but the shipped composition still had to pin one backend: -browse everywhere meant a local operator never got the OS chooser, -native everywhere breaks every remote deployment. The right default depends on facts only the running host knows — where the server binds, whether the process was launched over SSH, whether a display session exists — so no static row is correct for all deployments.
Decision
A third sibling package, dsh-host-directory-picker-auto: a node-half-only chooser that owns no picking code and no UI. Its apply samples the host facts exactly once at boot — bind host from the injected httpServer (a new host getter mirrors the existing port), SSH_CONNECTION/SSH_TTY, platform, DISPLAY/WAYLAND_DISPLAY — resolves them through one exported pure function, and mounts the chosen dual-face backend with ctx.loader.create({name}) into the Loader's in-memory root tree; the effect's disposer removes the entry again. native requires every attended-host signal (loopback bind ∧ no SSH markers ∧ display session, assumed on darwin/win32); anything ambiguous resolves to browse, which works everywhere. apps/cli now mounts -auto as its directory-picker row; composing -native or -browse directly remains the pin.
Why entry-level mounting is the load-bearing mechanism: the client module table (dsh-client-modules) reconciles Loader entries reactively over internal/plugin, so a backend mounted as a real entry gets its browser half discovered exactly as a config-row's would be — the seam's one-row-swaps-both-faces invariant survives adaptivity with zero duplicated client code. The dev HMR row (AppCLIEntry) is the mechanism precedent. Root-tree targeting matters: the root tree's write() is a no-op, so the resolved row can never be persisted back into cordis.yml (the Include subtree does write).
Alternatives considered
- Boot-glue resolution in
AppCLIEntry(ship both rows with staticdisabled, patchdisabledfrom a--directory-picker=auto|native|browseflag). Works —PatchOptionspatches metadata, and the modules scan skips disabled rows — but leaves the decision app-private where every future composition re-implements it; the chooser plugin gives anycordis.ymlthe same one-row adaptivity. Reintroduce the flag only when a deployment needs to force a backend without editing its yml. - One merged plugin branching per call (client tries
pick, falls back to the browse dialog ondirectory-picker-unavailable). Rejected: the client would need both flows in one bundle — the bundle-purity gate forbids cross-plugin value imports and jscpd forbids copying the dialog — and per-call probing pays a doomed RPC on every open of a browse host. - Resurrecting the wire advertisement so both client flows mount and branch on the host's kind. Rejected: reverses the seam note's deletion for no consumer the chooser doesn't already serve, and collides with the
singledirectory-flow holes. - Per-connection adaptivity (native for a loopback browser, browse for a remote one, same server). Deferred: needs a per-client capability, the advertisement above, and both flows mounted; no deployment serves both operator shapes at once today.
Consequences
- The shipped web GUI adapts out of the box: attended local host → OS chooser; SSH launch, all-interfaces bind, or headless host → in-app browser. Detection is a heuristic (a detached tmux session loses
SSH_*; a non-Aqua darwin process still counts as displayed) — a wrongnativechoice degrades to the backend's existing retryable failure dialog, and composing-browsepins the safe interaction. - One resolution per boot keeps the seam's capability-stability contract; per-connection shapes remain out of scope until a deployment demands them.
- Mounting the chooser and a backend row together fails loud (duplicate
directoryPickerservice; duplicate flow in thesingleholes). - The host typecheck aggregate now references the two backend projects (declarations only, node entries carry no client merge) so the chooser's REAL-composition test can mount them — the mirror of the client aggregate's
webserverreference.