feat(desktop): add pricing override management UI - #4164
Conversation
|
Converting to draft and parking this, pending a product-direction decision. This PR implements the Pricing editor as the full built-in ∪ overrides table. After #2329, the maintainer re-evaluated the feature on #2218 and paused #2015, closing the dedicated full-catalog PR and suggesting a narrower overrides-only + catalog-picker surface (the built-in set is now ~1,400 models.dev entries), with the override schema to be settled in #2330. So the surface this PR builds is exactly the one under reconsideration — no amount of incremental polish resolves that, and it's a maintainer call, not a code one. The Runtime Host adapter (#2148) and Host projection are already merged; the remaining review items that are code-fixable have been addressed across the rounds (host-scoped writes, CAS against the loaded revision, reconciled-control reconnection that never replays, generation-race invalidation, conflict/uncertain UX, field-level validation, skeleton loading, disabled-reason tooltips, Heading role, reason propagation, tests). What remains open is genuinely gated on the direction: whether to keep a full effective table (needs pagination/virtualization + realistic fixtures) or pivot to overrides-only per #2330. Happy to reshape this into the overrides-only + picker surface once the #2330 contract is agreed, or to close this in favor of that. Parking rather than closing so the adapter-backed wiring and tests are easy to pick up. |
43ede3c to
78e5c2f
Compare
78e5c2f to
3bc7f25
Compare
Extract the Usage settings surface out of the frozen legacy renderer zone into `src/renderer/features/usage/`, matching main's feature-migration pattern (ports / services-context / ui + a settings-provided services boundary). Pure refactor — behavior identical to main (incl. the request-target Tooltip and the read-only pricing tab). Tracks apache#4425; unblocks apache#2015 / apache#4164. - features/usage: UsageSettingsView (surface + tabs + panels; owns the usage-stats state and loads via injected UsageServices), UsageServicesProvider + ports, feature-local table / metric-card / action-guard / optimistic-settings-draft, and a feature-local copy type. - settings/usage-settings-page.tsx reduced to a thin shim: assembles the services (loadUsageStats provided by settings-surface bound to the selected Host; updateUsageSettings delegates to onUpdateSettings, preserving its client-settings reconciliation), mounts the provider keyed by host:epoch, supplies SettingsPage chrome + locale copy + error description. - settings-surface.tsx: drops the usageStats state / reloadUsage / reload-effect; builds loadUsageStats bound to the selected Host and passes it + a host:epoch key + loadErrorTitle as props (no feature/platform import; no dependency-budget or bridge-path growth). - Feature files carry no legacy/preload import; copy stays in locales (passed as a prop). Regenerated the renderer-architecture ledger + astryx inventory. Refs apache#4425, apache#3439
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior matches main (incl. the request-target tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`), so the snapshot survives a Skeleton/Banner state or a section change, and a Host change never remounts the rest of Settings. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425.
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`), so the snapshot survives a Skeleton/Banner state or a section change, and a Host change never remounts the rest of Settings. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425.
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior matches main (incl. the request-target tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
|
wait #4440 |
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
Pure refactor: extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the apache#4088 AppShell-closure ratchet. Behavior is identical to main (incl. the request-target tooltip and the read-only pricing tab). - `settings/usage-settings-page.tsx` becomes a thin shim: assembles UsageServices, mounts the provider keyed by host:epoch, provides SettingsPage chrome + copy + describeError. `settings-surface.tsx` drops the usage-stats state/reload/effect and passes a host-bound loadUsageStats + providerKey as props (its dep count decreases). - Stats are stored tagged with their range; the derived view rejects a stale, late, or failed load whose range no longer matches the persisted one (covered by usage-settings-view.test.ts). Two deviations from the composition-feature pattern, both forced by the ratchet (documented in the feature README): the copy TYPE is feature-local (a shared file would be new closure debt), and there is no platform adapter / composition registration (frozen `settings-surface` cannot import the feature or platform, so the shim assembles the services). Refs apache#4425 Generated-by: Claude Code
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
Extract the Usage settings surface out of the frozen legacy renderer zone into a feature slice `src/renderer/features/usage/` (ports + services-context + ui), so net-new Usage work (the apache#2015/apache#4164 pricing tab) is no longer blocked by the tooltip and the read-only pricing tab). - `features/usage/` owns the surface: narrow `UsageServices` ports (`loadUsageStats`, `updateUsageSettings` — `UsageSettings` in and out, never the whole `AppSettings`); a persistent `UsageFeatureScope` holding the single tagged `{ range, value }` snapshot + reload ticket + Host-generation invalidation + load-failure toast; and a disposable `UsageSettingsView`. - `settings/usage-settings-page.tsx` is a thin legacy shim (`UsageScopeMount` + `UsageSettingsPage`). `settings-surface.tsx` mounts the scope ABOVE its loading/error gate and passes a `host:epoch` `targetKey` as a prop; the scope clears and fences in place on a target change (no React `key`). The Host-change handler also calls the scope's imperative `fenceTarget()` synchronously, so an in-flight old-Host load cannot land in the window before the re-render. - Regenerate renderer-architecture.json and the astryx surface inventory. Extraction-only; the Desktop adapter/composition wiring and the editable pricing tab remain follow-ups. Refs apache#4425. Generated-by: Claude Code
#4440) The renderer-architecture ratchet froze the legacy AppShell closure: no new file may enter it and no legacy file's dependency count may grow. Settings → Usage lives inside that closure, so net-new Usage work (most immediately the editable pricing tab of #2015 / #4164, which has to live in the Usage tabs) had nowhere to go. Usage moves to `src/renderer/features/usage/`, following the #3439 reference boundary. `ports.ts` narrows the port to `UsageSettings` / `UsageStats` instead of the whole `AppSettings`. `services-context.tsx` owns the state: one tagged `{ range, value }` snapshot, the reload ticket, unmount isolation, Host-generation invalidation, the failure toast, and an imperative `fenceTarget()`. `ui/usage-settings-view.tsx` is a disposable view that reads the snapshot and reloads on `[range, targetKey]`. `settings-surface.tsx` drops the `usageStats` state, `reloadUsage` and the prop threading; it mounts the scope above the loading gate so the snapshot survives a section change, and calls `fenceTarget()` synchronously on a Host change alongside the other Host-scoped resources. Behavior is unchanged. The DOM was compared tag by tag against the old page; the only differences are the two wrappers that moved into the shim and a type narrowing. No CSS, IPC or copy changed. One deviation from the composition-feature pattern, forced by the ratchet: there is no `platform/desktop` adapter and no composition wiring, because a frozen closure file cannot import the feature, and usage stats are scoped to the settings-selected Runtime Host, which the app-global composition root does not know about. `settings-surface` assembles the services and mounts the feature through a thin shim. When #4425's composition step lands, only that seam moves; the scope stays feature-owned. Two follow-ups are recorded in the feature README rather than done here: a `SettingsSurface` integration test covering the three lifecycle obligations (the current tests reconstruct the mount shape by hand and never load `settings-surface`), and de-duplicating the feature-local `action-guard` and `optimistic-settings-draft` copies, which are about 212 lines and the main source of the net addition. Refs #4425, #4582, #4088 Generated-by: Claude Code
3bc7f25 to
56263be
Compare
…pache#4164) Integrate the editable Pricing Settings surface (apache#2015) into the migrated `features/usage` feature (apache#4425), replacing the read-only pricing tab with a full add/edit/reset editor over the CAS + reconciliation protocol. Renderer (feature-owned, ratchet-clean): - Add the pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab in place of the old read-only `UsagePricingPanel`. - Give pricing its own services seam — `UsagePricingServices` port + `UsagePricingServicesProvider` + a `platform/desktop` adapter — wired through `composition/desktop-feature-services.tsx`. This keeps the sole `window.maka.settings.pricing` bridge access in the platform zone, so no new bridge path lands in a frozen legacy-AppShell closure file. The controller reloads on the Usage scope's `generationKey` (host:epoch) change. Main / preload: - Add the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over the existing `DesktopRuntimeHostClient` snapshot/mutation methods, and expose a public `reconcilePricingMutation` for the reconciled-control path (reload fresh authority + compare intent; never replay). - Retire the interim, unreachable `usage:pricing:list/put/reset` handlers (and their base-synthesizing helper) so a single CAS surface owns pricing writes. - Split the cross-boundary pricing types into a declaration-only `desktop-pricing.d.ts` (kept out of the renderer closure) plus a main-only `desktop-pricing-decode.ts` runtime guard. Tests: pricing view-model, the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay), and a `PricingEditor` render suite covering the saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft states. Regenerate the Astryx surface inventory. Refs apache#4164 apache#2015 apache#4425 Generated-by: Claude Code
…pache#4164) Integrate the editable Pricing Settings surface (apache#2015) into the migrated `features/usage` feature (apache#4425), replacing the read-only pricing tab with a full add/edit/reset editor over the CAS + reconciliation protocol. Renderer (feature-owned, ratchet-clean): - Add the pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab in place of the old read-only `UsagePricingPanel`. - Give pricing its own services seam — `UsagePricingServices` port + `UsagePricingServicesProvider` + a `platform/desktop` adapter — wired through `composition/desktop-feature-services.tsx`. This keeps the sole `window.maka.settings.pricing` bridge access in the platform zone, so no new bridge path lands in a frozen legacy-AppShell closure file. The controller reloads on the Usage scope's `generationKey` (host:epoch) change. Main / preload: - Add the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over the existing `DesktopRuntimeHostClient` snapshot/mutation methods, and expose a public `reconcilePricingMutation` for the reconciled-control path (reload fresh authority + compare intent; never replay). - Retire the interim, unreachable `usage:pricing:list/put/reset` handlers (and their base-synthesizing helper) so a single CAS surface owns pricing writes. - Split the cross-boundary pricing types into a declaration-only `desktop-pricing.d.ts` (kept out of the renderer closure) plus a main-only `desktop-pricing-decode.ts` runtime guard. Tests: pricing view-model, the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay), and a `PricingEditor` render suite covering the saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft states. Regenerate the Astryx surface inventory. Refs apache#4164 apache#2015 apache#4425 Generated-by: Claude Code
56263be to
66ec432
Compare
…pache#4164) Integrate the editable Pricing Settings surface (apache#2015) into the migrated `features/usage` feature (apache#4425), replacing the read-only pricing tab with a full add/edit/reset editor over the CAS + reconciliation protocol. Renderer (feature-owned, ratchet-clean): - Add the pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab in place of the old read-only `UsagePricingPanel`. - Give pricing its own services seam — `UsagePricingServices` port + `UsagePricingServicesProvider` + a `platform/desktop` adapter — wired through `composition/desktop-feature-services.tsx`. This keeps the sole `window.maka.settings.pricing` bridge access in the platform zone, so no new bridge path lands in a frozen legacy-AppShell closure file. The controller reloads on the Usage scope's `generationKey` (host:epoch) change. - Hide the Usage range/summary toolbar on the Pricing tab (apache#2015 acceptance apache#2): the tab is not time-scoped, so the date range cannot be read as a Pricing scope. Restores gating the read-only tab had before the feature migration. Main / preload: - Add the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over the existing `DesktopRuntimeHostClient` snapshot/mutation methods, and expose a public `reconcilePricingMutation` for the reconciled-control path (reload fresh authority + compare intent; never replay). - Retire the interim, unreachable `usage:pricing:list/put/reset` handlers (and their base-synthesizing helper) so a single CAS surface owns pricing writes. - Split the cross-boundary pricing types into a declaration-only `desktop-pricing.d.ts` (kept out of the renderer closure) plus a main-only `desktop-pricing-decode.ts` runtime guard. Core: - Remove the now-orphaned `UsageStats.pricing` field and the usage-stats IPC projection that fed it: the read-only pricing table it served is gone, and the editor reads the effective snapshot directly through the pricing IPC. Tests: pricing view-model, the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay), and a `PricingEditor` render suite covering the saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft states. Add a Desktop E2E (`e2e/settings-pricing.spec.ts`) that drives 设置 → 使用统计 → 定价配置 against the real embedded Runtime Host — the Host-backed effective table with a 内置-labelled row (apache#2015 apache#1), the absent range/summary toolbar (apache#2), and editor focus restore (apache#11) — plus `Product/Settings/Pricing` Storybook stories (populated / empty / loading / load-failed). Regenerate the Astryx surface inventory. Refs apache#4164 apache#2015 apache#4425 Generated-by: Claude Code
66ec432 to
11f2f21
Compare
…pache#4164) Integrate the editable Pricing Settings surface (apache#2015) into the migrated `features/usage` feature (apache#4425), replacing the read-only pricing tab with a full add/edit/reset editor over the CAS + reconciliation protocol. Renderer (feature-owned, ratchet-clean): - Add the pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab in place of the old read-only `UsagePricingPanel`. - Give pricing its own services seam — `UsagePricingServices` port + `UsagePricingServicesProvider` + a `platform/desktop` adapter — wired through `composition/desktop-feature-services.tsx`. This keeps the sole `window.maka.settings.pricing` bridge access in the platform zone, so no new bridge path lands in a frozen legacy-AppShell closure file. The controller reloads on the Usage scope's `generationKey` (host:epoch) change. - Hide the Usage range/summary toolbar on the Pricing tab (apache#2015 acceptance apache#2): the tab is not time-scoped, so the date range cannot be read as a Pricing scope. Restores gating the read-only tab had before the feature migration. - Search + paginate the effective table. It is the Host's full built-in model set (~1.4k rows) ∪ overrides, so the panel filters by model key and paginates (Astryx Pagination, 50 rows/page) and only one page is ever materialised — an unbounded render blocked/exhausted the renderer (a real Electron E2E failure under CI). Adds `filterPricingRows`/`pricingPageSlice` view-model helpers and optional `rowIndexStart`/`rowCount` on the shared usage table so the paged view keeps correct `aria-rowindex`/`aria-rowcount`. Main / preload: - Add the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over the existing `DesktopRuntimeHostClient` snapshot/mutation methods, and expose a public `reconcilePricingMutation` for the reconciled-control path (reload fresh authority + compare intent; never replay). - Retire the interim, unreachable `usage:pricing:list/put/reset` handlers (and their base-synthesizing helper) so a single CAS surface owns pricing writes. - Split the cross-boundary pricing types into a declaration-only `desktop-pricing.d.ts` (kept out of the renderer closure) plus a main-only `desktop-pricing-decode.ts` runtime guard. Core: - Remove the now-orphaned `UsageStats.pricing` field and the usage-stats IPC projection that fed it: the read-only pricing table it served is gone, and the editor reads the effective snapshot directly through the pricing IPC. Tests: pricing view-model (incl. filter + pagination slicing), the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay), and a `PricingEditor` render suite covering the saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft states plus the one-page render bound and search-narrowing. A Desktop E2E (`e2e/settings-pricing.spec.ts`) drives 设置 → 使用统计 → 定价配置 against the real embedded Runtime Host: searching for a built-in row (来源 = 内置, apache#2015 apache#1) keeps the render light, the range/summary toolbar is absent (apache#2), and editor focus is restored (apache#11). Adds `Product/Settings/Pricing` Storybook stories (populated / paginated / empty / loading / load-failed) and regenerates the Astryx surface inventory. Refs apache#4164 apache#2015 apache#4425 Generated-by: Claude Code
11f2f21 to
08e7940
Compare
apache#4164) Add the editable Pricing Settings surface (apache#2015) to the migrated `features/usage` feature (apache#4425) as the Usage "pricing" tab, over the Host's CAS + reconciliation protocol. Per the maintainer direction on apache#2218 the surface is **overrides-only**: the table lists only the user's custom rows, and the built-in models.dev catalog (~1.4k rows) is reached only through the Add flow — never rendered as a table. (apache#3129's user-overridable model facts deliberately exclude pricing and ship no UI, so pricing remains its own dedicated surface.) Renderer (feature-owned, ratchet-clean): - Pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab. Its services come from a dedicated `UsagePricingServices` port + provider + `platform/desktop` adapter wired through `composition/desktop-feature-services.tsx`, so the sole `window.maka.settings.pricing` bridge access stays in the platform zone. - Overrides-only table: `overrideRows` = the Host's `custom` entries (the Host collapses an overridden built-in into one custom row). The Usage range/summary toolbar is hidden on this tab (apache#2015 acceptance apache#2 — not time-scoped). - Add flow uses an Astryx `Typeahead` catalog picker over the Host's `builtin` entries (renders only the top matches, never the full list; a pick pre-fills the built-in price), with a manual-entry fallback for a model not in the catalog (local/new keys). Edit locks the key. Duplicate detection stays over the full built-in ∪ overrides union. Correctness (fixes found in review of the earlier full-table revision): - A committed mutation fences an in-flight reload (shared authority sequence), so a slow refresh can't overwrite the saved authority or clear a write-block. - A Host generation change resets all transient state (editor/draft/busy latches + action guard), so a dialog can't stick saving and an old-Host draft can't be saved onto the new authority. - An Add conflict whose key now exists elsewhere converts the Add into an Edit locked on that key, so the required second save upserts instead of being silently blocked by the duplicate check. - A saved-but-refresh-failed outcome clears the now-stale list (no speculative final list, per apache#2015) while retaining the draft until a successful refresh. Main / preload: the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over `DesktopRuntimeHostClient`, a public `reconcilePricingMutation` for the reconciled-control path (reload + compare intent; never replay), and the declaration-only `desktop-pricing.d.ts` + main-only `desktop-pricing-decode.ts`. Core: remove the orphaned `UsageStats.pricing` field + its usage-stats projection. Tests: pricing view-model + a `PricingEditor` render suite (overrides-only table, catalog/manual Add, saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft, and one regression per correctness fix above), plus the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay). A Desktop E2E drives 设置 → 使用统计 → 定价配置: overrides-only (no 内置 rows), the absent range toolbar (apache#2), the catalog/manual Add UI, and editor focus restore (apache#11). Storybook stories (populated / empty / loading / load-failed) and the Astryx surface inventory regenerated. Refs apache#4164 apache#2015 apache#4425 apache#2218 Generated-by: Claude Code
08e7940 to
e59acf4
Compare
apache#4164) Add the editable Pricing Settings surface (apache#2015) to the migrated `features/usage` feature (apache#4425) as the Usage "pricing" tab, over the Host's CAS + reconciliation protocol. Per the maintainer direction on apache#2218 the surface is **overrides-only**: the table lists only the user's custom rows, and the built-in models.dev catalog (~1.4k rows) is reached only through the Add flow — never rendered as a table. (apache#3129's user-overridable model facts deliberately exclude pricing and ship no UI, so pricing remains its own dedicated surface.) Renderer (feature-owned, ratchet-clean): - Pricing controller, view-model, copy, and editor UI under `features/usage/`, rendered as the Usage "pricing" tab. Its services come from a dedicated `UsagePricingServices` port + provider + `platform/desktop` adapter wired through `composition/desktop-feature-services.tsx`, so the sole `window.maka.settings.pricing` bridge access stays in the platform zone. - Overrides-only table: `overrideRows` = the Host's `custom` entries (the Host collapses an overridden built-in into one custom row). The Usage range/summary toolbar is hidden on this tab (apache#2015 acceptance apache#2 — not time-scoped). - Add flow uses an Astryx `Typeahead` catalog picker over the Host's `builtin` entries (renders only the top matches, never the full list; a pick pre-fills the built-in price), with a manual-entry fallback for a model not in the catalog (local/new keys). Edit locks the key. Duplicate detection stays over the full built-in ∪ overrides union. Correctness (fixes found in review of the earlier full-table revision): - A committed mutation fences an in-flight reload (shared authority sequence), so a slow refresh can't overwrite the saved authority or clear a write-block. - A Host generation change resets all transient state (editor/draft/busy latches + action guard), so a dialog can't stick saving and an old-Host draft can't be saved onto the new authority. - An Add conflict whose key now exists elsewhere converts the Add into an Edit locked on that key, so the required second save upserts instead of being silently blocked by the duplicate check. - A saved-but-refresh-failed outcome clears the now-stale list (no speculative final list, per apache#2015) while retaining the draft until a successful refresh. Main / preload: the CAS pricing IPC (`usage:pricing:load` / `usage:pricing:mutate`) over `DesktopRuntimeHostClient`, a public `reconcilePricingMutation` for the reconciled-control path (reload + compare intent; never replay), and the declaration-only `desktop-pricing.d.ts` + main-only `desktop-pricing-decode.ts`. Core: remove the orphaned `UsageStats.pricing` field + its usage-stats projection. Tests: pricing view-model + a `PricingEditor` render suite (overrides-only table, catalog/manual Add, saved / refresh-failed / conflict / reconcile-unavailable / invalid-draft, and one regression per correctness fix above), plus the load/mutate IPC (base pass-through, malformed base, reconcile-no-replay). A Desktop E2E drives 设置 → 使用统计 → 定价配置: overrides-only (no 内置 rows), the absent range toolbar (apache#2), the catalog/manual Add UI, and editor focus restore (apache#11). Storybook stories (populated / empty / loading / load-failed) and the Astryx surface inventory regenerated. Refs apache#4164 apache#2015 apache#4425 apache#2218 Generated-by: Claude Code
e59acf4 to
6d23dbc
Compare
|
Reshaped this per your note on #2218 — it's now overrides-only with a catalog-backed picker, not the full effective table:
Rebased onto latest Happy to adjust further — in particular, if you'd prefer pricing overrides to live in #3129's unified model-facts layer (by extending its schema) rather than a dedicated surface, I'm glad to pivot to that instead. |
apache#4440) The renderer-architecture ratchet froze the legacy AppShell closure: no new file may enter it and no legacy file's dependency count may grow. Settings → Usage lives inside that closure, so net-new Usage work (most immediately the editable pricing tab of apache#2015 / apache#4164, which has to live in the Usage tabs) had nowhere to go. Usage moves to `src/renderer/features/usage/`, following the apache#3439 reference boundary. `ports.ts` narrows the port to `UsageSettings` / `UsageStats` instead of the whole `AppSettings`. `services-context.tsx` owns the state: one tagged `{ range, value }` snapshot, the reload ticket, unmount isolation, Host-generation invalidation, the failure toast, and an imperative `fenceTarget()`. `ui/usage-settings-view.tsx` is a disposable view that reads the snapshot and reloads on `[range, targetKey]`. `settings-surface.tsx` drops the `usageStats` state, `reloadUsage` and the prop threading; it mounts the scope above the loading gate so the snapshot survives a section change, and calls `fenceTarget()` synchronously on a Host change alongside the other Host-scoped resources. Behavior is unchanged. The DOM was compared tag by tag against the old page; the only differences are the two wrappers that moved into the shim and a type narrowing. No CSS, IPC or copy changed. One deviation from the composition-feature pattern, forced by the ratchet: there is no `platform/desktop` adapter and no composition wiring, because a frozen closure file cannot import the feature, and usage stats are scoped to the settings-selected Runtime Host, which the app-global composition root does not know about. `settings-surface` assembles the services and mounts the feature through a thin shim. When apache#4425's composition step lands, only that seam moves; the scope stays feature-owned. Two follow-ups are recorded in the feature README rather than done here: a `SettingsSurface` integration test covering the three lifecycle obligations (the current tests reconstruct the mount shape by hand and never load `settings-surface`), and de-duplicating the feature-local `action-guard` and `optimistic-settings-draft` copies, which are about 212 lines and the main source of the net addition. Refs apache#4425, apache#4582, apache#4088 Generated-by: Claude Code Generated-by: GLM-5.3-Flash (ZCode)
Summary
Adds an editable, Runtime Host–backed pricing-override editor as the
Settings → Usage → 定价配置tab, integrated into thefeatures/usageslice (built on #4440's Usage→feature migration). Implements the Desktop UI slice of #2015.Following the maintainer's direction on #2218, the surface is overrides-only: the table lists only the user's custom price overrides, and the bundled models.dev catalog (~1.4k models) is reached only through the Add flow's searchable catalog picker — it is never rendered as a table. (#3129's user-overridable model facts deliberately exclude pricing and ship no UI, so pricing stays its own dedicated surface.) Edits go through the Host's full CAS + reconciliation path.
Builds on #4440 · Refs #2015 #2218 #4425 · supersedes the earlier settings-zone and full-catalog versions of this PR.
Screenshots
Overrides-only table — only the user's custom rows; built-ins are not listed.
Add dialog — catalog picker — a
Typeaheadover the built-in catalog (renders only the top matches, never the full list) that pre-fills the built-in price; a manual-entry fallback covers models not in the catalog.Design
Net-new UI reachable from the frozen legacy AppShell closure (#4088 ratchet), so it is built as feature code with a dedicated composition service seam (the canonical pattern, like
session-collaboration):features/usage/:ui/pricing-editor.tsx,controller/pricing-controller.ts,pricing-view-model.ts,pricing-copy.ts(feature-local),pricing-ports.ts(UsagePricingServices),pricing-services-context.tsx.platform/desktop/create-usage-pricing-services.ts— the ONLY placewindow.maka.settings.pricingis called (platform zone), wired viacomposition/desktop-feature-services.tsx, so the frozensettings-surface/shim gains no new bridge path.customentry, so the table = the snapshot'scustomrows and the picker = itsbuiltinrows (built-ins not yet overridden). Duplicate detection still runs over the full built-in ∪ overrides union.Typeahead+createStaticSourceover the built-in rows; picking one pre-fills the built-in price. A "模型不在列表中?手动输入" toggle falls back to free-text keys (local/new models). Edit locks the key.settings-surface→ shim → view → controller →services.loadPricing(host)/mutatePricing(host, …)). No host → empty state (no active-host fallback).DesktopPricing*types from the bridge signature, so it imports noshared/runtime module;shared/desktop-pricingis split into a closure-excluded.d.ts+ a main-only decode module.Correctness (CAS / reconcile state machine)
Process boundary
usage:pricing:load/usage:pricing:mutate(per-Host scoped); the renderer round-trips the loaded snapshot as the CASbase; unknown outcomes reconcile against a fresh Host without replay.usage:pricing:{list,put,reset}handlers are removed (Acceptance Add RuntimeEvent read model projection #12: one CAS write path).pricingtab AND the now-unusedUsageStats.pricingfield (core type + usage-IPC projection + fixtures/tests). The range/summary toolbar is hidden on the pricing tab (not time-scoped, feat(desktop): add editable Runtime Host-backed pricing settings #2015 Harden runtime, storage, gateway, credentials, and IPC inputs #2).Verification
Local, against latest
main: fulltsc --noEmit×4 desktop projects +@maka/*builds 0 errors; the pricing/usage focused suites pass (view-model,PricingEditorrender across the overrides-only table / catalog + manual Add / saved / refresh-failed / conflict / reconcile-unavailable / validation states plus one regression per correctness fix above, load/mutate IPC incl. base pass-through / malformed base / reconcile-no-replay, host-threading + no-host, and usage IPC);check-renderer-architecture --base origin/main,astryx:surface-inventory,biome lint,check:asf-headersclean. A Host-backed Desktop E2E (e2e/settings-pricing.spec.ts) drives 设置 → 使用统计 → 定价配置 — overrides-only (no 内置 rows), absent range toolbar (#2), the catalog/manual Add UI, and editor focus restore (#11). Storybook smoke covers the pricing states (populated / empty / loading / load-failed). CItestis green.AI use
Tool(s) and scope: Claude Code (Claude Opus 4.8) — the overrides-only pivot, the catalog picker, the CAS/reconcile correctness fixes, and verification. Reviewed and submitted by the human contributor of record; commit carries
Generated-by: Claude Code.Checklist
Does this PR entail a change in behavior?