From a0c5d1c8f2278873a920bd0194c21644da488ccf Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:32:53 +0900 Subject: [PATCH 1/2] docs: plan catalog chain readiness reconciliation --- .../_plan/260912_catalog_lane_readiness/000_plan.md | 11 +++++++++++ .../260912_catalog_lane_readiness/010_roadmap.md | 2 ++ .../011_design_reflection.md | 2 ++ .../_plan/260912_catalog_lane_readiness/020_review.md | 4 ++++ .../260912_catalog_lane_readiness/030_evidence.md | 6 ++++++ 5 files changed, 25 insertions(+) create mode 100644 devlog/_plan/260912_catalog_lane_readiness/000_plan.md create mode 100644 devlog/_plan/260912_catalog_lane_readiness/010_roadmap.md create mode 100644 devlog/_plan/260912_catalog_lane_readiness/011_design_reflection.md create mode 100644 devlog/_plan/260912_catalog_lane_readiness/020_review.md create mode 100644 devlog/_plan/260912_catalog_lane_readiness/030_evidence.md diff --git a/devlog/_plan/260912_catalog_lane_readiness/000_plan.md b/devlog/_plan/260912_catalog_lane_readiness/000_plan.md new file mode 100644 index 0000000000..f961b79e73 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/000_plan.md @@ -0,0 +1,11 @@ +# Catalog chain readiness + +Review the existing catalog chain without duplicating its implementation. The lane produces a precise integration handoff; an additional product PR exists only if a concrete defect or necessary regression gap remains. + +Loop: satisfy-spec, triggered by the catalog lane delegation. Class C3 review; security changes would promote their slice to C4. Goal: readiness for #4325 -> #4328 -> #4331. Non-goals: merging, original branch writes, issue closure, release, services and configuration. Local suites of every size are NOT RUN, including wrappers; large local install/build/typecheck are also excluded. Only task-owned files and scoped commits/push --no-verify/PR creation are authorized. Existing GitHub credentials only; no user token/time/agent-count bound. + +Verifier: live gh PR/review/run JSON and Git ancestry/source inspection observe exact catalog tips; product suites execute only on hosted CI. Text checks observe these documents, never establish product test success. Stop: durable exact-head evidence/disposition plus honest remaining acceptance. Outcomes: DONE for completed readiness scope, NOOP for existing sufficient implementation, NEEDS_HUMAN for unresolved integration decisions, BLOCKED only for demonstrated unavailable prerequisites. Memory artifact: this unit and ignored .tmp/catalog-review/HANDOFF.md. Escalation: original-task write collision or necessary authority beyond scope goes to parent; two failed distinct reviewer calls are reclaimed with independent-review gap recorded. + +Dependency order: roadmap (010), scoped review/coverage (020), final hosted CI and GUI evidence (030). No native stacks. Preserve public author commits. Source of truth: structure/gui-and-management-api.md, changed only if a product contract changes. No new fields, enums, enforcement or interfaces planned. Native architect role is not exposed; supported inherited-model design review and reflection provide consultation per explicit user direction, without claiming native role selection. User explicitly instructs independent work to continue when tools are unavailable. + +Parent scope correction: existing owner is merging its own chain; this lane reconciles read-only, with own follow-up only for a concrete newly verified defect. Supported inherited-model independent design review replaces the unavailable native-role transport per explicit user direction, without claiming native architect selection. diff --git a/devlog/_plan/260912_catalog_lane_readiness/010_roadmap.md b/devlog/_plan/260912_catalog_lane_readiness/010_roadmap.md new file mode 100644 index 0000000000..51958d8370 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/010_roadmap.md @@ -0,0 +1,2 @@ +# Roadmap documentation cycle +NEW 000_plan.md and decade documents 010/020/030 in this unit; before: absent; after: outcome, authority, exact read targets and acceptance. NEW .tmp/catalog-review/HANDOFF.md: identity, current PR states and evidence pointers. No product delta. Check: read all four documents and git diff --check; confirm every phase has real outputs and user restrictions. D locks this roadmap and directs the next cycle to review exact tip source. diff --git a/devlog/_plan/260912_catalog_lane_readiness/011_design_reflection.md b/devlog/_plan/260912_catalog_lane_readiness/011_design_reflection.md new file mode 100644 index 0000000000..51fe050002 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/011_design_reflection.md @@ -0,0 +1,2 @@ +# Design review dispositions +Inherited-model read-only reviewer Hilbert supplied CAT-DEC-01..06. Main accepts evidence ownership (01), exact-head provenance (02), no-suite restrictions (05). Amended collision boundary (03) to require a concrete new defect plus owner/head refresh and parent coordination. Amended consultation (04) to distinguish supported independent design reflection from unavailable native architect role. Amended integration authority (06) to preserve separately authorized original-owner merges and parent-only follow-up integration. Reflection recheck requested after amendments. diff --git a/devlog/_plan/260912_catalog_lane_readiness/020_review.md b/devlog/_plan/260912_catalog_lane_readiness/020_review.md new file mode 100644 index 0000000000..38c3be5e77 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/020_review.md @@ -0,0 +1,4 @@ +# Scoped review and coverage cycle +Depends on roadmap. READ exact #4331 head gui/src/components/AddProviderModal.tsx, provider-catalog/ProviderCatalog.tsx, CatalogAccountRow.tsx, ProviderNoteModal.tsx, provider-presets.ts, gui/tests/provider-catalog-search.test.tsx and tests/gui/provider-workspace-data.test.ts. READ #4328 diff and live review threads. NEW .tmp/catalog-review/020_review.md: file:line findings, dispositions, remaining acceptance and attribution. Before: no independent lane review; after: a checked result against the pinned SHA. + +Potential MODIFY gui/tests/provider-catalog-search.test.tsx only for a concrete defect absent from the existing owner work, after refreshing owner/head evidence and parent coordination. A coverage gap alone is not authority to duplicate the owner delivery. Activate Escape with a nonempty query then empty query; expect query clear before modal close. Activate ArrowDown from search with a disabled first account control and later enabled controls; expect the first enabled result to receive focus. Also inspect no-actionable-result behavior and note-popup -> query -> dialog Escape ordering. If required, append a precise repair work-phase at P and use a separate task-owned child branch of the refreshed final tip; never edit the original branch. Reuse existing tests and source docs, no speculative abstraction. No test execution locally. Check: independent source review, diff --check and GitHub-hosted CI for any new code. If no patch is justified, record NOOP explicitly. diff --git a/devlog/_plan/260912_catalog_lane_readiness/030_evidence.md b/devlog/_plan/260912_catalog_lane_readiness/030_evidence.md new file mode 100644 index 0000000000..763f2c5965 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/030_evidence.md @@ -0,0 +1,6 @@ +# Final evidence and handoff cycle +Depends on reviewed source/repair disposition. READ live gh pr view for #4325/#4328/#4331 and any follow-up, GraphQL reviewThreads, gh run view for exact head, workflow triggers and Git ancestry. READ screenshots carried by the source PR using local git blobs; observe them with image viewer, distinguish screenshot commit from final source head. NEW .tmp/catalog-review/030_evidence.md with CI run IDs/URLs, job outcomes, missing/skipped distinctions, GUI provenance and outstanding reviews. MODIFY .tmp/catalog-review/HANDOFF.md from preliminary to complete: worktree/branch, own phase/cycle evidence, all original PR dispositions, final chain/head SHAs, authors, remaining acceptance and local tests NOT RUN. No product change. Check: exact SHA equality between PR and CI plus fresh PR state; do not claim intermediate tips passed. This lane never merges or retargets. Reconcile the original-chain delivery by its separately authorized existing owner; parent controls additional follow-up integration. + +Inspect exact-tip hosted dashboard preview artifact if available; compare narrow-width tabs and popup focus against historical screenshots. If preview cannot be exercised within authorized no-build/no-install scope, leave final-tip dynamic GUI acceptance explicitly unmet, not inferred from old PNGs. + +Record observation timestamp, base SHA, workflow event/run attempt and merge commit. Head/base movement triggers reconciliation refresh. From 22cdbfc9a96b58041c10da0aec42f4a0dc6206f4 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:37:58 +0900 Subject: [PATCH 2/2] fix(gui): skip disabled catalog actions on ArrowDown Preserve the existing keyboard path while OpenAI enablement pins a disabled account action before searchable presets. Add behavior coverage for disabled-first, disabled-only, empty and normal results. --- .../022_keyboard_repair.md | 13 ++++ .../src/content/docs/guides/web-dashboard.md | 2 +- .../provider-catalog/ProviderCatalog.tsx | 2 +- gui/tests/provider-catalog-search.test.tsx | 63 +++++++++++++++++++ structure/gui-and-management-api.md | 2 +- 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 devlog/_plan/260912_catalog_lane_readiness/022_keyboard_repair.md diff --git a/devlog/_plan/260912_catalog_lane_readiness/022_keyboard_repair.md b/devlog/_plan/260912_catalog_lane_readiness/022_keyboard_repair.md new file mode 100644 index 0000000000..ff3a9ebcf1 --- /dev/null +++ b/devlog/_plan/260912_catalog_lane_readiness/022_keyboard_repair.md @@ -0,0 +1,13 @@ +# Keyboard repair plan + +Previous D locked the docs-only roadmap; source reconciliation now establishes one new defect. The original owner merged #4331 at 9a37813593514c2d90b1ebac129c4541fd2a9af4; its reviewed source tip a154645d76e98af199fd79aff8c8d393afaf30ab passed hosted CI 34672274572. This task rebased only its own unpushed roadmap commit onto that dev tip. Parent was notified of the new distinct defect; original task scope readback shows only tab overflow, description disclosure and popup focus repairs. + +Class C1 behavioral patch plus existing-test coverage; no new abstraction, type, field, token, endpoint, UI copy or dependency. Do-nothing would retain a broken keyboard path; configuration cannot change the selector; reuse the existing handler and test mount. Product diff is confined to the existing selector. + +MODIFY gui/src/components/provider-catalog/ProviderCatalog.tsx:207: before querySelector("button, a[href]"); after querySelector("button:not(:disabled), a[href]"). CSS :disabled also excludes a disabled fieldset descendant, while preserving actionable anchors. + +MODIFY gui/tests/provider-catalog-search.test.tsx: append behavior tests using current mount/type/search helpers. Busy openai Codex row (logged out, onAccountLogin supplied), query nvidia: disabled account button is first in DOM, ArrowDown must focus NVIDIA preset and prevent default. Same busy account with unmatched query and no other actionable row: focus stays on search and default remains untouched. Empty results: same no-op. A normal preset-only query checks normal first-result focus. All tests dispatch a bubbling/cancelable KeyboardEvent from the focused input inside act. No sleep helper or exported test-only production function. + +MODIFY structure/gui-and-management-api.md Add provider row: ArrowDown focuses first enabled result action; no available action leaves input focus unchanged. MODIFY docs-site/src/content/docs/guides/web-dashboard.md Add provider row with the same keyboard behavior, translated pages must not contradict (they currently say nothing about this shortcut). + +Verification: git diff --check for patch formatting ONLY, independent source audit, GitHub-hosted Cross-platform CI at the exact published head. No local test/build/typecheck/install. Read hosted preview artifact from that run if GUI evidence requires it; serve artifact in scratch without product build, no live proxy mutation. No original branch/PR mutation, merge or auto-merge. Existing author commits stay in ancestry. Follow-up ordinary PR targets dev because original chain is now merged. diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index a077b39f28..6550f404fe 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -87,7 +87,7 @@ badge or the version value to read the full value. | **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. | | **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. | | **Providers** | Add, edit, set the default (enabled providers only), enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Removing the current default switches to the first remaining enabled provider when one exists; otherwise deletion is refused and the current default is kept. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. For Claude (Anthropic) OAuth pools, each logged-in account shows its own 5-hour and weekly rate-limit bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. The Provider Overview shown when no provider is selected carries a **Refresh all quotas** control that forces one server-side re-read of every configured provider; a provider whose upstream probe fails keeps its last-good row, so the status line reports that the check completed rather than claiming every value is fresh, and each row's own age stays the per-provider freshness signal. | -| **Add provider** | One search above the tabs reaches all four at once — Accounts, Free, Local, Paid. While a query is live the results are grouped by tab with a count each, and the selected tab stays put rather than jumping. Local runtimes (Ollama, vLLM, LM Studio, LiteLLM) have their own tab, and a long provider note clamps to two lines with the full text one click away. | +| **Add provider** | One search above the tabs reaches all four at once — Accounts, Free, Local, Paid. While a query is live the results are grouped by tab with a count each, and the selected tab stays put rather than jumping. Press ArrowDown in the search box to focus the first available result action, skipping disabled buttons; if no action is available, focus stays in the search box. Local runtimes (Ollama, vLLM, LM Studio, LiteLLM) have their own tab, and a long provider note clamps to two lines with the full text one click away. | | **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. | | **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. | | **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. | diff --git a/gui/src/components/provider-catalog/ProviderCatalog.tsx b/gui/src/components/provider-catalog/ProviderCatalog.tsx index b0e7f50992..c342461733 100644 --- a/gui/src/components/provider-catalog/ProviderCatalog.tsx +++ b/gui/src/components/provider-catalog/ProviderCatalog.tsx @@ -204,7 +204,7 @@ export default function ProviderCatalog({ /** ArrowDown out of the input lands on the first result, never on a chip. */ const onSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key !== "ArrowDown") return; - const first = rowsRef.current?.querySelector("button, a[href]"); + const first = rowsRef.current?.querySelector("button:not(:disabled), a[href]"); if (!first) return; e.preventDefault(); first.focus(); diff --git a/gui/tests/provider-catalog-search.test.tsx b/gui/tests/provider-catalog-search.test.tsx index c4e067e3bc..8d1199218e 100644 --- a/gui/tests/provider-catalog-search.test.tsx +++ b/gui/tests/provider-catalog-search.test.tsx @@ -145,3 +145,66 @@ test("a login in flight survives a query that does not match its row", async () expect(rows).toContain("Cursor"); expect(rows).not.toContain("Anthropic"); }); + +test("ArrowDown skips a disabled account action before an enabled preset result", async () => { + await mount({ + accountRows: [{ id: "openai", label: "OpenAI", kind: "codex" }], + accountBusy: "openai", + onAccountLogin: () => {}, + }); + await type("nvidia"); + const firstButton = win.document.querySelector(".provider-catalog-rows button"); + expect(firstButton?.disabled).toBe(true); + const target = win.document.querySelector(".provider-catalog-row-wrap > button"); + expect(target?.textContent).toContain("NVIDIA NIM"); + const input = search(); + input.focus(); + const event = new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }); + await act(async () => { input.dispatchEvent(event as never); }); + expect(win.document.activeElement).toBe(target); + expect(event.defaultPrevented).toBe(true); +}); + +test("ArrowDown leaves search focused when the only result action is disabled", async () => { + await mount({ + accountRows: [{ id: "openai", label: "OpenAI", kind: "codex" }], + accountBusy: "openai", + onAccountLogin: () => {}, + }); + await type("no-provider-matches"); + const buttons = win.document.querySelectorAll(".provider-catalog-rows button"); + expect(buttons).toHaveLength(1); + expect(buttons[0]?.disabled).toBe(true); + expect(win.document.querySelector(".provider-catalog-rows a[href]")).toBeNull(); + const input = search(); + input.focus(); + const event = new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }); + await act(async () => { input.dispatchEvent(event as never); }); + expect(win.document.activeElement).toBe(input); + expect(event.defaultPrevented).toBe(false); +}); + +test("ArrowDown leaves search focused when there are no results", async () => { + await mount(); + await type("no-provider-matches"); + expect(win.document.querySelector(".provider-catalog-rows button, .provider-catalog-rows a[href]")).toBeNull(); + const input = search(); + input.focus(); + const event = new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }); + await act(async () => { input.dispatchEvent(event as never); }); + expect(win.document.activeElement).toBe(input); + expect(event.defaultPrevented).toBe(false); +}); + +test("ArrowDown moves directly to the first preset result", async () => { + await mount(); + await type("nvidia"); + const target = win.document.querySelector(".provider-catalog-row-wrap > button"); + expect(target?.textContent).toContain("NVIDIA NIM"); + const input = search(); + input.focus(); + const event = new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }); + await act(async () => { input.dispatchEvent(event as never); }); + expect(win.document.activeElement).toBe(target); + expect(event.defaultPrevented).toBe(true); +}); diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index f4c3e27d6c..551e8dc472 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -308,7 +308,7 @@ single forms, and the shell pattern is the part worth keeping stable: | Storage | Rail plus cleanup and trash detail (`gui/src/components/storage-workspace/`). | | Subagents | Featured-roster selection workspace (`gui/src/components/subagents-workspace/`). | | Combos | Rail, detail panel, and an add flow (`gui/src/components/ComboWorkspace.tsx`). | -| Add provider | Catalog browser plus form and OAuth panes (`gui/src/components/provider-catalog/`, `gui/src/components/AddProviderModal.tsx`). The catalog browses four tabs — Accounts, Free, Local, Paid — where Local is a catalog-only bucket peeled out of `bucketPresets` after `presetTier` has classified; the workspace `providerTier` stays three-way, so the rail, the free-paid sort and the Free count still treat a local runtime as free. Search sits above the tabs and reaches every tab at once: while a query is live the list renders all four groups with headings and the strip becomes jump chips with counts rather than a tablist, because moving the selected tab would change the row kind under the user (a preset-select button becomes a login row). The tab strip wraps within narrow modals. Every nonempty note has a full-text button so narrow rows never hide content permanently; the native note dialog closes during teardown and restores focus to its trigger. Provider notes clamp to two lines and open in full in a stacked native `` owned by `AddProviderModal`, which also owns the search text so its `window` Escape handler can unwind popup, then query, then dialog. | +| Add provider | Catalog browser plus form and OAuth panes (`gui/src/components/provider-catalog/`, `gui/src/components/AddProviderModal.tsx`). The catalog browses four tabs — Accounts, Free, Local, Paid — where Local is a catalog-only bucket peeled out of `bucketPresets` after `presetTier` has classified; the workspace `providerTier` stays three-way, so the rail, the free-paid sort and the Free count still treat a local runtime as free. Search sits above the tabs and reaches every tab at once: while a query is live the list renders all four groups with headings and the strip becomes jump chips with counts rather than a tablist, because moving the selected tab would change the row kind under the user (a preset-select button becomes a login row). ArrowDown from the search input focuses the first enabled result action; if none is available, focus stays in the input. The tab strip wraps within narrow modals. Every nonempty note has a full-text button so narrow rows never hide content permanently; the native note dialog closes during teardown and restores focus to its trigger. Provider notes clamp to two lines and open in full in a stacked native `` owned by `AddProviderModal`, which also owns the search text so its `window` Escape handler can unwind popup, then query, then dialog. | | Codex accounts | Account pool cards, add-account flow, switch and reset modals (`gui/src/components/CodexAccountPool.tsx`, `gui/src/components/AddCodexAccountModal.tsx`), plus the generic account-targeting picker opt-in on `gui/src/pages/codex-set-multiauth.tsx`. Add/delete/login completion is projected to one boolean before presentation; pending catalog work is a warning, not a failed account mutation. | | Dashboard overview | Overview, Providers, and Models tabs at the page level (`gui/src/pages/Dashboard.tsx`), the 30-day token and coverage stats in the overview head (`gui/src/pages/dashboard-overview-head.tsx`), and the effort-cap, injection, maintenance, sidecar, and memory panels below it (`gui/src/pages/dashboard-overview-panels.tsx`). |