Skip to content

fix(dsh-role-model): settings panel - representable channels, no hang on Apply, Remote read via ctx.get - #320

Open
try-works wants to merge 3 commits into
devfrom
fix/role-model-channel-options
Open

try-works wants to merge 3 commits into
devfrom
fix/role-model-channel-options

Conversation

@try-works

@try-works try-works commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Three defects in the role-model settings panel, all reported from live use.

1. The channel select could not represent production

The select offered only [unset, 3457, 3458] — production 3456 was filtered out, on the reasoning that choosing the default was the same as leaving the field alone. That made port: 3456 unrepresentable, so the <select> carried a value no <option> had and the control rendered with nothing selected.

Production is now first-class and the sentinel says what it does:

unset — keep the endpoint field (defaults to :3456)
production — role-model (:3456)
stage — role-model-stage (:3457)
development — role-model-dev (:3458)

2. Apply could sit on "Applying…" forever

Host-side root cause: the settings write runs inside the loader's exclusive queue (config-editor:153 -> hmr.runExclusive), an unbounded FIFO (hmr/src/index.ts:139-147) with no deadline. A write issued while another profile mutation is in flight is queued rather than refused, and its promise does not settle until that operation finishes. Observed live: package.json.lock created the same second as dsh plugin --profile web add @try-works/dsh-stt@0.2.0, held for minutes, profile still holding the old value.

The plugin cannot fix the host queue but must not hang on it: writeConfig now bounds its wait (20s default), and onApply clears saving outside the happy path.

3. cannot get property "remote" without inject

The guard meant to make the Remote optional assumed ctx.remote answers undefined when absent. On the real client context it throws; ctx.get is the lookup that answers undefined. So settingsRemote threw on both paths:

  • readConfig rejected, and it was called as void readConfig(ctx).then(...) with no catch, so the failure was silent and the form never seeded — hence the blank endpoint / selectedAlias / providerRoute / requestTimeoutMs fields.
  • writeConfig rejected before its own try, so Apply surfaced the raw Cordis message. It only became visible because fix 2 made onApply report failures rather than freeze.

settingsRemote now reads ctx.get("remote") inside a guard. The client half keeps injecting only slots.

Note on the control: the Remote is injectable — the harness's own ui-agent-preset injects 'remote' and 'remote.settings'. This half stays on ctx.get because it must still render when the Remote is missing.

Tests

  • every RUNTIME_CHANNELS port must appear in CHANNEL_OPTIONS (RED: "channel 3456 is not selectable")
  • rendered option list is [0, 3456, 3457, 3458], with production/stage/development labels
  • a write the host accepts but never answers yields a message instead of hanging (RED: test timed out)
  • the client context double is now faithful to Cordis — a throwing remote property plus a working get — which reproduced the reported error exactly: six specs failed with cannot get property "remote" without inject before the fix

Suite: 409 passing; tsc and biome clean.

…tings page

The channel select offered only `[unset, 3457, 3458]` — production 3456 was filtered
out on the reasoning that choosing the default was the same as leaving the field alone.
That made `port: 3456` unrepresentable: it is the schema default and a legal stored
value, so the select carried a value no option had. The control rendered with nothing
selected and read as broken, which is the reported "the plugin's UI that chooses
dev/stage/prod doesn't work".

Production is now a first-class option and the sentinel is labelled for what it does:

    unset — keep the endpoint field (defaults to :3456)
    production — role-model (:3456)
    stage — role-model-stage (:3457)
    development — role-model-dev (:3458)

The unset sentinel stays distinct from a deliberate choice of production, so an
explicit `endpoint` (a remote host) still survives.

Tests: every `RUNTIME_CHANNELS` port must appear in `CHANNEL_OPTIONS`, and the rendered
option list must be `[0, 3456, 3457, 3458]`. Suite: 406 passing; tsc and biome clean.
Pressing Apply could sit on "Applying…" forever with no message and no way back but a
page reload.

Root cause is on the host side, not in this plugin: the settings write runs inside the
loader's exclusive queue (`config-editor` line 153 -> `hmr.runExclusive`), and that queue
is an unbounded FIFO (`hmr/src/index.ts:139-147`) — every exclusive operation awaits all
queued ones with no deadline. A write issued while another profile mutation is in flight
(a plugin install, a reload) is therefore *queued* rather than refused, and its Remote
promise does not settle until that operation finishes. Observed live: `package.json.lock`
created at the same second as `dsh plugin --profile web add @try-works/dsh-stt@0.2.0`,
held for minutes, while the write never landed and the profile still held the old value.

The plugin cannot fix the host queue, but it must not hang on it:

- `writeConfig` now bounds its wait (default 20s, overridable) and returns a message
  explaining that the host has not answered and another profile change may be holding
  the configuration queue.
- `onApply` clears `saving` outside the happy path, so a rejected, failed, or timed-out
  write all return the button to a usable state instead of freezing it.

Tests: a write the host accepts but never answers yields a message rather than hanging
(RED before the fix: the test timed out). Suite: 407 passing; tsc and biome clean.
@try-works try-works changed the title fix(dsh-role-model): make every runtime channel selectable in the settings page fix(dsh-role-model): settings panel - every channel selectable, and Apply can no longer hang Oct 7, 2026
Pressing Apply reported `cannot get property "remote" without inject`, and the form
rendered every field empty.

The guard that was supposed to make the Remote optional assumed `ctx.remote` answers
`undefined` when the service is absent. On the real client context it does not: a service
property that is not declared in `inject` **throws**, and `ctx.get` is the lookup that
answers `undefined`. So `settingsRemote(ctx)` threw on both the read and the write path:

- `readConfig` rejected, and because the panel called it as `void readConfig(ctx).then(...)`
  with no `catch`, the rejection was silent and the form never seeded — hence the blank
  endpoint/selectedAlias/providerRoute/requestTimeoutMs fields.
- `writeConfig` rejected before its own `try`, so Apply surfaced the raw Cordis message.
  It only became visible now because the previous change made `onApply` report failures
  instead of leaving the button stuck.

`settingsRemote` now reads `ctx.get("remote")` inside a guard, so an absent or
unreadable Remote degrades to "unavailable" rather than throwing. The client half keeps
injecting only `slots`; this is the lookup Cordis provides for services a caller must be
able to do without.

Tests: the client context double is now faithful to Cordis (a throwing `remote` property
plus a working `get`), which reproduces the reported error exactly — six specs failed with
`cannot get property "remote" without inject` before this fix. Suite: 409 passing; tsc and
biome clean.
@try-works try-works changed the title fix(dsh-role-model): settings panel - every channel selectable, and Apply can no longer hang fix(dsh-role-model): settings panel - representable channels, no hang on Apply, Remote read via ctx.get Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant