Skip to content

feat(ui-web): split model providers from model settings, one picker - #565

Merged
gloryfromca merged 14 commits into
refactor/ui_web_architecturefrom
feat/settings_model_section
Sep 21, 2026
Merged

gloryfromca merged 14 commits into
refactor/ui_web_architecturefrom
feat/settings_model_section

Conversation

@gloryfromca

Copy link
Copy Markdown
Member

Summary

The settings dialog's Model page answered two questions at once -- which accounts this install holds, and which model each job runs on -- and it listed the connected providers only, so the fifty-odd vendors Raven supports were reachable through a dropdown inside an "add" form and nowhere else. This splits the page in two, gives every model a kind, and merges the two model pickers into one.

Model providers is a two-column page. The left column is every provider model.options returns (fifty-five on a stock registry, hosted_vllm and custom among them) with a search box and six filters -- connected, direct vendors, aggregators, browser sign-in, local -- connected rows first and each carrying its vendor mark. The right column is that provider's connection, beside the list rather than paged into.

Model settings keeps the roles card alone, and each of its eleven slots offers its own kind: the embedding slot lists embedding models, image lists image models, and a chat model appears in neither.

Adding a model is a popover under its button. It fetches the vendor's list, groups a gateway's ids by vendor, counts the kinds present over the current search result and filters to the one pressed, and writes one model per click rather than collecting ticks for a confirming press. An id the vendor does not list is typed into the same box with a chip saying what kind it is.

