diff --git a/apps/desktop/src/main/__tests__/pricing-settings-boundary.test.ts b/apps/desktop/src/main/__tests__/pricing-settings-boundary.test.ts new file mode 100644 index 0000000000..5ed92989e7 --- /dev/null +++ b/apps/desktop/src/main/__tests__/pricing-settings-boundary.test.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const PANEL = new URL('../../../src/renderer/settings/pricing-settings-panel.tsx', import.meta.url); +const SURFACE = new URL('../../../src/renderer/settings/settings-surface.tsx', import.meta.url); + +test('Pricing renderer stays behind the semantic adapter and away from legacy authorities', async () => { + const source = await readFile(PANEL, 'utf8'); + + assert.match(source, /DesktopPricingSettingsPort/); + assert.doesNotMatch(source, /window\.maka\.usage/); + assert.doesNotMatch(source, /usage:pricing:(?:list|put|reset)/); + assert.doesNotMatch(source, /@maka\/storage/); + assert.doesNotMatch(source, /runtime-host-client/); +}); + +test('production SettingsSurface keeps Pricing activation as an optional injection', async () => { + const source = await readFile(SURFACE, 'utf8'); + + assert.match(source, /pricingPort\?: DesktopPricingSettingsPort/); + assert.doesNotMatch(source, /window\.maka\.(?:usage|runtimeHost).*pricing/i); +}); diff --git a/apps/desktop/src/main/__tests__/pricing-settings-model.test.ts b/apps/desktop/src/main/__tests__/pricing-settings-model.test.ts new file mode 100644 index 0000000000..8d1503fdc4 --- /dev/null +++ b/apps/desktop/src/main/__tests__/pricing-settings-model.test.ts @@ -0,0 +1,250 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { EffectivePricingEntry } from '@maka/runtime-host/protocol'; +import type { DesktopPricingSnapshot } from '../../shared/runtime-host-pricing.js'; +import { + decidePricingRecovery, + findCurrentPricingDeleteTarget, + createPricingDraft, + formatPricingRate, + preparePricingDraftForAuthorityReview, + pricingSnapshotIdentityChanged, + pricingTargetMatchesSnapshot, + validatePricingDraft, +} from '../../renderer/settings/pricing-settings-model.js'; + +test('Pricing draft preserves exact keys and distinguishes blank cache rates from zero', () => { + const result = validatePricingDraft({ + mode: 'add', + modelKey: ' Acme:Coder-β ', + inputUsdPer1M: '1.25', + outputUsdPer1M: '2.5', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '0', + }, []); + + assert.equal(result.ok, true); + if (!result.ok) return; + assert.deepEqual(result.pricing, { + modelKey: 'Acme:Coder-β', + inputUsdPer1M: 1.25, + outputUsdPer1M: 2.5, + cacheWriteUsdPer1M: 0, + }); +}); + +test('Pricing draft rejects missing, non-finite, negative, overlong, and duplicate values', () => { + const existing = [builtin('provider:Existing', 1)]; + const invalid = validatePricingDraft({ + mode: 'add', + modelKey: 'provider:Existing', + inputUsdPer1M: '', + outputUsdPer1M: 'Infinity', + cacheReadUsdPer1M: '-0.1', + cacheWriteUsdPer1M: 'NaN', + }, existing); + assert.deepEqual(invalid, { + ok: false, + errors: { + modelKey: 'duplicate_model_key', + inputUsdPer1M: 'required', + outputUsdPer1M: 'invalid_rate', + cacheReadUsdPer1M: 'invalid_rate', + cacheWriteUsdPer1M: 'invalid_rate', + }, + }); + + const overlong = validatePricingDraft({ + mode: 'add', + modelKey: 'x'.repeat(129), + inputUsdPer1M: '0', + outputUsdPer1M: '0', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '', + }, existing); + assert.deepEqual(overlong.errors, { modelKey: 'model_key_too_long' }); + + const caseDistinct = validatePricingDraft({ + mode: 'add', + modelKey: 'provider:existing', + inputUsdPer1M: '0', + outputUsdPer1M: '0', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '', + }, existing); + assert.equal(caseDistinct.ok, true); +}); + +test('editing seeds canonical numeric strings without filling omitted optionals', () => { + assert.deepEqual(createPricingDraft(custom('provider:model', 0.00000001, 'become_unpriced')), { + mode: 'edit', + modelKey: 'provider:model', + inputUsdPer1M: '1e-8', + outputUsdPer1M: '2e-8', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '0', + }); + assert.equal(formatPricingRate(0.00000001), '$1e-8'); + assert.notEqual(formatPricingRate(0.00000001), '$0'); +}); + +test('authority review preserves an Add draft and makes an exact fresh key editable', () => { + const draft = { + mode: 'add' as const, + modelKey: ' Acme:Coder-β ', + inputUsdPer1M: '1.25', + outputUsdPer1M: '2.5', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '0', + }; + assert.deepEqual( + preparePricingDraftForAuthorityReview(draft, [builtin('Acme:Coder-β', 4)]), + { ...draft, mode: 'edit' }, + ); + assert.equal( + preparePricingDraftForAuthorityReview(draft, [builtin('acme:coder-β', 4)]), + draft, + ); +}); + +test('recovery matching includes override provenance and delete consequence', () => { + const equalBuiltin = builtin('provider:model', 4); + const equalCustom = custom('provider:model', 4, 'restore_builtin'); + const upsertTarget = { kind: 'upsert' as const, pricing: equalCustom.pricing }; + + assert.equal(pricingTargetMatchesSnapshot(upsertTarget, snapshot([equalBuiltin])), false); + assert.equal(pricingTargetMatchesSnapshot(upsertTarget, snapshot([equalCustom])), true); + assert.equal(pricingTargetMatchesSnapshot({ + kind: 'delete', + modelKey: 'provider:model', + expected: 'builtin', + }, snapshot([equalBuiltin])), true); + assert.equal(pricingTargetMatchesSnapshot({ + kind: 'delete', + modelKey: 'provider:model', + expected: 'unpriced', + }, snapshot([])), true); + assert.equal(pricingTargetMatchesSnapshot({ + kind: 'delete', + modelKey: 'provider:model', + expected: 'no_override', + }, snapshot([equalBuiltin])), true); + assert.equal(pricingTargetMatchesSnapshot({ + kind: 'delete', + modelKey: 'provider:model', + expected: 'no_override', + }, snapshot([])), true); + assert.equal(pricingTargetMatchesSnapshot({ + kind: 'delete', + modelKey: 'provider:model', + expected: 'no_override', + }, snapshot([equalCustom])), false); +}); + +test('a revision or connection change invalidates the editor save base', () => { + const base = snapshot([], 4); + assert.equal(pricingSnapshotIdentityChanged(base, snapshot([], 4)), false); + assert.equal(pricingSnapshotIdentityChanged(base, snapshot([], 5)), true); + assert.equal(pricingSnapshotIdentityChanged(base, { + ...base, + connectionId: 'connection-replaced', + }), true); + assert.equal(pricingSnapshotIdentityChanged(base, { + ...base, + hostEpoch: 'host-replaced', + }), true); +}); + +test('delayed recovery preserves the real conflict, unknown, saved, or stale cause', () => { + assert.deepEqual(decidePricingRecovery('revision_conflict', true), { + kind: 'complete', + notice: 'synchronized_conflict', + }); + assert.deepEqual(decidePricingRecovery('revision_conflict', false), { + kind: 'review', + reason: 'revision_conflict', + notice: 'review_conflict', + }); + assert.deepEqual(decidePricingRecovery('outcome_unknown', true), { + kind: 'complete', + notice: 'synchronized_unknown', + }); + assert.deepEqual(decidePricingRecovery('outcome_unknown', false), { + kind: 'review', + reason: 'outcome_unknown', + notice: 'review_unknown', + }); + assert.deepEqual(decidePricingRecovery('known_saved', false), { + kind: 'review', + reason: 'authority_changed', + notice: 'authority_changed', + }); + assert.deepEqual(decidePricingRecovery('stale', true), { + kind: 'complete', + notice: 'synchronized_conflict', + }); +}); + +test('delete review derives its consequence only from a current Custom override', () => { + assert.deepEqual(findCurrentPricingDeleteTarget( + 'provider:model', + [custom('provider:model', 4, 'restore_builtin')], + ), { + kind: 'delete', + modelKey: 'provider:model', + expected: 'builtin', + }); + assert.deepEqual(findCurrentPricingDeleteTarget( + 'provider:model', + [custom('provider:model', 4, 'become_unpriced')], + ), { + kind: 'delete', + modelKey: 'provider:model', + expected: 'unpriced', + }); + assert.equal(findCurrentPricingDeleteTarget( + 'provider:model', + [builtin('provider:model', 4)], + ), undefined); + assert.equal(findCurrentPricingDeleteTarget('provider:model', []), undefined); +}); + +function builtin(modelKey: string, rate: number): EffectivePricingEntry { + return { + pricing: { + modelKey, + inputUsdPer1M: rate, + outputUsdPer1M: rate * 2, + }, + source: 'builtin', + }; +} + +function custom( + modelKey: string, + rate: number, + resetEffect: 'restore_builtin' | 'become_unpriced', +): EffectivePricingEntry { + return { + pricing: { + modelKey, + inputUsdPer1M: rate, + outputUsdPer1M: rate * 2, + cacheWriteUsdPer1M: 0, + }, + source: 'custom', + resetEffect, + }; +} + +function snapshot( + entries: readonly EffectivePricingEntry[], + revision = 4, +): DesktopPricingSnapshot { + return { + hostEpoch: 'host-current', + connectionId: 'connection-current', + revision, + entries, + }; +} diff --git a/apps/desktop/src/main/__tests__/pricing-settings-operation-gate.test.ts b/apps/desktop/src/main/__tests__/pricing-settings-operation-gate.test.ts new file mode 100644 index 0000000000..3e3066558a --- /dev/null +++ b/apps/desktop/src/main/__tests__/pricing-settings-operation-gate.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { PricingSettingsOperationGate } from '../../renderer/settings/pricing-settings-operation-gate.js'; + +test('Pricing Settings serializes reads and writes through one operation gate', () => { + const gate = new PricingSettingsOperationGate(); + const read = gate.begin('read'); + assert.ok(read); + assert.equal(gate.activeKind, 'read'); + assert.equal(gate.begin('write'), null); + + assert.equal(gate.finish(read), true); + const write = gate.begin('write'); + assert.ok(write); + assert.equal(gate.begin('read'), null); + assert.equal(gate.finish(write), true); + assert.equal(gate.activeKind, null); +}); + +test('a late read from a replaced Pricing port cannot overwrite new authority', async () => { + const gate = new PricingSettingsOperationGate(); + const oldResponse = deferred(); + const newResponse = deferred(); + const committed: string[] = []; + + const oldRead = captureCurrent(gate, oldResponse.promise, committed); + gate.replacePort(); + const newRead = captureCurrent(gate, newResponse.promise, committed); + + newResponse.resolve('new-port'); + await newRead; + oldResponse.resolve('old-port'); + await oldRead; + + assert.deepEqual(committed, ['new-port']); + assert.equal(gate.activeKind, null); +}); + +async function captureCurrent( + gate: PricingSettingsOperationGate, + response: Promise, + committed: string[], +): Promise { + const token = gate.begin('read'); + assert.ok(token); + const value = await response; + if (gate.isCurrent(token)) committed.push(value); + gate.finish(token); +} + +function deferred(): { + readonly promise: Promise; + resolve(value: T): void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((accept) => { + resolve = accept; + }); + return { promise, resolve }; +} diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index d36021af47..ec89d154a8 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -15,10 +15,7 @@ import type { RuntimePolicy, RuntimePolicyMutation, } from "@maka/core/runtime-policy"; -import { - canonicalPricingConfigsEqual, - comparePricingModelKeys, -} from "@maka/core/usage-stats/pricing"; +import { comparePricingModelKeys } from "@maka/core/usage-stats/pricing"; import type { PricingConfig } from "@maka/core/usage-stats/types"; import { isSessionTrace, @@ -95,6 +92,20 @@ import { type TurnMessageSubmitInput, type TurnMessageSubmitResult, } from "@maka/runtime-host/protocol"; +import type { + DesktopPricingMutationInput, + DesktopPricingMutationOutcome, + DesktopPricingSnapshot, + PricingReconciliationTarget, +} from "../shared/runtime-host-pricing.js"; +import { pricingTargetMatchesSnapshot } from "../shared/runtime-host-pricing.js"; + +export type { + DesktopPricingMutationInput, + DesktopPricingMutationOutcome, + DesktopPricingSettingsPort, + DesktopPricingSnapshot, +} from "../shared/runtime-host-pricing.js"; const MAX_OPTIMISTIC_ATTEMPTS = 3; const MAX_SESSION_REVISION_ATTEMPTS = 8; @@ -130,52 +141,11 @@ export interface DesktopRuntimeHostSession { close(): Promise; } -export interface DesktopPricingSnapshot { - readonly hostEpoch: string; - readonly connectionId: string; - readonly revision: number; - readonly entries: readonly EffectivePricingEntry[]; -} - export interface DesktopSkillCatalogSnapshot { readonly revision: SkillCatalogRevision; readonly view: SkillCatalogView; readonly items: readonly SkillCatalogPageItem[]; } - -export interface DesktopPricingMutationInput { - readonly base: DesktopPricingSnapshot; - readonly mutation: PricingMutation; -} - -export type DesktopPricingMutationOutcome = - | { - readonly kind: "saved"; - readonly disposition: "committed" | "unchanged"; - readonly snapshot: DesktopPricingSnapshot; - } - | { - readonly kind: "saved_refresh_failed"; - readonly disposition: "committed" | "unchanged"; - } - | { - readonly kind: "synchronized" | "review_required"; - readonly reason: "revision_conflict" | "outcome_unknown"; - readonly snapshot: DesktopPricingSnapshot; - } - | { - readonly kind: "reconciliation_unavailable"; - readonly reason: "revision_conflict" | "outcome_unknown"; - }; - -type PricingReconciliationTarget = - | { readonly kind: "upsert"; readonly pricing: Readonly } - | { - readonly kind: "delete"; - readonly modelKey: string; - readonly expected: "builtin" | "unpriced" | "no_override"; - }; - export class DesktopRuntimeHostClient { readonly #sessions = new Set(); #closeTask: Promise | undefined; @@ -1583,33 +1553,6 @@ function createPricingReconciliationTarget( return { kind: "delete", modelKey: mutation.modelKey, expected }; } -function pricingTargetMatchesSnapshot( - target: PricingReconciliationTarget, - snapshot: DesktopPricingSnapshot, -): boolean { - const current = snapshot.entries.find( - ({ pricing }) => pricing.modelKey === pricingTargetModelKey(target), - ); - if (target.kind === "upsert") { - return ( - current?.source === "custom" && - canonicalPricingConfigsEqual(current.pricing, target.pricing) - ); - } - switch (target.expected) { - case "builtin": - return current?.source === "builtin"; - case "unpriced": - return current === undefined; - case "no_override": - return current === undefined || current.source === "builtin"; - } -} - -function pricingTargetModelKey(target: PricingReconciliationTarget): string { - return target.kind === "upsert" ? target.pricing.modelKey : target.modelKey; -} - function pricingEntriesAreCanonical( entries: readonly EffectivePricingEntry[], ): boolean { diff --git a/apps/desktop/src/renderer/locales/settings-pricing-copy.ts b/apps/desktop/src/renderer/locales/settings-pricing-copy.ts new file mode 100644 index 0000000000..c233b268ed --- /dev/null +++ b/apps/desktop/src/renderer/locales/settings-pricing-copy.ts @@ -0,0 +1,240 @@ +import type { UiCatalog, UiLocale } from '@maka/core'; +import type { PricingDraftError, PricingDraftField } from '../settings/pricing-settings-model'; + +export type PricingSettingsCopy = { + heading: string; + description: string; + disclaimer: string; + refresh: string; + refreshing: string; + addPrice: string; + loading: string; + tableAria: string; + headers: readonly [string, string, string, string, string, string, string]; + emptyTitle: string; + emptyBody: string; + notSet: string; + sourceBuiltin: string; + sourceCustomWithFallback: string; + sourceCustomOnly: string; + customize: string; + edit: string; + reset: string; + delete: string; + editor: { + addTitle: string; + editTitle: string; + modelKey: string; + modelKeyDescription: string; + inputRate: string; + outputRate: string; + cacheReadRate: string; + cacheWriteRate: string; + rateDescription: string; + cacheDescription: string; + draftValues: string; + latestValues: string; + latestMissing: string; + cancel: string; + save: string; + saveAgain: string; + }; + validation(field: PricingDraftField, error: PricingDraftError): string; + confirm: { + resetTitle: string; + deleteTitle: string; + resetDescription(modelKey: string): string; + deleteDescription(modelKey: string): string; + reviewDescription(modelKey: string, action: 'reset' | 'delete'): string; + cancel: string; + confirmAgain: string; + }; + notice: { + loadFailed: string; + loadFailedDescription(detail: string): string; + saved: string; + unchanged: string; + savedRefreshFailed: string; + savedRefreshFailedDescription: string; + synchronizedConflict: string; + synchronizedUnknown: string; + reviewConflict: string; + reviewUnknown: string; + reconciliationUnavailable: string; + reconciliationUnavailableDescription: string; + staleSnapshot: string; + staleSnapshotDescription: string; + mutationFailed: string; + mutationFailedDescription(detail: string): string; + refreshed: string; + refreshedForReview: string; + deleteNoLongerApplies: string; + reviewDelete: string; + pending: string; + }; +}; + +const SETTINGS_PRICING_COPY = { + zh: { + heading: '定价配置', + description: '单位为 USD / 百万 Token。修改适用于之后新激活的模型工作;正在运行的 Run 保持启动时的价格。', + disclaimer: '历史费用不会重算;最终账单以模型供应商为准。', + refresh: '刷新定价', + refreshing: '正在刷新定价', + addPrice: '添加价格', + loading: '正在加载生效价格…', + tableAria: '生效模型定价', + headers: ['模型 Key', '来源', '输入', '输出', '缓存读取', '缓存写入', '操作'], + emptyTitle: '暂无生效价格', + emptyBody: '添加精确的 Runtime 模型 Key,为之后新激活的模型工作设置价格。', + notSet: '未设置 · Maka 估算不计缓存费用', + sourceBuiltin: '内置', + sourceCustomWithFallback: '自定义 · 有内置回退', + sourceCustomOnly: '仅自定义', + customize: '自定义', + edit: '编辑', + reset: '恢复', + delete: '删除', + editor: { + addTitle: '添加模型价格', + editTitle: '编辑模型价格', + modelKey: '模型 Key', + modelKeyDescription: '填写 Runtime 精确查找 Key,例如 openai:gpt-4o。Key 区分大小写,不要填写连接 slug。', + inputRate: '输入 / 1M Token', + outputRate: '输出 / 1M Token', + cacheReadRate: '缓存读取 / 1M Token', + cacheWriteRate: '缓存写入 / 1M Token', + rateDescription: '必填,有限且不小于 0。', + cacheDescription: '可选;留空表示未设置,填写 0 表示显式零费率。', + draftValues: '草稿', + latestValues: '当前权威值', + latestMissing: '当前没有这个 Key', + cancel: '取消', + save: '保存', + saveAgain: '复核后保存', + }, + validation: (field, error) => { + if (error === 'required') return '此费率为必填项'; + if (error === 'invalid_rate') return '请输入有限且不小于 0 的数字'; + if (error === 'model_key_empty') return '模型 Key 不能为空'; + if (error === 'model_key_too_long') return '模型 Key 最多 128 个字符'; + if (error === 'duplicate_model_key') return '这个 Key 已存在,请从列表中编辑或自定义'; + return field === 'modelKey' ? '模型 Key 无效' : '费率无效'; + }, + confirm: { + resetTitle: '恢复内置价格?', + deleteTitle: '删除自定义价格?', + resetDescription: (modelKey) => `将删除 ${modelKey} 的 override。之后新激活的模型工作会恢复内置价格;正在运行的工作保持启动时的价格。`, + deleteDescription: (modelKey) => `将删除 ${modelKey} 的 override。之后新激活的模型工作会变为未定价,而不是 $0;正在运行的工作保持启动时的价格。`, + reviewDescription: (modelKey, action) => `${modelKey} 的当前权威状态已经变化。请核对最新列表,再次确认${action === 'reset' ? '恢复' : '删除'}。`, + cancel: '取消', + confirmAgain: '再次确认', + }, + notice: { + loadFailed: '无法加载定价', + loadFailedDescription: (detail) => `Runtime Host 没有返回生效价格。${detail}`, + saved: '定价已保存并重新加载', + unchanged: '定价未发生变化,当前列表已重新加载', + savedRefreshFailed: '保存已完成,但最新定价未能加载', + savedRefreshFailedDescription: '草稿已保留,旧列表已隐藏。刷新成功前不会发送新的写入。', + synchronizedConflict: '其他更改已经产生了相同结果,未重复写入', + synchronizedUnknown: '当前权威状态与草稿一致;无法判断是哪次命令完成了写入', + reviewConflict: '定价已被其他更改更新,请对照最新值后再次保存', + reviewUnknown: '写入结果无法确定,最新权威状态与草稿不同;请复核后再决定是否保存', + reconciliationUnavailable: '暂时无法核对写入结果', + reconciliationUnavailableDescription: '草稿已保留,旧列表已隐藏。请先刷新权威状态,不会自动重发写入。', + staleSnapshot: 'Runtime Host 连接已经变化', + staleSnapshotDescription: '草稿已保留。请刷新并核对新连接返回的价格后再保存。', + mutationFailed: '定价未保存', + mutationFailedDescription: (detail) => `Runtime Host 拒绝了这次操作。${detail}`, + refreshed: '已加载最新生效价格', + refreshedForReview: '已加载最新价格,草稿仍保留,请复核后保存', + deleteNoLongerApplies: '该 override 已不存在;已显示最新生效价格,不会发送无效删除', + reviewDelete: '复核待处理操作', + pending: '正在提交定价更改', + }, + }, + en: { + heading: 'Pricing', + description: 'USD per 1M tokens. Changes apply to newly activated model work; an active run keeps its starting prices.', + disclaimer: 'Historical costs are not recalculated. Provider billing is authoritative.', + refresh: 'Refresh pricing', + refreshing: 'Refreshing pricing', + addPrice: 'Add price', + loading: 'Loading effective prices…', + tableAria: 'Effective model pricing', + headers: ['Model key', 'Source', 'Input', 'Output', 'Cache read', 'Cache write', 'Actions'], + emptyTitle: 'No effective prices', + emptyBody: 'Add an exact Runtime model key to price newly activated model work.', + notSet: 'Not set · no cache charge in Maka estimates', + sourceBuiltin: 'Built-in', + sourceCustomWithFallback: 'Custom · built-in fallback', + sourceCustomOnly: 'Custom-only', + customize: 'Customize', + edit: 'Edit', + reset: 'Reset', + delete: 'Delete', + editor: { + addTitle: 'Add model price', + editTitle: 'Edit model price', + modelKey: 'Model key', + modelKeyDescription: 'Enter the exact Runtime lookup key, such as openai:gpt-4o. Keys are case-sensitive; do not use a connection slug.', + inputRate: 'Input / 1M tokens', + outputRate: 'Output / 1M tokens', + cacheReadRate: 'Cache read / 1M tokens', + cacheWriteRate: 'Cache write / 1M tokens', + rateDescription: 'Required, finite, and at least 0.', + cacheDescription: 'Optional. Blank means not set; 0 is an explicit zero rate.', + draftValues: 'Draft', + latestValues: 'Current authority', + latestMissing: 'This key is not currently priced', + cancel: 'Cancel', + save: 'Save', + saveAgain: 'Save after review', + }, + validation: (field, error) => { + if (error === 'required') return 'This rate is required'; + if (error === 'invalid_rate') return 'Enter a finite number that is at least 0'; + if (error === 'model_key_empty') return 'Model key cannot be empty'; + if (error === 'model_key_too_long') return 'Model key must be 128 characters or fewer'; + if (error === 'duplicate_model_key') return 'This key already exists; edit or customize it from the list'; + return field === 'modelKey' ? 'Invalid model key' : 'Invalid rate'; + }, + confirm: { + resetTitle: 'Restore built-in pricing?', + deleteTitle: 'Delete custom pricing?', + resetDescription: (modelKey) => `This deletes the override for ${modelKey}. Newly activated work will use built-in pricing; active work keeps its starting prices.`, + deleteDescription: (modelKey) => `This deletes the override for ${modelKey}. Newly activated work becomes unpriced, not $0; active work keeps its starting prices.`, + reviewDescription: (modelKey, action) => `Current authority for ${modelKey} changed. Review the latest list, then confirm ${action === 'reset' ? 'reset' : 'delete'} again.`, + cancel: 'Cancel', + confirmAgain: 'Confirm again', + }, + notice: { + loadFailed: 'Pricing could not be loaded', + loadFailedDescription: (detail) => `The Runtime Host did not return effective prices. ${detail}`, + saved: 'Pricing saved and reloaded', + unchanged: 'Pricing was unchanged and the current list was reloaded', + savedRefreshFailed: 'The save completed, but current pricing could not be loaded', + savedRefreshFailedDescription: 'The draft is preserved and the old list is hidden. No new write will be sent until refresh succeeds.', + synchronizedConflict: 'Another change already produced the same result; no write was replayed', + synchronizedUnknown: 'Current authority matches the draft; Maka cannot tell which command completed the write', + reviewConflict: 'Pricing changed elsewhere. Compare the latest value before saving again', + reviewUnknown: 'The write outcome is unknown and current authority differs from the draft. Review before deciding whether to save again', + reconciliationUnavailable: 'The write outcome cannot be reconciled yet', + reconciliationUnavailableDescription: 'The draft is preserved and the old list is hidden. Refresh authority first; the write will not be replayed automatically.', + staleSnapshot: 'The Runtime Host connection changed', + staleSnapshotDescription: 'The draft is preserved. Refresh and review pricing from the new connection before saving.', + mutationFailed: 'Pricing was not saved', + mutationFailedDescription: (detail) => `The Runtime Host rejected this operation. ${detail}`, + refreshed: 'Latest effective pricing loaded', + refreshedForReview: 'Latest pricing loaded. The draft is preserved for review', + deleteNoLongerApplies: 'The override no longer exists. Current effective pricing is shown; no invalid delete will be sent', + reviewDelete: 'Review pending action', + pending: 'Submitting pricing changes', + }, + }, +} satisfies UiCatalog; + +export function getPricingSettingsCopy(locale: UiLocale): PricingSettingsCopy { + return SETTINGS_PRICING_COPY[locale]; +} diff --git a/apps/desktop/src/renderer/settings/pricing-settings-model.ts b/apps/desktop/src/renderer/settings/pricing-settings-model.ts new file mode 100644 index 0000000000..8c3f8aed5a --- /dev/null +++ b/apps/desktop/src/renderer/settings/pricing-settings-model.ts @@ -0,0 +1,234 @@ +import { + normalizePricingConfig, + normalizePricingModelKey, +} from '@maka/core/usage-stats/pricing'; +import type { PricingConfig } from '@maka/core/usage-stats/types'; +import type { EffectivePricingEntry } from '@maka/runtime-host/protocol'; +export { + pricingTargetMatchesSnapshot, +} from '../../shared/runtime-host-pricing.js'; +import type { DesktopPricingSnapshot } from '../../shared/runtime-host-pricing.js'; + +export type PricingDraftField = + | 'modelKey' + | 'inputUsdPer1M' + | 'outputUsdPer1M' + | 'cacheReadUsdPer1M' + | 'cacheWriteUsdPer1M'; + +export type PricingDraftError = + | 'required' + | 'invalid_rate' + | 'model_key_empty' + | 'model_key_too_long' + | 'duplicate_model_key'; + +export interface PricingDraft { + readonly mode: 'add' | 'edit'; + readonly modelKey: string; + readonly inputUsdPer1M: string; + readonly outputUsdPer1M: string; + readonly cacheReadUsdPer1M: string; + readonly cacheWriteUsdPer1M: string; +} + +export type PricingDraftValidation = + | { + readonly ok: true; + readonly pricing: PricingConfig; + readonly errors: Readonly>>; + } + | { + readonly ok: false; + readonly errors: Readonly>>; + }; + +export type PricingMutationTarget = + | { readonly kind: 'upsert'; readonly pricing: Readonly } + | { + readonly kind: 'delete'; + readonly modelKey: string; + readonly expected: 'builtin' | 'unpriced'; + }; + +export type PricingReviewReason = + | 'revision_conflict' + | 'outcome_unknown' + | 'authority_changed'; + +export type PricingRecoveryCause = + | 'known_saved' + | 'revision_conflict' + | 'outcome_unknown' + | 'stale'; + +export type PricingRecoveryDecision = + | { + readonly kind: 'complete'; + readonly notice: 'saved' | 'synchronized_conflict' | 'synchronized_unknown'; + } + | { + readonly kind: 'review'; + readonly reason: PricingReviewReason; + readonly notice: 'review_conflict' | 'review_unknown' | 'authority_changed'; + }; + +export function createPricingDraft(entry?: EffectivePricingEntry): PricingDraft { + if (!entry) { + return { + mode: 'add', + modelKey: '', + inputUsdPer1M: '', + outputUsdPer1M: '', + cacheReadUsdPer1M: '', + cacheWriteUsdPer1M: '', + }; + } + return { + mode: 'edit', + modelKey: entry.pricing.modelKey, + inputUsdPer1M: String(entry.pricing.inputUsdPer1M), + outputUsdPer1M: String(entry.pricing.outputUsdPer1M), + cacheReadUsdPer1M: rateDraftValue(entry.pricing.cacheReadUsdPer1M), + cacheWriteUsdPer1M: rateDraftValue(entry.pricing.cacheWriteUsdPer1M), + }; +} + +export function preparePricingDraftForAuthorityReview( + draft: PricingDraft, + entries: readonly EffectivePricingEntry[], +): PricingDraft { + if (draft.mode !== 'add') return draft; + const modelKey = normalizePricingModelKey(draft.modelKey); + if ( + !modelKey.ok + || !entries.some((entry) => entry.pricing.modelKey === modelKey.value) + ) return draft; + return { ...draft, mode: 'edit' }; +} + +export function validatePricingDraft( + draft: PricingDraft, + entries: readonly EffectivePricingEntry[], +): PricingDraftValidation { + const errors: Partial> = {}; + const normalizedKey = normalizePricingModelKey(draft.modelKey); + if (!normalizedKey.ok) { + errors.modelKey = draft.modelKey.trim() === '' ? 'model_key_empty' : 'model_key_too_long'; + } else if ( + draft.mode === 'add' + && entries.some((entry) => entry.pricing.modelKey === normalizedKey.value) + ) { + errors.modelKey = 'duplicate_model_key'; + } + + const input = parseRate(draft.inputUsdPer1M, true); + if (!input.ok) errors.inputUsdPer1M = input.error; + const output = parseRate(draft.outputUsdPer1M, true); + if (!output.ok) errors.outputUsdPer1M = output.error; + const cacheRead = parseRate(draft.cacheReadUsdPer1M, false); + if (!cacheRead.ok) errors.cacheReadUsdPer1M = cacheRead.error; + const cacheWrite = parseRate(draft.cacheWriteUsdPer1M, false); + if (!cacheWrite.ok) errors.cacheWriteUsdPer1M = cacheWrite.error; + + if ( + !normalizedKey.ok + || !input.ok + || !output.ok + || !cacheRead.ok + || !cacheWrite.ok + || Object.keys(errors).length > 0 + ) { + return { ok: false, errors }; + } + + const canonical = normalizePricingConfig({ + modelKey: normalizedKey.value, + inputUsdPer1M: input.value, + outputUsdPer1M: output.value, + ...(cacheRead.value !== undefined ? { cacheReadUsdPer1M: cacheRead.value } : {}), + ...(cacheWrite.value !== undefined ? { cacheWriteUsdPer1M: cacheWrite.value } : {}), + }); + if (!canonical.ok) { + return { ok: false, errors: { inputUsdPer1M: 'invalid_rate' } }; + } + return { ok: true, pricing: canonical.value, errors }; +} + +/** Display is deliberately separate from editor seeding and persisted input. */ +export function formatPricingRate(rate: number): string { + return `$${String(rate)}`; +} + +export function pricingSnapshotIdentityChanged( + previous: DesktopPricingSnapshot, + current: DesktopPricingSnapshot, +): boolean { + return previous.hostEpoch !== current.hostEpoch + || previous.connectionId !== current.connectionId + || previous.revision !== current.revision; +} + +export function decidePricingRecovery( + cause: PricingRecoveryCause, + targetMatches: boolean, +): PricingRecoveryDecision { + if (targetMatches) { + if (cause === 'known_saved') return { kind: 'complete', notice: 'saved' }; + if (cause === 'outcome_unknown') { + return { kind: 'complete', notice: 'synchronized_unknown' }; + } + return { kind: 'complete', notice: 'synchronized_conflict' }; + } + if (cause === 'revision_conflict') { + return { kind: 'review', reason: 'revision_conflict', notice: 'review_conflict' }; + } + if (cause === 'outcome_unknown') { + return { kind: 'review', reason: 'outcome_unknown', notice: 'review_unknown' }; + } + return { kind: 'review', reason: 'authority_changed', notice: 'authority_changed' }; +} + +export function createPricingDeleteTarget( + entry: Extract, +): Extract { + return { + kind: 'delete', + modelKey: entry.pricing.modelKey, + expected: entry.resetEffect === 'restore_builtin' ? 'builtin' : 'unpriced', + }; +} + +export function findCurrentPricingDeleteTarget( + modelKey: string, + entries: readonly EffectivePricingEntry[], +): Extract | undefined { + const entry = entries.find( + (candidate): candidate is Extract => + candidate.pricing.modelKey === modelKey && candidate.source === 'custom', + ); + return entry ? createPricingDeleteTarget(entry) : undefined; +} + +function rateDraftValue(rate: number | undefined): string { + return rate === undefined ? '' : String(rate); +} + +function parseRate( + raw: string, + required: boolean, +): + | { readonly ok: true; readonly value: number | undefined } + | { readonly ok: false; readonly error: PricingDraftError } { + const trimmed = raw.trim(); + if (trimmed === '') { + return required + ? { ok: false, error: 'required' } + : { ok: true, value: undefined }; + } + const value = Number(trimmed); + if (!Number.isFinite(value) || value < 0) { + return { ok: false, error: 'invalid_rate' }; + } + return { ok: true, value }; +} diff --git a/apps/desktop/src/renderer/settings/pricing-settings-operation-gate.ts b/apps/desktop/src/renderer/settings/pricing-settings-operation-gate.ts new file mode 100644 index 0000000000..d28f6aa981 --- /dev/null +++ b/apps/desktop/src/renderer/settings/pricing-settings-operation-gate.ts @@ -0,0 +1,53 @@ +export type PricingSettingsOperationKind = 'read' | 'write'; + +export interface PricingSettingsOperationToken { + readonly generation: number; + readonly id: number; + readonly kind: PricingSettingsOperationKind; +} + +/** + * Serializes Pricing Settings reads and writes and invalidates completions from + * a replaced semantic port. React state mirrors this gate for presentation; + * the token remains the authority at every async completion boundary. + * Unlike the shared action guards, the generation is part of correctness here: + * replacing the port must invalidate an already-running read or write. + */ +export class PricingSettingsOperationGate { + #generation = 0; + #nextId = 0; + #active: PricingSettingsOperationToken | null = null; + + get activeKind(): PricingSettingsOperationKind | null { + return this.#active?.kind ?? null; + } + + begin(kind: PricingSettingsOperationKind): PricingSettingsOperationToken | null { + if (this.#active) return null; + const token = { + generation: this.#generation, + id: this.#nextId, + kind, + } satisfies PricingSettingsOperationToken; + this.#nextId += 1; + this.#active = token; + return token; + } + + isCurrent(token: PricingSettingsOperationToken): boolean { + return this.#active?.generation === token.generation + && this.#active.id === token.id + && this.#active.kind === token.kind; + } + + finish(token: PricingSettingsOperationToken): boolean { + if (!this.isCurrent(token)) return false; + this.#active = null; + return true; + } + + replacePort(): void { + this.#generation += 1; + this.#active = null; + } +} diff --git a/apps/desktop/src/renderer/settings/pricing-settings-panel.tsx b/apps/desktop/src/renderer/settings/pricing-settings-panel.tsx new file mode 100644 index 0000000000..0386ffd2a8 --- /dev/null +++ b/apps/desktop/src/renderer/settings/pricing-settings-panel.tsx @@ -0,0 +1,1025 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, + type FormEvent, + type ReactNode, +} from 'react'; +import { + AlertDialog, + Badge, + Banner, + Button, + Card, + Dialog, + DialogHeader, + EmptyState, + FormLayout, + HStack, + Layout, + LayoutContent, + LayoutFooter, + Skeleton, + Table, + Text, + TextInput, + type BannerStatus, + type TableColumn, + type TablePlugin, + pixel, + proportional, +} from '@astryxdesign/core'; +import type { PricingConfig } from '@maka/core/usage-stats/types'; +import type { EffectivePricingEntry, PricingMutation } from '@maka/runtime-host/protocol'; +import { useMountedRef, useUiLocale } from '@maka/ui'; +import { ICON_SIZE, BarChart3, Plus, RefreshCcw } from '@maka/ui/icons'; +import type { + DesktopPricingSettingsPort, + DesktopPricingSnapshot, +} from '../../shared/runtime-host-pricing'; +import { + getPricingSettingsCopy, + type PricingSettingsCopy, +} from '../locales/settings-pricing-copy'; +import { settingsActionErrorMessage } from './settings-error-copy'; +import { + createPricingDeleteTarget, + createPricingDraft, + decidePricingRecovery, + findCurrentPricingDeleteTarget, + formatPricingRate, + preparePricingDraftForAuthorityReview, + pricingSnapshotIdentityChanged, + pricingTargetMatchesSnapshot, + validatePricingDraft, + type PricingDraft, + type PricingDraftField, + type PricingMutationTarget, + type PricingRecoveryCause, + type PricingRecoveryDecision, + type PricingReviewReason, +} from './pricing-settings-model'; +import { PricingSettingsOperationGate } from './pricing-settings-operation-gate'; +import { SettingsSection } from './settings-section'; + +interface EditorSession { + readonly draft: PricingDraft; + readonly touched: Readonly>>; + readonly review?: PricingReviewReason; +} + +interface DeleteSession { + readonly modelKey: string; + readonly action: 'reset' | 'delete'; + readonly target: Extract; + readonly open: boolean; + readonly review?: PricingReviewReason; +} + +interface RecoveryState { + readonly cause: PricingRecoveryCause; + readonly target: PricingMutationTarget; +} + +interface PricingNotice { + readonly status: BannerStatus; + readonly title: string; + readonly description?: string; +} + +type PricingTableRow = Record & { + readonly modelKey: string; + readonly entry: EffectivePricingEntry; +}; + +const pricingTablePlugins = { + rowHeader: { + transformBodyCell: (cell, _column, _row, columnIndex) => columnIndex === 0 + ? { ...cell, htmlProps: { ...cell.htmlProps, role: 'rowheader' } } + : cell, + }, +} satisfies Record>; + +export function PricingSettingsPanel(props: { port: DesktopPricingSettingsPort }) { + const locale = useUiLocale(); + const copy = getPricingSettingsCopy(locale); + const mountedRef = useMountedRef(); + const copyRef = useRef(copy); + const localeRef = useRef(locale); + copyRef.current = copy; + localeRef.current = locale; + + const [snapshot, setSnapshot] = useState(null); + const snapshotRef = useRef(null); + const [initialLoading, setInitialLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [writePending, setWritePending] = useState(false); + const [notice, setNotice] = useState(null); + const operationGateRef = useRef(new PricingSettingsOperationGate()); + const portRef = useRef(props.port); + const appliedPortRef = useRef(null); + const authorityReviewPendingRef = useRef(false); + portRef.current = props.port; + + const [editor, setEditor] = useState(null); + const editorRef = useRef(null); + const [deleteSession, setDeleteSession] = useState(null); + const deleteSessionRef = useRef(null); + const recoveryRef = useRef(null); + + const addButtonRef = useRef(null); + const refreshButtonRef = useRef(null); + const returnFocusRef = useRef(null); + const shouldRestoreFocusRef = useRef(false); + + function replaceSnapshot(next: DesktopPricingSnapshot | null) { + snapshotRef.current = next; + setSnapshot(next); + } + + function replaceEditor(next: EditorSession | null) { + editorRef.current = next; + setEditor(next); + } + + function updateEditor(update: (current: EditorSession) => EditorSession) { + const current = editorRef.current; + if (!current) return; + replaceEditor(update(current)); + } + + function replaceDeleteSession(next: DeleteSession | null) { + deleteSessionRef.current = next; + setDeleteSession(next); + } + + function replaceRecovery(next: RecoveryState | null) { + recoveryRef.current = next; + } + + function requestFocusRestore() { + shouldRestoreFocusRef.current = true; + } + + useEffect(() => { + if (!shouldRestoreFocusRef.current || editor !== null || deleteSession?.open) return; + shouldRestoreFocusRef.current = false; + const requested = returnFocusRef.current; + const fallback = addButtonRef.current && !addButtonRef.current.disabled + ? addButtonRef.current + : refreshButtonRef.current; + (requested?.isConnected ? requested : fallback)?.focus(); + returnFocusRef.current = null; + }, [editor, deleteSession]); + + function reviewEditor( + reason: PricingReviewReason, + latest: DesktopPricingSnapshot, + ): boolean { + if (!editorRef.current) return false; + updateEditor((current) => ({ + ...current, + draft: preparePricingDraftForAuthorityReview(current.draft, latest.entries), + review: reason, + })); + return true; + } + + function reviewIntent( + target: PricingMutationTarget, + reason: PricingReviewReason, + latest: DesktopPricingSnapshot, + ): 'review' | 'no_override' | 'missing_intent' { + if (target.kind === 'upsert') { + return reviewEditor(reason, latest) + ? 'review' + : 'missing_intent'; + } + const current = deleteSessionRef.current; + if (!current) return 'missing_intent'; + const nextTarget = findCurrentPricingDeleteTarget(target.modelKey, latest.entries); + if (!nextTarget) { + replaceDeleteSession(null); + requestFocusRestore(); + return 'no_override'; + } + replaceDeleteSession({ + ...current, + action: nextTarget.expected === 'builtin' ? 'reset' : 'delete', + target: nextTarget, + open: false, + review: reason, + }); + return 'review'; + } + + function clearIntent(target: PricingMutationTarget) { + if (target.kind === 'upsert') replaceEditor(null); + else replaceDeleteSession(null); + requestFocusRestore(); + } + + const loadSnapshot = useCallback(async ( + announce: boolean, + requestedPort: DesktopPricingSettingsPort = portRef.current, + ) => { + const gate = operationGateRef.current; + const operation = gate.begin('read'); + if (!operation) return; + const previous = snapshotRef.current; + if (previous === null) setInitialLoading(true); + setRefreshing(true); + try { + const latest = await requestedPort.loadPricingSnapshot(); + if (!mountedRef.current || !gate.isCurrent(operation)) return; + replaceSnapshot(latest); + const pendingRecovery = recoveryRef.current; + if (pendingRecovery) { + replaceRecovery(null); + const decision = decidePricingRecovery( + pendingRecovery.cause, + pricingTargetMatchesSnapshot(pendingRecovery.target, latest), + ); + if (decision.kind === 'complete') { + clearIntent(pendingRecovery.target); + setNotice({ + status: decision.notice === 'saved' ? 'success' : 'info', + title: recoveryNoticeTitle(decision.notice, copyRef.current), + }); + } else { + const reviewed = reviewIntent(pendingRecovery.target, decision.reason, latest); + setNotice(reviewed === 'no_override' + ? { status: 'info', title: copyRef.current.notice.deleteNoLongerApplies } + : { status: 'warning', title: recoveryNoticeTitle(decision.notice, copyRef.current) }); + } + } else if (authorityReviewPendingRef.current) { + authorityReviewPendingRef.current = false; + const currentEditor = editorRef.current; + if (currentEditor) { + reviewEditor('authority_changed', latest); + } + const currentDelete = deleteSessionRef.current; + const deleteReview = currentDelete + ? reviewIntent(currentDelete.target, 'authority_changed', latest) + : 'missing_intent'; + setNotice(deleteReview === 'no_override' && !currentEditor + ? { status: 'info', title: copyRef.current.notice.deleteNoLongerApplies } + : currentEditor || deleteReview === 'review' + ? { status: 'warning', title: copyRef.current.notice.refreshedForReview } + : null); + } else if ( + previous + && pricingSnapshotIdentityChanged(previous, latest) + && (editorRef.current || deleteSessionRef.current) + ) { + const currentEditor = editorRef.current; + if (currentEditor) { + reviewEditor('authority_changed', latest); + } + const currentDelete = deleteSessionRef.current; + const deleteReview = currentDelete + ? reviewIntent(currentDelete.target, 'authority_changed', latest) + : 'missing_intent'; + setNotice(deleteReview === 'no_override' && !currentEditor + ? { status: 'info', title: copyRef.current.notice.deleteNoLongerApplies } + : { status: 'warning', title: copyRef.current.notice.refreshedForReview }); + } else if (announce) { + setNotice({ status: 'success', title: copyRef.current.notice.refreshed }); + } else { + setNotice(null); + } + } catch (error) { + if (!mountedRef.current || !gate.isCurrent(operation)) return; + const detail = settingsActionErrorMessage(error, localeRef.current); + setNotice({ + status: 'error', + title: copyRef.current.notice.loadFailed, + description: copyRef.current.notice.loadFailedDescription(detail), + }); + } finally { + if (gate.finish(operation) && mountedRef.current) { + setInitialLoading(false); + setRefreshing(false); + } + } + }, [mountedRef]); + + useLayoutEffect(() => { + const replacement = appliedPortRef.current !== null + && appliedPortRef.current !== props.port; + appliedPortRef.current = props.port; + operationGateRef.current.replacePort(); + setRefreshing(false); + setWritePending(false); + replaceSnapshot(null); + replaceRecovery(null); + if (replacement) { + const currentEditor = editorRef.current; + const currentDelete = deleteSessionRef.current; + if (currentEditor) { + replaceEditor({ ...currentEditor, review: 'authority_changed' }); + } + if (currentDelete) { + replaceDeleteSession({ ...currentDelete, open: false, review: 'authority_changed' }); + } + authorityReviewPendingRef.current = Boolean(currentEditor || currentDelete); + setNotice(currentEditor || currentDelete + ? { + status: 'warning', + title: copyRef.current.notice.staleSnapshot, + description: copyRef.current.notice.staleSnapshotDescription, + } + : null); + } else { + authorityReviewPendingRef.current = false; + setNotice(null); + } + setInitialLoading(true); + void loadSnapshot(false, props.port); + }, [loadSnapshot, props.port]); + + function openEditor(entry: EffectivePricingEntry | undefined, trigger: HTMLButtonElement) { + if (!snapshotRef.current || operationGateRef.current.activeKind !== null) return; + returnFocusRef.current = trigger; + replaceEditor({ draft: createPricingDraft(entry), touched: {} }); + } + + function closeEditor() { + if (operationGateRef.current.activeKind === 'write') return; + if (recoveryRef.current?.target.kind === 'upsert') replaceRecovery(null); + replaceEditor(null); + setNotice((current) => snapshotRef.current === null && current + ? { ...current, description: undefined } + : null); + requestFocusRestore(); + } + + function openDelete(entry: EffectivePricingEntry, trigger: HTMLButtonElement) { + if ( + entry.source !== 'custom' + || !snapshotRef.current + || operationGateRef.current.activeKind !== null + ) return; + const target = createPricingDeleteTarget(entry); + returnFocusRef.current = trigger; + replaceDeleteSession({ + modelKey: entry.pricing.modelKey, + action: target.expected === 'builtin' ? 'reset' : 'delete', + target, + open: true, + }); + } + + function closeDelete() { + if (operationGateRef.current.activeKind === 'write') return; + const current = deleteSessionRef.current; + replaceDeleteSession(null); + if (current?.review) setNotice(null); + requestFocusRestore(); + } + + async function applyMutation(target: PricingMutationTarget, mutation: PricingMutation) { + const base = snapshotRef.current; + if (!base) return; + const gate = operationGateRef.current; + const operation = gate.begin('write'); + if (!operation) return; + const requestedPort = portRef.current; + setWritePending(true); + setNotice(null); + try { + const outcome = await requestedPort.applyPricingMutation({ base, mutation }); + if (!mountedRef.current || !gate.isCurrent(operation)) return; + switch (outcome.kind) { + case 'saved': + replaceSnapshot(outcome.snapshot); + clearIntent(target); + setNotice({ + status: 'success', + title: outcome.disposition === 'committed' + ? copyRef.current.notice.saved + : copyRef.current.notice.unchanged, + }); + break; + case 'saved_refresh_failed': + replaceSnapshot(null); + replaceRecovery({ cause: 'known_saved', target }); + if (target.kind === 'delete' && deleteSessionRef.current) { + replaceDeleteSession({ ...deleteSessionRef.current, open: false }); + } + setNotice({ + status: 'warning', + title: copyRef.current.notice.savedRefreshFailed, + description: copyRef.current.notice.savedRefreshFailedDescription, + }); + break; + case 'synchronized': + replaceSnapshot(outcome.snapshot); + clearIntent(target); + setNotice({ + status: 'info', + title: outcome.reason === 'revision_conflict' + ? copyRef.current.notice.synchronizedConflict + : copyRef.current.notice.synchronizedUnknown, + }); + break; + case 'review_required': + replaceSnapshot(outcome.snapshot); + setNotice(reviewIntent(target, outcome.reason, outcome.snapshot) === 'no_override' + ? { status: 'info', title: copyRef.current.notice.deleteNoLongerApplies } + : { + status: 'warning', + title: outcome.reason === 'revision_conflict' + ? copyRef.current.notice.reviewConflict + : copyRef.current.notice.reviewUnknown, + }); + break; + case 'reconciliation_unavailable': + replaceSnapshot(null); + replaceRecovery({ cause: outcome.reason, target }); + if (target.kind === 'delete' && deleteSessionRef.current) { + replaceDeleteSession({ ...deleteSessionRef.current, open: false }); + } + setNotice({ + status: 'warning', + title: copyRef.current.notice.reconciliationUnavailable, + description: copyRef.current.notice.reconciliationUnavailableDescription, + }); + break; + } + } catch (error) { + if (!mountedRef.current || !gate.isCurrent(operation)) return; + if (runtimeHostErrorCode(error) === 'pricing_snapshot_stale') { + replaceSnapshot(null); + replaceRecovery({ cause: 'stale', target }); + if (target.kind === 'delete' && deleteSessionRef.current) { + replaceDeleteSession({ ...deleteSessionRef.current, open: false }); + } + setNotice({ + status: 'warning', + title: copyRef.current.notice.staleSnapshot, + description: copyRef.current.notice.staleSnapshotDescription, + }); + } else { + if (target.kind === 'delete' && deleteSessionRef.current) { + replaceDeleteSession({ ...deleteSessionRef.current, open: false }); + } + setNotice({ + status: 'error', + title: copyRef.current.notice.mutationFailed, + description: copyRef.current.notice.mutationFailedDescription( + settingsActionErrorMessage(error, localeRef.current), + ), + }); + } + } finally { + if (gate.finish(operation) && mountedRef.current) setWritePending(false); + } + } + + const entries = snapshot?.entries ?? []; + const data: PricingTableRow[] = entries.map((entry) => ({ + modelKey: entry.pricing.modelKey, + entry, + })); + const columns: Array> = [ + { + key: 'modelKey', + header: copy.headers[0], + width: proportional(2, { minWidth: 180 }), + renderCell: (row) => {row.modelKey}, + }, + { + key: 'source', + header: copy.headers[1], + width: pixel(150), + renderCell: (row) => , + }, + ...(['inputUsdPer1M', 'outputUsdPer1M', 'cacheReadUsdPer1M', 'cacheWriteUsdPer1M'] as const) + .map((field, index): TableColumn => ({ + key: field, + header: copy.headers[index + 2], + align: 'end', + width: pixel(field.startsWith('cache') ? 112 : 72), + renderCell: (row) => ( + + {rateCell(row.entry.pricing[field], copy)} + + ), + })), + { + key: 'actions', + header: copy.headers[6], + width: pixel(132), + resizable: false, + renderCell: (row) => ( + +