Skip to content

feat(desktop): add pricing override management UI - #4164

Open
liuxiaocs7 wants to merge 6 commits into
apache:mainfrom
liuxiaocs7:feat/usage-pricing-override-ui
Open

feat(desktop): add pricing override management UI#4164
liuxiaocs7 wants to merge 6 commits into
apache:mainfrom
liuxiaocs7:feat/usage-pricing-override-ui

Conversation

@liuxiaocs7

@liuxiaocs7 liuxiaocs7 commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

Adds an editable pricing override manager under Settings → Usage → Pricing for the Runtime Host selected in Settings.

Following the maintainer direction on #2218, this is an overrides-only surface:

  • The table renders only user-defined overrides, never the full ~1,400-model built-in catalog.
  • The Add dialog provides a searchable catalog picker that pre-fills built-in prices, plus manual entry for local or newly introduced model keys.
  • Loads and mutations remain scoped to the selected Host and use the existing CAS and reconciliation protocol.
  • Unknown write outcomes are reconciled by reloading and comparing authoritative state; mutations are never replayed automatically.
  • Conflict and recovery notices remain inside the active dialog, and focus returns to a stable action when deleting a row removes its trigger.

Refs #2015 #2218 #4425 #4440

Verification

  • GitHub Actions CI passed on head 29f6b6ba5: run 34059080865.
  • Desktop Pricing/Usage focused suites: 65/65 passed.
  • Desktop TypeScript projects: passed.
  • Renderer architecture checks: 101/101 passed against origin/main.
  • Electron Pricing E2E: passed, including catalog/manual Add flows and delete-focus fallback.
  • Electron E2E budget: 33 tests in 18 files.
  • Locale hygiene, Astryx inventory, Biome, ASF headers, protocol epoch guard, Storybook build, and Storybook smoke: passed.

Overrides-only table

Only custom overrides are listed; built-in rows remain available through the Add flow.

Pricing overrides-only table

Catalog-backed Add dialog

Selecting a built-in model pre-fills its prices while keeping every value editable; manual entry remains available.

Pricing catalog picker with prefilled prices

Review focus

  • Duplicate validation intentionally checks existing overrides only. Treating the built-in catalog as duplicates would prevent creating an override for any catalog model.
  • A reset/delete conflict requires an explicit second confirmation against the latest CAS snapshot.
  • saved_refresh_failed and reconciliation_unavailable retain the draft or reset intent and expose refresh inside the modal focus trap.
  • A selected-Host generation change synchronously fences stale asynchronous results before React commits the new target.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope:

  • Claude Code: initial overrides-only editor, catalog picker, CAS/reconciliation integration, tests, and verification.
  • Codex: implementation ablation, state/concurrency fixes, modal recovery and focus accessibility fixes, regression tests, screenshots, and PR documentation.

The affected commits include Generated-by: trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Aug 29, 2026
@liuxiaocs7
liuxiaocs7 marked this pull request as draft August 29, 2026 21:25
@liuxiaocs7

Copy link
Copy Markdown
Member Author

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.

@github-actions github-actions Bot added effort/XL Under 2500 readable lines and removed effort/M Under 500 readable lines labels Aug 30, 2026
@liuxiaocs7
liuxiaocs7 marked this pull request as ready for review August 31, 2026 19:05
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Aug 31, 2026
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Aug 31, 2026
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 43ede3c to 78e5c2f Compare August 31, 2026 19:27
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Aug 31, 2026
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 78e5c2f to 3bc7f25 Compare August 31, 2026 19:33
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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.
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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.
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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
@liuxiaocs7

Copy link
Copy Markdown
Member Author

wait #4440

liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 11f2f21 to 08e7940 Compare September 3, 2026 09:30
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 3, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 08e7940 to e59acf4 Compare September 3, 2026 13:50
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 3, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from e59acf4 to 6d23dbc Compare September 3, 2026 14:03
@liuxiaocs7

Copy link
Copy Markdown
Member Author

Reshaped this per your note on #2218 — it's now overrides-only with a catalog-backed picker, not the full effective table:

  • The table lists only the user's custom overrides. The ~1.4k built-in models.dev catalog is reached only through the Add dialog's searchable Typeahead picker (it renders just the top matches, never the full list, and pre-fills the built-in price), with a manual-entry fallback for keys not in the catalog. No 1.4k-row surface is ever rendered.
  • I checked feat(core,storage): add user-overridable model facts #3129 now that it's merged: its user-overridable model facts schema deliberately excludes pricing and it ships no UI, and pricing is a separate subsystem (PricingConfig / PricingStore / usage-pricing-coordinator). So pricing can't simply fold into that layer without extending feat(core,storage): add user-overridable model facts #3129's schema — hence keeping a dedicated, but now minimal, pricing surface.
  • The CAS + reconciliation write path is retained. I also fixed a few state-machine issues found in review of the earlier revision (reload-vs-save ordering, Host-generation reset, the second save after an Add conflict, and the saved-but-refresh-failed stale list).