One picker. components/ModelPicker (164 lines, the roles card's) is deleted; the roles card opens the composer's. What an opening lists is now an Offer: a kind, the providers the caller allows, a title, the pair it marks, and what a pick means.

Two decisions worth reading

The kind narrows each provider's column, never the provider list. A tester reported that a provider with a working key but no model list built vanished from the composer's picker, and the model the chip named -- set by onboarding, never "added" -- was in no column to be marked. So a provider with nothing of the offered kind is listed with a count of zero and a column that says so plus a row to type an id into; and the model a slot or the conversation currently holds is listed in its provider's column even when that column does not carry it. Verified on a real host: with agents.defaults.model set to an id absent from providers.deepseek.models, the picker opens on DeepSeek and lists it first, marked, and writes nothing.

One classifier, in Python. registry_data.kind_of files a model by what it writes, and ModelLabel.kind carries that answer to the page; nothing in TypeScript derives a kind from capabilities or modalities. The one exception is the chip on a typed id, which guesses from the name -- a translation of inferred_tags, with a nine-row table asserted on both sides so the copy cannot drift.

Backend

Two read-only, additive fields on model.options: ModelLabel.kind and ModelOptionProvider.gateway (the registry's is_gateway, which no client can derive from a slug). No new method, no new config key. Contract first: openrpc.json, then pydantic, then the handlers, then npm run gen.

Not in this change

The three navigation entries the prototype also adds (channels, cron, memory); the prototype's OAuth pane, which tells the reader to run a CLI command -- the browser device flow stays; backend enforcement of kinds, which remains a filter the page applies; the onboarding wizard, which keeps rendering providers/Providers.tsx untouched; the TUI.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Rebased onto refactor/ui_web_architecture at 6062b36 and re-run after the rebase.

uv run pytest tests/test_rpc_model.py tests/test_provider_registry_data.py tests/test_rpc_schema_match.py -q
630 passed in 12.44s

(cd ui-web && npx vitest run)
Test Files  186 passed (186)
     Tests  2552 passed (2552)

(cd ui-web && npx tsc --noEmit && npm run gen:check && npm run build && python3 build.py)
generated.ts matches the contract (189 methods)
boot-snapshot: OK (250 nodes match golden)
boot-snapshot: OK (251 nodes match golden)

make check-source-language; make check-large-files; npm run lint:i18n --prefix ui-tui
i18n: generated catalogue is up to date

uv run ruff check raven tests
All checks passed!

Mutation checks on the two new backend fields: commenting out the "gateway" line fails test_options_rows_carry_the_gateway_flag on KeyError: 'gateway'; commenting out the "kind" key fails test_model_labels_carry_a_kind on KeyError: 'kind'. Restored, git diff clean.

Driven on a real host (raven serve on its own home and port 18899, real keys for DeepSeek / OpenRouter / Gemini, a real browser):

  • Model providers draws 55 rows in two columns of 286px and 462px, the three connected ones first with a status dot, DeepSeek carrying the default badge, 54 vendor marks and one lettered tile (custom).

  • The embedding slot's picker lists all three connected providers with counts 1 / 0 / 0 and exactly one model. It is at the body, not inside the dialog, at z-index 46 over the veil's 40.

  • The add-model popover against OpenRouter's live catalogue: 534 models, kind tabs reading all 534 / text 445 / image 52 / embedding 33 / audio 4, five vendor groups, the four seeded models checked. Typing my-team/bge-custom offers it with an "embedding" chip, which is what inferred_tags answers for that name.

  • The composer's chip lists the same three providers with OpenRouter counted 1: its embedding, reranker and image models are not offered for chat.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

Composer users see two changes: embedding and reranker models leave the model chip's picker, and typing an id there now adds it to the provider before switching. Settings users learn two navigation entries where there was one; settingsTab.id values written elsewhere still land on a model page, since the roles page keeps the id model.

A model the registry classifies wrongly -- an embedding model with no embedding tag whose name the two patterns miss -- is invisible to the embedding slot until an overlay states its kind. The add-model popover's chip is the way to state it.

Rollback: the two wire fields are additive and optional on the reader's side; the section split is a data change in SECTIONS; the deleted picker is one git revert away.

One gate pin moved: fixture-shape's UNSENT list loses its six model_labels entries, because the offline fixtures now send them (a page that filters by kind needs a kind offline too). The gate's own message asks for exactly this.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: route missing-provider recovery and preserve typed non-text model kinds.

Reviewed the full target diff and the later head delta, including AGENTS.md / CLAUDE.md / CONTEXT*.md rules, the changed tests, callers, relevant history, backward compatibility, and the ui-web layering constraints. Three concrete blockers are marked inline.

Verification on this revision:

  • uv run pytest tests/test_rpc_model.py tests/test_provider_registry_data.py -q: 244 passed.
  • ui-web: npm test -- --run: 186 files, 2552 tests passed.
  • ui-web: npm run gen:check && npm run lint && npm run type-check: passed (five existing warnings, no errors).
  • ui-tui: npm run lint:rpc && npm run lint && npm run type-check: passed (existing warnings, no errors).
  • source-language and large-file gates: passed.

Comment thread ui-web/src/features/settings/SettingsApp.tsx
Comment thread ui-web/src/features/settings/providers/Roles.tsx Outdated
Comment thread ui-web/src/features/model/store.ts Outdated

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The current revision fixes all three prior blockers: both recovery doors reach Model providers, every empty role state has a working route there, and typed role picks preserve their stated kind. The corresponding regression tests exercise the concrete failures, and all three review threads are resolved.

Coverage included the repository rules in AGENTS.md and ui-web/CONTEXT.md, the full target diff and the current revision delta, affected callers and history, backward compatibility at the Offer/source and RPC seams, test changes for weakening, and the documented UI architecture boundaries. No new findings survived verification.

Verification:

  • ui-web: 186 test files, 2,556 tests passed
  • ui-web: RPC generation check, ESLint, and TypeScript passed (5 existing warnings, 0 errors)
  • backend: 244 focused model/provider tests passed
  • ui-tui: RPC generation check, ESLint, and TypeScript passed (23 existing warnings, 0 errors)
  • source-language and large-file gates passed

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The current revision restores the text-only registry fallback needed by the first-run model step, preserves empty non-text offers, keeps hand-added model kinds and gateway grouping intact, and removes the hidden store-to-component dependency. The subsequent head delta fixes the import ordering and documents the fallback; no new review finding survived verification.

Coverage included AGENTS.md/CLAUDE.md, CONTEXT-MAP.md and the relevant Runtime, Web UI, and TUI context rules; the full target diff plus both new deltas; affected callers and commit history; backward compatibility for older model sources and additive RPC fields; test changes for weakening; and the UI/RPC architecture boundaries.

Verification:

  • ui-web: 186 test files, 2,559 tests passed; RPC generation and TypeScript checks passed
  • ESLint: the full run found one import-order error plus 5 existing warnings; the latest head fixes that error, and its changed source passes ESLint directly
  • backend: 244 focused model/provider tests passed
  • ui-tui: RPC generation, ESLint, and TypeScript passed (23 existing warnings, 0 errors)
  • source-language and large-file gates passed against the latest head

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The new qualified/bare-id comparison fixes the reported duplicate rows and unreachable batch count for the direct-provider case. One nonblocking follow-up remains: isAdded() now trusts ModelCandidate.added, but that flag is the result of the one-time model.fetch_models call. After a catalogue row is removed, run() refreshes only the provider snapshot/configured list; the open sheet retains m.added: true. The row therefore remains checked and excluded from Add all until the popover is closed and reopened. Reopening refetches the catalogue and is a working escape hatch, so in this fifth review round this does not meet the blocking bar; it is named here rather than left as an open thread.

Verification at 737b718b5072: focused provider suite 18/18 passed; full web suite 186 files and 2,560 tests passed (with the suite's existing aborted localhost-fetch diagnostics); TypeScript check, changed-file ESLint, and full/delta git diff --check passed.

Coverage: repository rules (AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and Web UI context/conventions), the full branch diff as previously reviewed plus the complete new delta, affected callers and backend identity/removal paths, relevant history, backward compatibility with stored spellings, test-strength review, and Web UI source/store architecture. No prior settled finding was reopened, and all reviewer-started threads remain resolved.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The new delta correctly shares model-identity comparison between the provider popover and the picker, suppresses the qualified/bare duplicate in the covered path, and restores the current model to a provider column when it is absent from that provider's list. No new finding survived review. The sole nonblocking follow-up is the stale remove-state item already named in the previous revision's summary; this delta does not touch that path, so it is not repeated here.

Verification at 3ea41bff2d4d: affected picker/provider suites 63/63 passed; full web suite 186 files and 2,562 tests passed (with the suite's existing aborted localhost-fetch diagnostics); TypeScript check, changed-file ESLint, and full/delta git diff --check passed.

Coverage: repository rules and Web UI context/conventions, the previously reviewed full branch plus the complete new delta, picker/popover callers, backend model-identity history and compatibility spellings, test-strength review, and source/store architecture. All reviewer-started threads remain resolved.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

This test-only delta adds meaningful coverage for vendor-group ordering, counts, unheaded bare rows, and group-scoped batch addition. It changes no production behavior and introduces no new finding. The sole nonblocking follow-up remains the item already named on the previous revisions; this delta does not touch that path, so it is not repeated here.

Verification at 51629cdb16cf: provider-detail suite 19/19 passed; full web suite 186 files and 2,563 tests passed (with the suite's existing aborted localhost-fetch diagnostics); TypeScript check, changed-file ESLint, and full/delta git diff --check passed.

Coverage: repository rules and Web UI context/conventions, the previously reviewed full branch plus the complete test delta, the grouped-row renderer and batch caller, relevant history and compatibility behavior, test-strength review, and source/store architecture. All reviewer-started threads remain resolved.

@gloryfromca
gloryfromca requested a review from 0xKT September 21, 2026 07:11
@gloryfromca
gloryfromca force-pushed the feat/settings_model_section branch from 51629cd to a429814 Compare September 21, 2026 07:30

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The rebase is sound. Range-diffing the old and new series shows the reviewed commits are preserved, while the target branch's agent-specific components/ModelPicker remains for its new caller. The rebase repair restores that component's anchorRow implementation and CSS from the target, and the surviving picker tests cover its placement and no-typed-id contract. No new finding survived review. The sole nonblocking follow-up remains the item already named on prior revisions and is not repeated here.

Verification at a42981481462: full web suite 191 files and 2,580 tests passed; focused backend suites 244 tests passed; TypeScript and generated-client checks passed; full lint completed with zero errors and five pre-existing warnings on untouched files; full/delta git diff --check passed.

Coverage: repository rules and Web/Runtime context, the full current PR diff plus old-to-new range-diff, rebase conflict resolution, affected picker callers and history, backward compatibility, test-strength review, and component/domain/source architecture. All reviewer-started threads remain resolved.

gloryfromca and others added 14 commits September 21, 2026 15:55
Two read-only facts join `model.options`, so a surface can file a model by
what it is for and a list can be filtered by the kind of account behind it.

`ModelLabel.kind` is `registry_data.kind_of` over the row the label already
describes: the bucket a model belongs in, derived from what it WRITES, so a
model that reads pictures is still text. `model.fetch_models` has reported it
per row since it shipped; this puts the same answer on the rows a settings
page and a picker read, and keeps one classifier rather than two.

`ModelOptionProvider.gateway` is the registry's `is_gateway`: twenty-one of
the fifty-five providers resell other vendors' models under `vendor/model`
ids. No client can derive that from a slug, and a catalogue page that offers
to show only aggregators has to know.

Both are additive and optional on the reader's side. Contract first:
openrpc.json, then the pydantic models, then the handlers, then `npm run gen`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There were two. `features/model/ModelPicker` is the composer chip's: a portal
at the body, capability icons, the protocol control, a way into settings.
`components/ModelPicker` was the settings roles card's, 164 lines of the same
idea with different geometry. Adding a kind filter to the slots meant building
it in one of them and watching them drift, or in both.

So the roles card opens the composer's. What an opening lists is now an
`Offer`: a kind, the providers the caller allows, a title, the pair it marks,
and what a pick means. The composer passes nothing and gets today's behaviour;
a role slot passes its kind and its own write.

The kind narrows each provider's COLUMN, never the provider list. A provider
whose key works but whose model list nobody has built stays visible with a
count of zero and a column that says so -- hiding it told a reader with a
working DeepSeek key that DeepSeek was not connected. For the same reason the
model a slot or the conversation currently holds is listed in its provider's
column even when that column does not carry it: `agents.defaults.model` is
routinely set by onboarding or the CLI without a matching entry under
`providers.<slug>.models`, and a model the chip names has to be somewhere the
reader can see it marked.

Typing an id the provider does not list adds it there and then picks it, in
both openings, with a chip saying what kind it is -- the name's own guess, or
the slot's kind where a slot asked. `guessKind` is a translation of
`registry_data.inferred_tags`, and a nine-row table asserted on both sides
keeps the copy from drifting.

`HIDDEN_PROVIDERS` goes: it dropped the two generic rows from every surface,
and the catalogue page that replaces the old list has a search box and six
filters instead.

Deleted with the second picker: its test, its stylesheet block, and
`lib/popover.ts`'s `anchorRow`, which existed to keep a panel inside the
settings dialog. A body-level panel has no such problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The settings dialog's Model page answered two questions at once: which
accounts this install holds, and which model each job runs on. It showed the
connected providers only, so the fifty-odd vendors Raven supports were
reachable through a dropdown inside an "add" form and nowhere else.

Model providers is now its own page, two columns. The left is every provider
`model.options` returns -- fifty-five on a stock registry -- with a search box
and six filters (connected, direct vendors, aggregators, browser sign-in,
local), connected first and each row carrying its vendor mark. The right is
that provider's connection, drawn beside the list rather than paged into: with
twenty rows the paging was the cost, and the breadcrumb back to a list that is
now beside it has gone with it.

Model settings keeps the roles card alone, and each slot offers its own kind:
the embedding slot lists embedding models, image lists image models, and a
chat model no longer appears in either.

Adding a model is a popover under its button. It fetches the vendor's list,
groups a gateway's ids by vendor, counts the kinds present over the current
search result and filters to the one pressed, and writes one model per click
rather than collecting ticks for a confirming press. An id the vendor does not
list is typed into the same box, with a chip saying what kind it is; a vendor
with no list endpoint says so above the models already added rather than in
place of them.

The popover renders at the body. The dialog carries a transform for its entry
animation, which makes it both the containing block and the clip for a
`position: fixed` child, and a 470px panel opened from the right-hand pane lost
everything past the dialog's edge.

`providers/Providers.tsx` is untouched: the onboarding wizard's model step
renders it, and that step is not this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three documents this change was built from: what it is for and what it
deliberately does not do (A1..A38, C1..C22, the decisions and what each one
rejected), the cases that check it on a real host with the layer each one
reads, and the plan the tasks followed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI caught what the local run did not: I regenerated `ui-web`'s client from the
edited contract and not the TUI's, and never ran either project's eslint.

`ui-tui/src/rpc/generated.ts` is regenerated, and the hand-written mirror in
`gatewayTypes.ts` declares `gateway` on a provider row and `kind` on a model
label -- a drift test holds the two equal, so a field the contract sends and
the mirror omits is a failure by design. The TUI draws neither today; the
declaration is what keeps that a choice rather than an oversight.

Three import orderings in the files this branch touched, fixed by eslint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three sites that the page split left pointing at a page which no longer
answers their question. All three are review findings, all three now have a
case that goes red without the fix.

`openModels` and `openProviderModels` open Model providers. They answer
"nothing is connected" and "this one has no model added"; Model settings holds
the roles card and no way to connect anything, so the composer's own dead ends
were landing a reader somewhere they could do nothing.

A role slot with no provider connected draws a button, not a sentence, and the
button switches to Model providers -- a media slot selecting OpenRouter, since
that is the only vendor it may use. On a fresh install every slot is in this
state, so a passive line naming a page the reader has to find themselves was
the whole of the first-run path. The tab switch comes first: changing section
clears the selected provider, so naming the row before the switch names it
into state the switch is about to discard.

A typed id keeps the kind its chip was showing when it was clicked. The
picker already carried it; the slot's branch of `choose` dropped it, so
`add_model` stated nothing and an id the two name patterns miss -- "our-
finetune" typed into the embedding slot -- was stored as text and disappeared
from the slot it was typed into.

Also here: `nextKind` in one place instead of the same modulo arithmetic in
four, and `vi.restoreAllMocks()` in the settings store's teardown, where a
spy was leaking into the next case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Findings from the completeness and maintainability passes over the branch.

**The wizard's model step had no list to pick from.** The old settings picker
carried a fallback -- a provider with nothing added offers the registry's own
shortlist -- and merging the pickers dropped it. On a fresh install
`configured_models` is empty for everyone, so the wizard's chat pill opened a
column with nothing in it and a typed-id row. `column()` falls back to
`p.models` again, for a text opening only: an embedding slot asking for the
vendor's whole catalogue would be a worse answer than "nothing here yet".

**The address row is one component in two places.** An aggregator's address is
a first-class field and sits in the body beside the key; every other vendor's
is an override under Advanced. Both now get a reset once the stored value
differs from the registry's default -- an address typed by hand is not one you
can retype from memory. The old code had two copies of the input and no reset
at all.

**Disconnect says what it drops**: the authorisation, the server, or the key.

**A model added by hand keeps its kind in the popover.** `allRows` filed every
configured id the vendor's list does not carry under `text`, so a hand-added
embedding model sat under the Text tab while the row's own label said
otherwise.

**A gateway narrowed to one vendor keeps its group head**, and with it the
control that adds the whole vendor: the flat-list rule is "no prefixed id at
all", not "one group".

**The store no longer reaches into a page component.** `sheetToggleModel` was
pulling `rolesUsing` out of `Roles.tsx` through `await import()` -- a cycle
hidden from the dependency gate, whose specifier regex sees no dynamic import,
and the only such import in the tree. The refusal moves to the call site,
where the tag list beside it already refuses the same way.

Also: the dead `Sheet.sel` and `sheetToggle` the popover replaced; two
comments pointing at a `kinds.ts` that was merged into `types.ts`; `nextKind`
where the same modulo arithmetic was written four times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The import-sorting rule, on the line the previous commit added. Caught by CI
because I counted eslint's error tally instead of reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design described the kind as narrowing each provider's column and said
nothing about a provider with nothing added at all. The picker that was merged
away carried a fallback for exactly that -- the registry's shortlist, which is
what the onboarding wizard's model step picks from before any list exists --
and the omission is why it was dropped and had to be put back.

Now in the Design section, as a Decision with what it rejected, and as a case
(T4.7c) that fails if the column is empty for a text opening or if a non-text
slot gets the shortlist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found driving the real host for the acceptance pass. DeepSeek's popover drew
five rows for three models and offered "add all (3)"; pressing it added one and
then offered "add all (2)", forever.

`model.fetch_models` returns provider-qualified ids (`deepseek/deepseek-v4-pro`)
and a model added by hand is stored as it was typed (`deepseek-v4-pro`). The
backend calls those the same model -- `providers/wire.py`'s `merge_key` strips a
leading `<provider>/` and lowercases -- and had already answered the question
per row, as `added`. The popover ignored that and compared the strings, so a
model already on the provider read as not added, and the batch control counted
work that could never be done.

It now uses the wire's own answer, and falls back to the same stripping rule for
a row the vendor's list never named. On the real host the same popover is three
rows, two ticked, "add all (1)".

The case asserts all three: reverting to string equality reads back
`['false','false','false']` where `['true','true','false']` is right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The popover learned yesterday that `deepseek/deepseek-v4-pro` and
`deepseek-v4-pro` are one model. The picker had not, and the acceptance pass
found where it shows: type `my-embedder` into the embedding slot, and the
column lists it twice.

A role stores the spelling it was handed; `model.add_model` stores the one it
derived (`merge_key` qualifies it). So the slot's current pair says
`my-embedder` while the provider's list says `openrouter/my-embedder`, and
`column()` compared the two strings, decided the list did not carry the current
model, and pinned a second copy on top. Both rows read the same, because the
provider half is not drawn.

`bareModel` / `sameModel` move to `features/model/types.ts` -- one rule, the
backend's, where both surfaces can reach it. The popover's private copy goes.

Two cases, each red without it: the duplicate spelling, and the pinning itself
(which had no test at all -- the two I thought I had written went into the plan
document by mistake).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`groups()` was exported and drawn without a single case on it. The acceptance
pass is what found the hole: `model.fetch_models` qualifies every id with the
provider's own slug, so on a real host every non-empty list has exactly one
vendor head, and the branch that puts unprefixed rows last and draws them
without a head of their own is unreachable from the browser.

The case mixes both shapes: two vendors from the fetched list, and the two bare
ids the harness's provider already carries. It pins the head order, the counts,
the row order, and that the group's control offers only the rows that are not
on the provider yet.

Red twice: without the sort rule the bare rows lead; without the `name` guard a
third, empty head appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The base grew a second caller for `components/ModelPicker` while this branch
was open: the agents page (#559) opens it with `allowTyped={false}`, and its
list is not the model store's -- an ACP agent advertises its own choices,
grouped the way the agent grouped them, and none of that is in `model.options`.
So the deletion this branch made is kept for the two callers it did migrate
(the composer and the roles card) and the file stays for that one.

Two things the merge resolved cleanly and wrongly:

- `page.css` lost the `.model-picker*` rules, so the surviving picker would
  have rendered unstyled. Restored verbatim from the base, ahead of `.mpick`.
- `lib/popover.ts` lost `anchorRow` (with `bounds` and `DIALOG`), which that
  picker imports; `tsc` caught it. Restored, and the header comment now covers
  both pickers instead of claiming there is one.

Merging the agents page's picker needs `Offer` to carry a caller-supplied
provider list, which is a change to `column()` -- left out of this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They are one control implemented twice: search, the two columns, the count,
the tick, the typed row and the empty state correspond class for class, and
`PickerProvider` is a strict subset of the model domain's `Provider`. Calling
that a real distinction, as the rebase commit did, was wrong.

What actually blocks the merge is one optional field. The newer picker reads
`source().providers()`, where the older one takes them as a prop, so a caller
that owns its list -- the agents page, whose choices an ACP agent advertises --
has nothing to pass. `store.column()` needs no change at all: `offered` already
falls back to `models` when nothing is `configured`, and an untagged model
already reads as text.

What makes it more than an afternoon is verification, not code: moving that
picker means re-testing the agents page, which this run has no cases for.

Both files say so, so whoever picks it up reads it where they are looking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

This documentation-only delta accurately records why the rebased target retains its agent-specific picker and the concrete contract needed for later consolidation. It changes no runtime behavior, tests, compatibility, or architecture boundaries, and no new finding survived review. The sole nonblocking follow-up remains the item already named on prior revisions and is not repeated here.

Verification at 34281ba1380f: full web suite 191 files and 2,580 tests passed; TypeScript and generated-client checks, changed-file ESLint, and full/delta git diff --check passed.

Coverage: repository comment rules and Web UI context/conventions, the previously reviewed full branch plus the complete documentation delta, both picker callers and their history, compatibility assumptions, test-strength review, and component/domain architecture. All reviewer-started threads remain resolved.

@gloryfromca
gloryfromca force-pushed the feat/settings_model_section branch from 34281ba to 2860baa Compare September 21, 2026 07:59

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

At 2860baa64bde, the 14 pull-request commits are patch-for-patch identical to the previously reviewed series after rebasing onto target commit 649ea4728 (every git range-diff entry is =). I checked the target's new settings refresh changes against the provider/model split and found no semantic collision or new finding. The sole nonblocking follow-up already named on this change remains unchanged.

Verification: npm test (192 files, 2,583 tests), npm run type-check, npm run gen:check (189 methods), and uv run pytest tests/test_provider_registry_data.py tests/test_rpc_model.py -q (244 tests) all pass. npm run lint passes with 0 errors and the same 5 warnings on untouched files; git diff --check passes.

Coverage included AGENTS.md/CLAUDE.md and the UI context rules, the target diff and old/new range-diff, touched callers and history, RPC/generated-client and TUI backward compatibility, test-strength changes, and the documented picker/provider architecture constraints. All previously opened review threads are settled and resolved.

@gloryfromca
gloryfromca merged commit 3b35ee5 into refactor/ui_web_architecture Sep 21, 2026
21 checks passed
@gloryfromca
gloryfromca deleted the feat/settings_model_section branch September 21, 2026 08:24
0xKT pushed a commit that referenced this pull request Sep 21, 2026
…#591)

## Summary

Brings the web UI to the updated prototype in three parts.

**Playbooks is gone.** The domain, its page row, rail button, source
seam, fixtures and live gate. The `load_playbook` references in
`features/dag/` and `features/tasks/` stay: those are runtime facts, not
this page's.

**Schedules, channels and memory are sections of the settings dialog**
rather than module pages. The rail keeps two rows, the More fold and
`state/navfly.ts` are gone, and the settings nav is twelve sections.
Each of the three is drawn in a shared two-pane frame
(`components/TwoPane.tsx`), so the channels modal and the memory drawer
go with them: inside a dialog both were a layer over a layer, and the
list each covered is what a reader comparing two entries needs to keep.

Three decisions worth a reviewer's eye:

- The three islands stay their own React roots, mounted into boxes
`App.tsx` renders beside `#spanels`, because a root inside the settings
island's tree would be unmounted the moment the reader picked another
section. Which one is on screen is `data-section` on the veil, written
by the settings island on every draw.
- A domain that is a section registers what arriving at it costs on
`state/settings.ts`'s `onEnter`, and what leaving costs on `onLeave`, at
its own module evaluation. Registering those from `app/install.ts`
instead pulled three island stores into the page's own wiring and
roughly doubled the module-graph load of the transcript's own test.
- `#jobVeil` moved above `#setVeil` in the body order. The new-job sheet
is raised from inside the dialog now, and the two veils share a `--z`
step, so the order they sit in is the whole of the stacking decision.

**The delivery row and the scheduled turn are the prototype's.** A
delivery row is a capped name and a dot-and-one-word verdict. A graph's
verdict now comes out of the counts line inside its fence, so a run that
partly failed no longer reads as "finished" -- a graph's own status is
always `ok`, because the manager reports placement rather than outcome.
Its receipt is drawn as the two things it is: the machine block in the
mono face, then one captioned section per terminal node.

A turn a timer opened is the reader's own side of the thread, outlined
rather than filled because nobody typed it, headed by a chip and
carrying the instruction that fired. Only part of that entry is a
reader's to see: `cron_stack.py` writes four parts and two of them were
written for the reader (the parenthetical, whose own docstring says so,
and `Scheduled instruction:`), while the header and the closing "when
you reply, mention ..." are wording aimed at the model. An origin whose
shape nothing reads leaves the chip standing alone rather than guessing.

The offline fixture library gains three scheduled conversations, which
is where those two rows can be read at all. Between them they carry the
cron chip and all five verdicts a delivery row has: a spawn that
returned, one that failed, a graph that partly ran, one that was
stopped, and one whose node is waiting on a decision. That also takes
`session.resume`'s `origin` and `delegated` off the fixture gate's
UNSENT list.

Rebased onto `refactor/ui_web_architecture` after #565 landed the
provider/model split. Where the two met, this branch takes that one's
shape: its `pages/Provider.tsx`, its `ProviderSide`, its `openModels`
name and its nav wording. This branch adds the three sections around it.

Known follow-up, not in this change: there are now two two-pane frames
in the dialog. #565's `.settings-tp` is the settings domain's own, and
`components/TwoPane.tsx` is the shared one the other three domains use,
which cannot import a sibling's private components. Converging them
means lifting `.settings-tp` into the component.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from `ui-web/`:

- `npx vitest run` -- 188 files, 2452 tests, all passing. Includes the
gates under `scripts/gates/`.
- `npx tsc --noEmit -p tsconfig.json` -- clean.
- `npm run lint` -- 0 errors, 4 warnings, all of them pre-existing
`react-hooks/exhaustive-deps`.
- `node scripts/check-class-namespace.mjs` -- OK.
- `npm run build && python build.py` -- both boot snapshots match their
goldens.

Run from the repo root, over `45e11b4f3..HEAD`:

- `python scripts/check_source_language.py` -- exit 0.
- `python scripts/check_large_files.py` -- exit 0.

Checked by hand in Chrome against the prototype, light and dark: all
twelve settings sections, the two-pane detail in each, the channels scan
wizard, the new-job sheet over the dialog, and both message styles
across the three scheduled conversations.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

`ui-web/CONTEXT.md`, `CONTRIBUTING.md` and `README.md` are updated for
the vocabulary and the tables that moved. No screenshots attached:
`AGENTS.md` section 7 bars image assets from the repo.

## Risk

User-visible, and deliberately so:

- The playbooks page is gone. Nothing in the rail or the settings dialog
reaches it any more. Rolling back is reverting this commit; the
runtime's playbook support is untouched.
- Schedules, channels and memory are no longer rail destinations.
Anything that linked to them by page now opens the settings dialog on
the matching section.
- The three `gui.deleg.delivered*` strings are shorter, because the row
is a dot and a word rather than a sentence.
- A turn a timer opened now shows the job's instruction, which it did
not before. It still shows none of the wording written for the model.

No wire change: no new RPC method, no new field read, no contract edit.
The catalogue gains eight keys and shortens three. Rollback is a single
revert.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

On security: the delivery fold still draws only what sits inside the
untrusted fence, and the scheduled turn draws two named lines of a
reminder rather than the entry's text. Both are narrower than what
shipped before, not wider.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
0xKT added a commit that referenced this pull request Sep 21, 2026
…it (#594)

## Summary

The provider split (#565) moved model picking to one body-level picker
surface and left the compact provider card to the onboarding wizard
alone, but the wizard's first step was not re-aligned with it. This PR
does that, keeping the step in the prototype's shape.

**Regression fix.** The picker now lives at the picker layer (46)
outside the wizard, so opening it from a role pill inside the wizard
(110) drew it behind the wizard. It is lifted the same way the confirm
veil already is while the wizard is shown, and the layer gate pins the
ladder (picker token below the wizard, base rule on the token, lifted
rule present).

**Wizard add block, aligned with the prototype and the catalogue page.**
- The vendor select shows the catalogue page's four groups (direct,
gateway, browser sign-in, local). One `groupOf` answers both the
wizard's select and the catalogue's filter, in the catalogue's order (an
aggregator first, whatever credential it takes), so the two cannot
drift; labels are the catalogue's and no i18n key is added. A new test
pins the buckets, that every vendor lands in exactly one, the
connected-first order and the search needle.
- An aggregator gets the address row the catalogue's connection card
already gives it, prefilled from its default base. The base is sent
whenever the field is non-empty, as before.
- When a device flow lands, the add form that started it closes:
pointing at a now-connected slug, it used to redraw itself silently for
the next unconnected vendor.

**The settings catalogue's columns.** Found while trying the catalogue
in the wizard, fixed where it lives: the provider catalogue sized its
grid with a max-height, which a grid's fr row does not resolve against,
so both columns grew to the vendor list's height and were clipped -- the
list could not scroll past the first seventeen vendors and the "pick
one" placeholder sat below the fold. A definite height bounds the row
track, so the list and the detail pane scroll on their own; a gate reads
the sheet and pins it.

Decisions for the reviewer:
- Local vendors keep their optional key row (the prototype has none). It
matches the catalogue's connection card and costs nothing; drop it on
request.
- Known debt, not addressed here: the wizard's add block duplicates
about sixty lines of form rules from the catalogue's connection card.
Folding them into one component is a follow-up.
- Two small inconsistencies surfaced by review, left for that same
follow-up: the vendor tag on the catalogue's detail card still says
"Local / self-hosted" where the filter and the wizard say "Local
deployment" (identical in Chinese), and three address-row conditions
carry an `endpoint` term that the kind helper already implies.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

```
cd ui-web && npm test          # see the PR checks for the counts; includes the layer and catalogue gates
cd ui-web && npm run type-check  # clean
cd ui-web && npm run lint        # 0 errors, 4 pre-existing warnings (same on the base)
cd ui-web && node scripts/check-class-namespace.mjs   # OK
```

Real host: a gateway from this branch under an isolated RAVEN_HOME,
driven by Playwright on the first-run wizard. Before: the picker
computed z-index 46 and the element under its centre belonged to the
wizard. After: z-index 111 and the element under its centre is the
picker's search field; the vendor select shows the four groups with the
same counts the registry gives the catalogue page (direct 25, gateway
21, browser sign-in 4, local 5). In the settings dialog the catalogue's
columns measure one screenful and the list scrolls on its own (they
measured 2008px and were clipped before).

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

- [ ] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

UI only. The lift rule applies only while the wizard is open. The
settings dialog changes in two places: its catalogue columns are bounded
and scroll (they were clipped before), and its filter reads the shared
grouping, which files every one of the registry's 55 vendors exactly
where the old predicate did (recomputed over the registry). The store
change touches only the OAuth poll's success branch. Rollback is a plain
revert.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca added a commit that referenced this pull request Sep 21, 2026
…565)

## Summary

The settings dialog's Model page answered two questions at once -- which
accounts this install holds, and which model each job runs on -- and it
listed the connected providers only, so the fifty-odd vendors Raven
supports were reachable through a dropdown inside an "add" form and
nowhere else. This splits the page in two, gives every model a kind, and
merges the two model pickers into one.

**Model providers** is a two-column page. The left column is every
provider `model.options` returns (fifty-five on a stock registry,
`hosted_vllm` and `custom` among them) with a search box and six filters
-- connected, direct vendors, aggregators, browser sign-in, local --
connected rows first and each carrying its vendor mark. The right column
is that provider's connection, beside the list rather than paged into.

**Model settings** keeps the roles card alone, and each of its eleven
slots offers its own kind: the embedding slot lists embedding models,
image lists image models, and a chat model appears in neither.

**Adding a model** is a popover under its button. It fetches the
vendor's list, groups a gateway's ids by vendor, counts the kinds
present over the current search result and filters to the one pressed,
and writes one model per click rather than collecting ticks for a
confirming press. An id the vendor does not list is typed into the same
box with a chip saying what kind it is.

**One picker.** `components/ModelPicker` (164 lines, the roles card's)
is deleted; the roles card opens the composer's. What an opening lists
is now an `Offer`: a kind, the providers the caller allows, a title, the
pair it marks, and what a pick means.

### Two decisions worth reading

**The kind narrows each provider's column, never the provider list.** A
tester reported that a provider with a working key but no model list
built vanished from the composer's picker, and the model the chip named
-- set by onboarding, never "added" -- was in no column to be marked. So
a provider with nothing of the offered kind is listed with a count of
zero and a column that says so plus a row to type an id into; and the
model a slot or the conversation currently holds is listed in its
provider's column even when that column does not carry it. Verified on a
real host: with `agents.defaults.model` set to an id absent from
`providers.deepseek.models`, the picker opens on DeepSeek and lists it
first, marked, and writes nothing.

**One classifier, in Python.** `registry_data.kind_of` files a model by
what it writes, and `ModelLabel.kind` carries that answer to the page;
nothing in TypeScript derives a kind from capabilities or modalities.
The one exception is the chip on a typed id, which guesses from the name
-- a translation of `inferred_tags`, with a nine-row table asserted on
both sides so the copy cannot drift.

### Backend

Two read-only, additive fields on `model.options`: `ModelLabel.kind` and
`ModelOptionProvider.gateway` (the registry's `is_gateway`, which no
client can derive from a slug). No new method, no new config key.
Contract first: `openrpc.json`, then pydantic, then the handlers, then
`npm run gen`.

### Not in this change

The three navigation entries the prototype also adds (channels, cron,
memory); the prototype's OAuth pane, which tells the reader to run a CLI
command -- the browser device flow stays; backend enforcement of kinds,
which remains a filter the page applies; the onboarding wizard, which
keeps rendering `providers/Providers.tsx` untouched; the TUI.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Rebased onto `refactor/ui_web_architecture` at 6062b36 and re-run
after the rebase.

```
uv run pytest tests/test_rpc_model.py tests/test_provider_registry_data.py tests/test_rpc_schema_match.py -q
630 passed in 12.44s

(cd ui-web && npx vitest run)
Test Files  186 passed (186)
     Tests  2552 passed (2552)

(cd ui-web && npx tsc --noEmit && npm run gen:check && npm run build && python3 build.py)
generated.ts matches the contract (189 methods)
boot-snapshot: OK (250 nodes match golden)
boot-snapshot: OK (251 nodes match golden)

make check-source-language; make check-large-files; npm run lint:i18n --prefix ui-tui
i18n: generated catalogue is up to date

uv run ruff check raven tests
All checks passed!
```

Mutation checks on the two new backend fields: commenting out the
`"gateway"` line fails `test_options_rows_carry_the_gateway_flag` on
`KeyError: 'gateway'`; commenting out the `"kind"` key fails
`test_model_labels_carry_a_kind` on `KeyError: 'kind'`. Restored, `git
diff` clean.

Driven on a real host (`raven serve` on its own home and port 18899,
real keys for DeepSeek / OpenRouter / Gemini, a real browser):

- Model providers draws 55 rows in two columns of 286px and 462px, the
three connected ones first with a status dot, DeepSeek carrying the
default badge, 54 vendor marks and one lettered tile (`custom`).
- The embedding slot's picker lists all three connected providers with
counts 1 / 0 / 0 and exactly one model. It is at the body, not inside
the dialog, at z-index 46 over the veil's 40.
- The add-model popover against OpenRouter's live catalogue: 534 models,
kind tabs reading all 534 / text 445 / image 52 / embedding 33 / audio
4, five vendor groups, the four seeded models checked. Typing
`my-team/bge-custom` offers it with an "embedding" chip, which is what
`inferred_tags` answers for that name.
- The composer's chip lists the same three providers with OpenRouter
counted 1: its embedding, reranker and image models are not offered for
chat.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Composer users see two changes: embedding and reranker models leave the
model chip's picker, and typing an id there now adds it to the provider
before switching. Settings users learn two navigation entries where
there was one; `settingsTab.id` values written elsewhere still land on a
model page, since the roles page keeps the id `model`.

A model the registry classifies wrongly -- an embedding model with no
`embedding` tag whose name the two patterns miss -- is invisible to the
embedding slot until an overlay states its kind. The add-model popover's
chip is the way to state it.

Rollback: the two wire fields are additive and optional on the reader's
side; the section split is a data change in `SECTIONS`; the deleted
picker is one `git revert` away.

One gate pin moved: `fixture-shape`'s `UNSENT` list loses its six
`model_labels` entries, because the offline fixtures now send them (a
page that filters by kind needs a kind offline too). The gate's own
message asks for exactly this.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gloryfromca pushed a commit that referenced this pull request Sep 21, 2026
…#591)

Brings the web UI to the updated prototype in three parts.

**Playbooks is gone.** The domain, its page row, rail button, source
seam, fixtures and live gate. The `load_playbook` references in
`features/dag/` and `features/tasks/` stay: those are runtime facts, not
this page's.

**Schedules, channels and memory are sections of the settings dialog**
rather than module pages. The rail keeps two rows, the More fold and
`state/navfly.ts` are gone, and the settings nav is twelve sections.
Each of the three is drawn in a shared two-pane frame
(`components/TwoPane.tsx`), so the channels modal and the memory drawer
go with them: inside a dialog both were a layer over a layer, and the
list each covered is what a reader comparing two entries needs to keep.

Three decisions worth a reviewer's eye:

- The three islands stay their own React roots, mounted into boxes
`App.tsx` renders beside `#spanels`, because a root inside the settings
island's tree would be unmounted the moment the reader picked another
section. Which one is on screen is `data-section` on the veil, written
by the settings island on every draw.
- A domain that is a section registers what arriving at it costs on
`state/settings.ts`'s `onEnter`, and what leaving costs on `onLeave`, at
its own module evaluation. Registering those from `app/install.ts`
instead pulled three island stores into the page's own wiring and
roughly doubled the module-graph load of the transcript's own test.
- `#jobVeil` moved above `#setVeil` in the body order. The new-job sheet
is raised from inside the dialog now, and the two veils share a `--z`
step, so the order they sit in is the whole of the stacking decision.

**The delivery row and the scheduled turn are the prototype's.** A
delivery row is a capped name and a dot-and-one-word verdict. A graph's
verdict now comes out of the counts line inside its fence, so a run that
partly failed no longer reads as "finished" -- a graph's own status is
always `ok`, because the manager reports placement rather than outcome.
Its receipt is drawn as the two things it is: the machine block in the
mono face, then one captioned section per terminal node.

A turn a timer opened is the reader's own side of the thread, outlined
rather than filled because nobody typed it, headed by a chip and
carrying the instruction that fired. Only part of that entry is a
reader's to see: `cron_stack.py` writes four parts and two of them were
written for the reader (the parenthetical, whose own docstring says so,
and `Scheduled instruction:`), while the header and the closing "when
you reply, mention ..." are wording aimed at the model. An origin whose
shape nothing reads leaves the chip standing alone rather than guessing.

The offline fixture library gains three scheduled conversations, which
is where those two rows can be read at all. Between them they carry the
cron chip and all five verdicts a delivery row has: a spawn that
returned, one that failed, a graph that partly ran, one that was
stopped, and one whose node is waiting on a decision. That also takes
`session.resume`'s `origin` and `delegated` off the fixture gate's
UNSENT list.

Rebased onto `refactor/ui_web_architecture` after #565 landed the
provider/model split. Where the two met, this branch takes that one's
shape: its `pages/Provider.tsx`, its `ProviderSide`, its `openModels`
name and its nav wording. This branch adds the three sections around it.

Known follow-up, not in this change: there are now two two-pane frames
in the dialog. #565's `.settings-tp` is the settings domain's own, and
`components/TwoPane.tsx` is the shared one the other three domains use,
which cannot import a sibling's private components. Converging them
means lifting `.settings-tp` into the component.

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

Run from `ui-web/`:

- `npx vitest run` -- 188 files, 2452 tests, all passing. Includes the
gates under `scripts/gates/`.
- `npx tsc --noEmit -p tsconfig.json` -- clean.
- `npm run lint` -- 0 errors, 4 warnings, all of them pre-existing
`react-hooks/exhaustive-deps`.
- `node scripts/check-class-namespace.mjs` -- OK.
- `npm run build && python build.py` -- both boot snapshots match their
goldens.

Run from the repo root, over `45e11b4f3..HEAD`:

- `python scripts/check_source_language.py` -- exit 0.
- `python scripts/check_large_files.py` -- exit 0.

Checked by hand in Chrome against the prototype, light and dark: all
twelve settings sections, the two-pane detail in each, the channels scan
wizard, the new-job sheet over the dialog, and both message styles
across the three scheduled conversations.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

`ui-web/CONTEXT.md`, `CONTRIBUTING.md` and `README.md` are updated for
the vocabulary and the tables that moved. No screenshots attached:
`AGENTS.md` section 7 bars image assets from the repo.

User-visible, and deliberately so:

- The playbooks page is gone. Nothing in the rail or the settings dialog
reaches it any more. Rolling back is reverting this commit; the
runtime's playbook support is untouched.
- Schedules, channels and memory are no longer rail destinations.
Anything that linked to them by page now opens the settings dialog on
the matching section.
- The three `gui.deleg.delivered*` strings are shorter, because the row
is a dot and a word rather than a sentence.
- A turn a timer opened now shows the job's instruction, which it did
not before. It still shows none of the wording written for the model.

No wire change: no new RPC method, no new field read, no contract edit.
The catalogue gains eight keys and shortens three. Rollback is a single
revert.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

On security: the delivery fold still draws only what sits inside the
untrusted fence, and the scheduled turn draws two named lines of a
reminder rather than the entry's text. Both are narrower than what
shipped before, not wider.

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
gloryfromca pushed a commit that referenced this pull request Sep 21, 2026
…it (#594)

## Summary

The provider split (#565) moved model picking to one body-level picker
surface and left the compact provider card to the onboarding wizard
alone, but the wizard's first step was not re-aligned with it. This PR
does that, keeping the step in the prototype's shape.

**Regression fix.** The picker now lives at the picker layer (46)
outside the wizard, so opening it from a role pill inside the wizard
(110) drew it behind the wizard. It is lifted the same way the confirm
veil already is while the wizard is shown, and the layer gate pins the
ladder (picker token below the wizard, base rule on the token, lifted
rule present).

**Wizard add block, aligned with the prototype and the catalogue page.**
- The vendor select shows the catalogue page's four groups (direct,
gateway, browser sign-in, local). One `groupOf` answers both the
wizard's select and the catalogue's filter, in the catalogue's order (an
aggregator first, whatever credential it takes), so the two cannot
drift; labels are the catalogue's and no i18n key is added. A new test
pins the buckets, that every vendor lands in exactly one, the
connected-first order and the search needle.
- An aggregator gets the address row the catalogue's connection card
already gives it, prefilled from its default base. The base is sent
whenever the field is non-empty, as before.
- When a device flow lands, the add form that started it closes:
pointing at a now-connected slug, it used to redraw itself silently for
the next unconnected vendor.

**The settings catalogue's columns.** Found while trying the catalogue
in the wizard, fixed where it lives: the provider catalogue sized its
grid with a max-height, which a grid's fr row does not resolve against,
so both columns grew to the vendor list's height and were clipped -- the
list could not scroll past the first seventeen vendors and the "pick
one" placeholder sat below the fold. A definite height bounds the row
track, so the list and the detail pane scroll on their own; a gate reads
the sheet and pins it.

Decisions for the reviewer:
- Local vendors keep their optional key row (the prototype has none). It
matches the catalogue's connection card and costs nothing; drop it on
request.
- Known debt, not addressed here: the wizard's add block duplicates
about sixty lines of form rules from the catalogue's connection card.
Folding them into one component is a follow-up.
- Two small inconsistencies surfaced by review, left for that same
follow-up: the vendor tag on the catalogue's detail card still says
"Local / self-hosted" where the filter and the wizard say "Local
deployment" (identical in Chinese), and three address-row conditions
carry an `endpoint` term that the kind helper already implies.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

```
cd ui-web && npm test          # see the PR checks for the counts; includes the layer and catalogue gates
cd ui-web && npm run type-check  # clean
cd ui-web && npm run lint        # 0 errors, 4 pre-existing warnings (same on the base)
cd ui-web && node scripts/check-class-namespace.mjs   # OK
```

Real host: a gateway from this branch under an isolated RAVEN_HOME,
driven by Playwright on the first-run wizard. Before: the picker
computed z-index 46 and the element under its centre belonged to the
wizard. After: z-index 111 and the element under its centre is the
picker's search field; the vendor select shows the four groups with the
same counts the registry gives the catalogue page (direct 25, gateway
21, browser sign-in 4, local 5). In the settings dialog the catalogue's
columns measure one screenful and the list scrolls on its own (they
measured 2008px and were clipped before).

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

- [ ] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

UI only. The lift rule applies only while the wizard is open. The
settings dialog changes in two places: its catalogue columns are bounded
and scroll (they were clipped before), and its filter reads the shared
grouping, which files every one of the registry's 55 vendors exactly
where the old predicate did (recomputed over the registry). The store
change touches only the OAuth poll's success branch. Rollback is a plain
revert.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
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.

2 participants