Repository navigation
feat(ui-web): split model providers from model settings, one picker - #565
Conversation
gloryfromca
left a comment
There was a problem hiding this comment.
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.
gloryfromca
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
51629cd to
a429814
Compare
gloryfromca
left a comment
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
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.
34281ba to
2860baa
Compare
gloryfromca
left a comment
There was a problem hiding this comment.
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.
…#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>
…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>
…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>
…#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>
…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>
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.optionsreturns (fifty-five on a stock registry,hosted_vllmandcustomamong 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 anOffer: 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.modelset to an id absent fromproviders.deepseek.models, the picker opens on DeepSeek and lists it first, marked, and writes nothing.One classifier, in Python.
registry_data.kind_offiles a model by what it writes, andModelLabel.kindcarries 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 ofinferred_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.kindandModelOptionProvider.gateway(the registry'sis_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, thennpm 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.tsxuntouched; the TUI.Type
Verification
Rebased onto
refactor/ui_web_architectureat 6062b36 and re-run after the rebase.Mutation checks on the two new backend fields: commenting out the
"gateway"line failstest_options_rows_carry_the_gateway_flagonKeyError: 'gateway'; commenting out the"kind"key failstest_model_labels_carry_a_kindonKeyError: 'kind'. Restored,git diffclean.Driven on a real host (
raven serveon 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-customoffers it with an "embedding" chip, which is whatinferred_tagsanswers 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.idvalues written elsewhere still land on a model page, since the roles page keeps the idmodel.A model the registry classifies wrongly -- an embedding model with no
embeddingtag 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 onegit revertaway.One gate pin moved:
fixture-shape'sUNSENTlist loses its sixmodel_labelsentries, 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.Related Issues
N/A