Uh oh!
There was an error while loading. Please reload this page.
chore(settings): R7 connection-detail decomposition + byTool aggregation fix — campaign close - #1254
Conversation
Settings → 使用统计 → 工具统计 repeated tool-name rows (multiple Bash rows) because byTool was built via sessions.flatMap(toolStatsFromMessages), producing one aggregate row per tool PER session. byProvider/byModel were already global (aggregateBy over the flattened modelLogs), so only byTool was affected. Replace toolStatsFromMessages with aggregateToolStats(sessions, since): tool_call.id ↔ tool_result.toolUseId matching stays session-scoped (ids are only unique within a session) while calls/success/errors/durations merge into a single row keyed by tool name. Rows sort by call count desc (tool name tie-break) to match the byProvider/byModel convention and give deterministic output. Row shape and the sole consumer (UsageToolsPanel) are unchanged. Regression test: two sessions each calling Bash (+ one Read) yield one merged Bash row (calls 2, success 1, errors 1, avg duration 30).
…del manager (arch R7) provider-connection-detail.tsx was the renderer's densest state cluster (983 lines, 12 useState in ConnectionDetailInner + a shared keyed action guard + lifecycle gate). Split following the settings house pattern: - use-connection-detail.ts (controller hook, ~520 lines): owns the whole mutually-exclusive action state machine (save / test / fetch-models / save-enabled-models / set-default / delete, all gated through one useKeyedActionGuard), the credential-presence probe, the prop-sync effects, every derived flag, and the pure snapshot/equality helpers + oauthLoginServiceFor. Returns a controller object; ZERO behavior change, every identifier and statement moved verbatim. - provider-enabled-model-manager.tsx (~187 lines): the roving-tabindex model-list editor (owns query + activeRowId) — an independent cluster. - provider-connection-detail.tsx (983 -> 373 lines): thin view that destructures the hook, plus ConnectionEndpointField / the OAuth & GitHub Copilot re-login notices (kept here as presentation). The connection/model/test/delete sub-clusters stay in one controller on purpose: they share a single keyed-action-guard state machine (mutual exclusion) and one lifecycle/current-ness gate, and cross-call (save auto-fetches models) — splitting them further would thread the guard and risk behavior drift.
The connection-detail state machine + helpers moved into
use-connection-detail.ts and the enabled-model editor into
provider-enabled-model-manager.tsx, so the model-oauth-section contract's
ConnectionDetail slices no longer live wholly in the view file.
- provider-contract-source-helpers: join use-connection-detail.ts +
provider-enabled-model-manager.tsx into the combined source, adjacent to
the detail view, so 'function ConnectionDetail ... function
modelIdListsEqual' slices span view + controller contiguously. Added
detailController / enabledModelManager slices to the sources interface.
- model-oauth-section-contract: widen the ConnectionDetail controller
slices' terminator from 'function GitHubCopilotReloginNotice' to
'function modelIdListsEqual(' so handler bodies / derived flags / effects
/ snapshot helpers (now in the hook) are covered; the ConnectionDetailInner
view-order slice is unchanged. Re-point the last-test-message helper
assertion to the combined source (the derivation moved to the controller).
Every behavior invariant is preserved, none deleted.
- web-search-boundary: add the two new renderer files to the scanned set.
Desktop 2744 + ui 196 suites green; knip x2 exit 0.R7 detail-controller decomposition (983 -> 373) recorded with blade plan, extraction table, contract re-pins, and gate totals. Closing summary: main.ts 1903 -> 1175 (R4-R6), provider-connection-detail 983 -> 373 (R7), visual-smoke split (R3), files-split inventory, and the byTool per-session aggregation fix shipped alongside.
Uh oh!
There was an error while loading. Please reload this page.
main's typecheck CI job (Biome check --write=false) fails on this file since apache#1254; rewrap the two tool_call literals the formatter expects. Formatting only, no behavior change.
main's typecheck CI job (Biome check --write=false) fails on this file since apache#1254; rewrap the two tool_call literals the formatter expects. Formatting only, no behavior change.
The setup wizard filtered out every provider without a built-in base URL — an explicit phase-1 scope cut (apache#1254) whose phase-2 base-URL prompt never landed — and the onboarding protocol had no field to carry an endpoint anyway, so the three custom relays were creatable from Desktop but unreachable from the TUI. List the category:'custom' relays (cloudflare-workers-ai stays out: its endpoint is an account-id template, not a user-supplied URL), add a base-URL step to the wizard between provider pick and API key with host-mirroring local validation, and thread an always-present 'baseUrl: string | null' through connection.onboarding.verify/save — exact-record wire style like apiKey, normalized by the shared catalog rules, rejected as base_url_not_configured when a relay has no endpoint from input, existing connection, or registry. Blank input on an existing relay reuses its persisted endpoint, mirroring the blank-key contract. Discovery runs against the supplied endpoint and commit persists it: the intent journal gains the field (legacy journals still replay), the upsert resolves input ?? previous ?? registry default, a URL-only change still commits, and a swapped endpoint drops relayModelProfiles and lastTest — the endpoint-keyed contract the update path already enforces. The onboarding wire shapes are closed schemas, so the compatibility epoch moves to 37. Fixesapache#3405 Generated-by: Claude Code
The setup wizard filtered out every provider without a built-in base URL — an explicit phase-1 scope cut (apache#1254) whose phase-2 base-URL prompt never landed — and the onboarding protocol had no field to carry an endpoint anyway, so the three custom relays were creatable from Desktop but unreachable from the TUI. List the category:'custom' relays (cloudflare-workers-ai stays out: its endpoint is an account-id template, not a user-supplied URL), add a base-URL step to the wizard between provider pick and API key with host-mirroring local validation, and thread an always-present 'baseUrl: string | null' through connection.onboarding.verify/save — exact-record wire style like apiKey, normalized by the shared catalog rules, rejected as base_url_not_configured when a relay has no endpoint from input, existing connection, or registry. Blank input on an existing relay reuses its persisted endpoint, mirroring the blank-key contract. Discovery runs against the supplied endpoint and commit persists it: the intent journal gains the field (legacy journals still replay), the upsert resolves input ?? previous ?? registry default, a URL-only change still commits, and a swapped endpoint drops relayModelProfiles and lastTest — the endpoint-keyed contract the update path already enforces. The onboarding wire shapes are closed schemas, so the compatibility epoch moves to 37. Fixesapache#3405 Generated-by: Claude Code
The setup wizard filtered out every provider without a built-in base URL — an explicit phase-1 scope cut (apache#1254) whose phase-2 base-URL prompt never landed — and the onboarding protocol had no field to carry an endpoint anyway, so the three custom relays were creatable from Desktop but unreachable from the TUI. List the category:'custom' relays (cloudflare-workers-ai stays out: its endpoint is an account-id template, not a user-supplied URL), add a base-URL step to the wizard between provider pick and API key with host-mirroring local validation, and thread an always-present 'baseUrl: string | null' through connection.onboarding.verify/save — exact-record wire style like apiKey, normalized by the shared catalog rules, rejected as base_url_not_configured when a relay has no endpoint from input, existing connection, or registry. Blank input on an existing relay reuses its persisted endpoint, mirroring the blank-key contract. Discovery runs against the supplied endpoint and commit persists it: the intent journal gains the field (legacy journals still replay), the upsert resolves input ?? previous ?? registry default, a URL-only change still commits, and a swapped endpoint drops relayModelProfiles and lastTest — the endpoint-keyed contract the update path already enforces. The onboarding wire shapes are closed schemas, so the compatibility epoch moves to 37. Fixesapache#3405 Generated-by: Claude Code
The setup wizard filtered out every provider without a built-in base URL — an explicit phase-1 scope cut (apache#1254) whose phase-2 base-URL prompt never landed — and the onboarding protocol had no field to carry an endpoint anyway, so the three custom relays were creatable from Desktop but unreachable from the TUI. List the category:'custom' relays (cloudflare-workers-ai stays out: its endpoint is an account-id template, not a user-supplied URL), add a base-URL step to the wizard between provider pick and API key with host-mirroring local validation, and thread an always-present 'baseUrl: string | null' through connection.onboarding.verify/save — exact-record wire style like apiKey, normalized by the shared catalog rules, rejected as base_url_not_configured when a relay has no endpoint from input, existing connection, or registry. Blank input on an existing relay reuses its persisted endpoint, mirroring the blank-key contract. Discovery runs against the supplied endpoint and commit persists it: the intent journal gains the field (legacy journals still replay), the upsert resolves input ?? previous ?? registry default, a URL-only change still commits, and a swapped endpoint drops relayModelProfiles and lastTest — the endpoint-keyed contract the update path already enforces. The onboarding wire shapes are closed schemas, so the compatibility epoch moves to 37. Fixesapache#3405 Generated-by: Claude Code
* fix(cli): let the TUI wizard create custom relay connections The setup wizard filtered out every provider without a built-in base URL — an explicit phase-1 scope cut (#1254) whose phase-2 base-URL prompt never landed — and the onboarding protocol had no field to carry an endpoint anyway, so the three custom relays were creatable from Desktop but unreachable from the TUI. List the category:'custom' relays (cloudflare-workers-ai stays out: its endpoint is an account-id template, not a user-supplied URL), add a base-URL step to the wizard between provider pick and API key with host-mirroring local validation, and thread an always-present 'baseUrl: string | null' through connection.onboarding.verify/save — exact-record wire style like apiKey, normalized by the shared catalog rules, rejected as base_url_not_configured when a relay has no endpoint from input, existing connection, or registry. Blank input on an existing relay reuses its persisted endpoint, mirroring the blank-key contract. Discovery runs against the supplied endpoint and commit persists it: the intent journal gains the field (legacy journals still replay), the upsert resolves input ?? previous ?? registry default, a URL-only change still commits, and a swapped endpoint drops relayModelProfiles and lastTest — the endpoint-keyed contract the update path already enforces. The onboarding wire shapes are closed schemas, so the compatibility epoch moves to 37. Fixes#3405 Generated-by: Claude Code * fix(cli): carry connection identity through onboarding so custom-slug relays edit in place The wizard recognized an existing connection only at the derived canonical slug, so a relay created in Desktop under a custom slug read as unconfigured in /setup and saving created a second canonical-slug connection, leaving the old credential and default target behind. Onboarding inputs now carry 'connectionId: string | null': the catalog projection resolves the existing connection (canonical slug first, else the provider type's sole connection), the wizard threads its identity through verify/save, the coordinator targets it directly (rejecting a stale id as connection_not_found instead of duplicating), and the storage upsert finds the row by identity first, preserving its slug. A stale-snapshot rejection renders without the retype-the-key framing. Epoch-37 builds from this PR's own review cycle require baseUrl but not connectionId, so the identity shape gets epoch 38 rather than reusing 37 for a second mutually-undecodable frame. Generated-by: Claude Code * fix(runtime-host): bind onboarding discovery to the basis its commit revalidates Model discovery ran outside the mutation lane and the final commit re-read latest state, so a concurrent supported policy update could persist relay B/key B with the inventory discovered from relay A/key A. Adopt the model-fetch ticket shape: beginConnectionOnboarding locates the target under the write lane and issues a one-shot WeakMap ticket whose basis pins the connection revision (covering the endpoint and every other catalog-visible property), the api-key credential status plus stored secret from one vault read, and the effective proxy with its credential — returning the pinned proxy so discovery runs through the egress the basis certifies rather than re-resolving it. complete revalidates that basis atomically before the durable intent is written: drift returns 'superseded' (a new save rejection, riding this PR's unpublished epoch), a vanished target keeps reporting connection_not_found, and no journal is written on either. Verify abandons its ticket (WeakMap-held); save begins its own. Regression test drives the reviewed race end to end: discovery paused on relay A/key A, endpoint moved and key rotated concurrently, the save supersedes with relay B/key B intact and relay A's inventory never persisted, and a retry commits cleanly. Generated-by: Claude Code * fix(runtime-host): send onboarding discovery through the connection's request customization The onboarding probe went out on the bare transport fetch, while the models path wraps it with the connection's custom request headers and body overlay — so a connection that authenticates through a custom header verified and fetched models fine but failed re-onboarding. beginConnectionOnboarding now pins the request-headers secret for the probe and adds its credential status to the ticket's basis, so a header rotation between discovery and commit supersedes the save the same way an endpoint or key change does. Generated-by: Claude Code
Final round of notes/frontend-architecture-map-2026-07-19.md.
byTool aggregation bug (found by #1252's restyle)
usageStats built byTool per-session (flatMap over sessions) → duplicate tool rows in 工具统计. Fixed in settings-store: session-scoped call↔result id matching (ids only unique per session) with global per-tool-name merging, sorted like the sibling aggregates; byProvider/byModel verified already-correct. Regression test (two sessions sharing a tool → one merged row); IPC-verified on the fixture: 6 unique rows, no duplicates (was ~10).
R7 — provider-connection-detail.tsx decomposition
983 → 373 (−610). The 12-useState controller + single action-guard interlock extracted WHOLE into use-connection-detail.ts (522 L) — the 6 mutually-exclusive actions share one guard, so per-cluster splitting risked drift; plus provider-enabled-model-manager.tsx (187 L) for the roving-tabindex model editor. Every statement moved verbatim, JSX identities literal. model-oauth-section contract re-pinned via a provider combined-source helper (~13 slice terminators widened, view-order slice untouched, nothing deleted). oauth-relogin fixture sheet renders identically (CDP).
Campaign closing numbers (map updated)
main.ts 1903→1175 · app-shell.tsx 1686→1654 · provider-connection-detail 983→373 · visual-smoke god-file → barrel+6 modules · knip 13 hints→0 · 13 auditor fixtures. All 8 rounds shipped across #1241/#1243/#1246/#1248/#1251/#1252/this.
Gates (merged tree): desktop 2744 · ui 196 · storage 396 · typecheck · check-dead-css · knip ×2 = 0 · a11y/copy/console clean · auditor all 13 fixtures. Implemented by an opus worktree agent.