Rebased onto latest main; CI is green, and the PR description + screenshots are updated to the new shape.

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.

@github-actions github-actions Bot added effort/XXL Over 2500 readable lines and removed effort/XL Under 2500 readable lines labels Sep 3, 2026
ggbdpq pushed a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
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)
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 6d23dbc to a82d286 Compare September 6, 2026 10:28
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from a82d286 to 52dc16a Compare September 6, 2026 10:39
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 52dc16a to 4eecde2 Compare September 6, 2026 14:00
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch 2 times, most recently from 0854f80 to ac574c8 Compare September 6, 2026 17:49
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 6, 2026
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
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch 2 times, most recently from 0be1e05 to ed120fe Compare September 6, 2026 18:21

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-freeze review at head a41c1e02 (44 files, +3974/−312) against base 2310035a. This confirms my earlier review's code conclusion and closes the gate blocker from the previous head.

Prior blocker (architecture debt, 9 items) — resolved. The pricing feature now lives inside features/usage/ (controller, view model, editor, ports, services context); the only window.maka.settings.pricing access sits in platform/desktop/create-usage-pricing-services.ts, injected through composition/desktop-feature-services.tsx, so no new bridge path lands in the frozen legacy-AppShell closure files. The renderer-architecture and Astryx surface inventory checks pass in CI on this exact head.

Product direction — settled. Following the maintainer direction on the closed #2218, the surface is overrides-only: the table lists only custom rows, the built-in catalog is reachable through the Add dialog picker (never rendered as a full table), and duplicate validation is over existing overrides only (picking an unmodified built-in correctly creates a new override). The legacy usage:pricing:{list,put,reset} handlers and the read-only UsageStats.pricing projection are removed.

Correctness (key paths): mutations round-trip the loaded snapshot as the CAS base (the IPC boundary decodes it strictly: hostEpoch, connectionId, non-negative integer revision, canonical key order; a foreign base is rejected by the adapter's stale guard, a stale revision degrades to revision_conflict). Unknown dispatch outcomes go through the reconciled-control harness (the goal:arm pattern): wait for a replacement Host, reload fresh authority, compare the intended end state, and never replay the mutation; delete reconciliation preserves the distinct restore-builtin / become-unpriced / no-existing-override semantics. In-flight work is fenced by reload tickets plus a Host-generation epoch; refresh_failed drops the stale list and blocks writes; review_required enters an explicit-conflict state requiring a second save against the latest snapshot (an Add whose key appeared elsewhere converts to an Edit locked on that key). The view model is pure and validates rates (finite, ≥ 0) and keys (normalize / duplicate / locked) with an overrides-only duplicate rule.

Integration: the Pricing tab uses the controller-driven editor; the time-scoped range and summary toolbar are hidden on that tab (pricing is not time-scoped, per #2015 acceptance #2); the e2e budget adds settings-pricing.spec.ts with a real embedded Host round-trip and real Electron focus justification.

P3 (recorded): the Pricing tab changes from a read-only time-scoped panel to a non-time-scoped override editor — an intended behavior change under #2015/#2218, not a regression.

Not verified: I did not run the suites locally (CI on the exact head covers the affected desktop suite); the 526-line editor was checked at the handler level (save / reset / conflict all route through the controller), not pixel-level. A manual pass over Add → pick catalog → save → switch Host in a real Electron build is worth doing.

No P0–P2 findings. Refactor + feature; I am not approving — the merge decision belongs to humans. Epoch is unchanged (121 on base, head, and main), so there is no epoch-ordering constraint at merge.


Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.

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
Consolidate mutation reconciliation in the Runtime Host protocol, fence stale Host results synchronously, and retain uncertain mutation intent for explicit refresh reconciliation.

Remove duplicate copy, bridge-derived types, stale styling, and unused controller surface while preserving the concurrency and CAS invariants required for correctness.

Generated-by: Codex
Allow the manual-entry Storybook interaction to match the required-field suffix included in the input's accessible name.

Generated-by: Codex
Surface conflict and uncertain-write recovery within the active pricing dialog, retain reconciliation intent without replay, and restore focus to a stable action when deleting an override removes its trigger.

Add regression coverage for fresh CAS authority, draft preservation, modal recovery, and reset focus fallback; also align localized Host terminology.

Generated-by: Codex
Re-pin the compatible pricing helper declaration to protocol epoch 136 and regenerate the Astryx surface inventory after rebasing onto current main.

Generated-by: Codex
@liuxiaocs7
liuxiaocs7 force-pushed the feat/usage-pricing-override-ui branch from 29f6b6b to f4538fb Compare September 10, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants