Skip to content

Commit e59acf4

Browse files
committed
feat(desktop): add overrides-only pricing editor with a catalog picker (#4164)
Add the editable Pricing Settings surface (#2015) to the migrated `features/usage` feature (#4425) as the Usage "pricing" tab, over the Host's CAS + reconciliation protocol. Per the maintainer direction on #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. (#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 (#2015 acceptance #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 #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 (#2), the catalog/manual Add UI, and editor focus restore (#11). Storybook stories (populated / empty / loading / load-failed) and the Astryx surface inventory regenerated. Refs #4164 #2015 #4425 #2218 Generated-by: Claude Code
1 parent 6ab7251 commit e59acf4

35 files changed

Lines changed: 3243 additions & 180 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { ensureSidebarExpanded, expect, test } from './fixtures';
21+
22+
// Real path: 设置 → 使用统计 → 定价配置. The editable Pricing tab (#2015 / PR #4164,
23+
// integrated into the features/usage slice) reads ONE Host-backed effective
24+
// pricing snapshot from the real embedded Runtime Host — no bridge stub. Per the
25+
// maintainer direction on #2218 the surface is OVERRIDES-ONLY: the table lists
26+
// only the user's custom rows, and the ~1.4k built-in catalog is reached only
27+
// through the Add flow's Typeahead picker (never rendered as a table, so nothing
28+
// heavy renders). This exercises #2015 acceptance #2 (the tab is not time-scoped:
29+
// the Usage date range/summary toolbar is gone) and #11 (the editor returns focus
30+
// to the trigger that opened it — real Electron focus the linkedom harness cannot
31+
// honestly exercise), plus the overrides-only shape and the picker/manual Add UI.
32+
test('pricing tab is overrides-only with a catalog-picker Add flow, is not time-scoped, and restores focus', async ({
33+
window: page,
34+
}) => {
35+
await ensureSidebarExpanded(page);
36+
await page.getByRole('button', { name: '设置' }).click();
37+
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
38+
39+
await page.getByRole('button', { name: '使用统计', exact: true }).click();
40+
// The Usage tabs render as a `navigation` (named by the view's aria-label)
41+
// whose tabs are `button`s.
42+
await page
43+
.getByRole('navigation', { name: '使用统计视图' })
44+
.getByRole('button', { name: '定价配置', exact: true })
45+
.click();
46+
47+
// The Pricing panel owns its own explanatory copy and its own Add control,
48+
// instead of the Usage range chrome. An enabled Add proves the snapshot loaded.
49+
await expect(page.getByText('美元 / 每百万 token。', { exact: false })).toBeVisible();
50+
const addButton = page.getByRole('button', { name: '添加定价' });
51+
await expect(addButton).toBeEnabled();
52+
53+
// #2015 acceptance #2: the Usage range + summary toolbar must be absent on the
54+
// Pricing tab so the Usage date range cannot read as a Pricing scope.
55+
await expect(page.getByRole('group', { name: '使用统计范围与刷新' })).toHaveCount(0);
56+
await expect(page.getByRole('group', { name: '使用统计汇总指标' })).toHaveCount(0);
57+
58+
// Overrides-only: the built-in catalog is never listed as table rows, so no
59+
// 来源 = 内置 cell appears anywhere on the panel (holds whether the Host has
60+
// zero or many overrides).
61+
await expect(page.getByText('内置', { exact: true })).toHaveCount(0);
62+
63+
// The Add flow opens in catalog mode and offers a manual-entry fallback;
64+
// switching to it reveals the free-text key inputs for a model not in the
65+
// catalog.
66+
await addButton.click();
67+
const editor = page.getByRole('dialog', { name: '添加定价' });
68+
await expect(editor).toBeVisible();
69+
await editor.getByRole('button', { name: '模型不在列表中?手动输入' }).click();
70+
await expect(editor.getByRole('textbox', { name: '供应商' })).toBeVisible();
71+
72+
// #2015 acceptance #11: closing the editor returns focus to the trigger.
73+
await editor.getByRole('button', { name: '取消' }).click();
74+
await expect(editor).toHaveCount(0);
75+
await expect(addButton).toBeFocused();
76+
});

apps/desktop/src/main/__tests__/desktop-session-projection.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ test('projects only present Usage Session ids into the Desktop host namespace',
226226
byProvider: [],
227227
byModel: [],
228228
byTool: [],
229-
pricing: [],
230229
provenance: EMPTY_USAGE_PROVENANCE,
231230
};
232231

0 commit comments

Comments
 (0)