feat(ui): make a session that cannot send refuse to accept one

A default naming a route the Models page has since removed left the
composer saying 选择模型 while the input still accepted a message, which
then failed inside the adapter mid-turn.

`session.prompt` now refuses with `model-unavailable` before opening a
turn. That is the enforcement boundary: the method stays callable no
matter what a client disables. `session.models` reports the same fact as
`routable`, and ui-model pushes a block through the new
`ctx.conversation.blocks` registry so the bar renders the disabled
textarea it already renders without a workspace, carrying the blocker's
own reason. The push direction is forced — ui-model already depends on
ui-conversation, so ui-conversation cannot read it back.

The gate is `routable`, not "matches no advertised group": catalog
membership is advisory, so a route serving a model it stopped advertising
is missing from the groups yet perfectly usable, and `null` before the
first load never blocks so a slow Host cannot lock a working composer.

The scaffold gains a route-only adapter for fixture-less keyless
scenarios. Registering zero providers is a test artifact — every product
composition mounts one — and the goldens that froze the seat's fallback
label now show the model those scenarios actually route to.
This commit is contained in:
Yichen Jiang
2026-08-07 15:26:42 +08:00
parent 72618f29b5
commit bb43ff4f37
58 changed files with 859 additions and 163 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
README.md: 38f18995f2982db2c5a48971d7d044448e5adc8c
README.zh.md: c444ed6b7485b6ddca059c5edf7f30828bd96ab7
README.md: 9e01423a36803477cb07d944e058fc388b5e72fd
README.zh.md: d4df79d7d8850c466f1ccc4c53097a15739013ea