@narumitw/pi-usage 0.60.0 → 0.60.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/usage.ts CHANGED
@@ -37,7 +37,11 @@ import {
37
37
  queryProviderUsage,
38
38
  resolveUsageAuth,
39
39
  } from "./query.js";
40
- import { createUsageSettingsRuntime, type UsageSettingsRuntime } from "./settings.js";
40
+ import {
41
+ createUsageSettingsRuntime,
42
+ type UsageSettingsRuntime,
43
+ type UsageSettingsState,
44
+ } from "./settings.js";
41
45
  import type {
42
46
  PiModel,
43
47
  ProviderUsageState,
@@ -54,6 +58,11 @@ import {
54
58
  setBoundedMap,
55
59
  } from "./usage-helpers.js";
56
60
  import { showUsageSettings } from "./usage-settings-ui.js";
61
+ import {
62
+ createUsageTargetSelectOptions,
63
+ listUsageTargets,
64
+ resolveUsageTarget,
65
+ } from "./usage-targets.js";
57
66
 
58
67
  const CACHE_TTL_MS = 5 * 60 * 1000;
59
68
  const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
@@ -80,8 +89,13 @@ type QueryOutcome = {
80
89
  state: ProviderUsageState;
81
90
  fingerprint?: string;
82
91
  authState?: "unavailable";
92
+ rememberedTargetId?: string;
83
93
  };
84
94
 
95
+ class UsageTargetSelectionChangedError extends Error {
96
+ override readonly name = "UsageTargetSelectionChangedError";
97
+ }
98
+
85
99
  type StableCurrent = {
86
100
  outcome: QueryOutcome;
87
101
  model: PiModel | undefined;
@@ -174,7 +188,11 @@ export default function usageExtension(
174
188
  if (
175
189
  safeSetStatus(
176
190
  ctx,
177
- outcome.state.status === "auth-unavailable" ? "auth unavailable" : "usage error",
191
+ outcome.state.status === "auth-unavailable"
192
+ ? "auth unavailable"
193
+ : outcome.state.status === "selection-required"
194
+ ? "selection required"
195
+ : "usage error",
178
196
  )
179
197
  ) {
180
198
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
@@ -248,10 +266,10 @@ export default function usageExtension(
248
266
  const expectedSessionGeneration = sessionGeneration;
249
267
  const expectedSessionId = ctx.sessionManager.getSessionId();
250
268
  const expectedModelIdentity = modelIdentity(ctx.model);
251
- const expectedFireworksAccountId =
252
- adapter.id === "fireworks" ? settingsRuntime.get().settings.fireworksAccountId : undefined;
253
- const querySettings =
254
- adapter.id === "fireworks" ? { fireworksAccountId: expectedFireworksAccountId } : undefined;
269
+ const expectedTargetId = adapter.targets
270
+ ? settingsRuntime.get().settings.selectedTargets[adapter.id]
271
+ : undefined;
272
+ const providerName = providerDisplayName(ctx, adapter.id);
255
273
  let auth: ResolvedUsageAuth | undefined;
256
274
  try {
257
275
  auth = await awaitWithDeadline(
@@ -268,32 +286,33 @@ export default function usageExtension(
268
286
  return {
269
287
  state: {
270
288
  providerId: adapter.id,
271
- providerName: adapter.displayName,
289
+ providerName,
272
290
  displayState,
273
291
  status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
274
292
  message: errorMessage(error),
275
293
  },
276
294
  };
277
295
  }
278
- const requiresRequestBoundaryGuard = [
279
- "baseten",
280
- "deepseek",
281
- "fireworks",
282
- "minimax",
283
- "minimax-cn",
284
- "moonshotai",
285
- "moonshotai-cn",
286
- "vercel-ai-gateway",
287
- "xai",
288
- "zai",
289
- "zai-coding-cn",
290
- ].includes(adapter.id);
296
+ const requiresRequestBoundaryGuard =
297
+ adapter.targets !== undefined ||
298
+ [
299
+ "baseten",
300
+ "deepseek",
301
+ "minimax",
302
+ "minimax-cn",
303
+ "moonshotai",
304
+ "moonshotai-cn",
305
+ "vercel-ai-gateway",
306
+ "xai",
307
+ "zai",
308
+ "zai-coding-cn",
309
+ ].includes(adapter.id);
291
310
  const requestContextChanged = () =>
292
311
  expectedSessionGeneration !== sessionGeneration ||
293
312
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
294
313
  modelIdentity(ctx.model) !== expectedModelIdentity ||
295
- (adapter.id === "fireworks" &&
296
- settingsRuntime.get().settings.fireworksAccountId !== expectedFireworksAccountId);
314
+ (adapter.targets !== undefined &&
315
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
297
316
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
298
317
  if (!auth) {
299
318
  if (displayState === "current") {
@@ -302,101 +321,142 @@ export default function usageExtension(
302
321
  return {
303
322
  state: {
304
323
  providerId: adapter.id,
305
- providerName: adapter.displayName,
324
+ providerName,
306
325
  displayState,
307
326
  status: "auth-unavailable",
308
- message: `No runtime credential is configured for ${adapter.displayName}.`,
327
+ message: `No runtime credential is configured for ${providerName}.`,
309
328
  },
310
329
  authState: "unavailable",
311
330
  };
312
331
  }
313
- const queryFingerprint =
314
- adapter.id === "fireworks"
315
- ? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
316
- : auth.fingerprint;
317
- if (displayState === "current") {
318
- transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
319
- }
320
-
321
- const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
322
- if (cached) {
323
- return {
324
- state: {
325
- providerId: adapter.id,
326
- providerName: adapter.displayName,
327
- displayState,
328
- status: "ready",
329
- report: cached,
330
- },
331
- fingerprint: auth.fingerprint,
332
- };
333
- }
334
-
335
- const failureKey = `${adapter.id}:${queryFingerprint}`;
336
- const previousFailure = failureBackoff.get(failureKey);
337
- if (!force && previousFailure && previousFailure.until > Date.now()) {
338
- return {
339
- state: {
340
- providerId: adapter.id,
341
- providerName: adapter.displayName,
342
- displayState,
343
- status: "query-failed",
344
- message: previousFailure.message,
345
- },
346
- fingerprint: auth.fingerprint,
347
- };
348
- }
349
- failureBackoff.delete(failureKey);
350
- querySequence += 1;
351
- const queryId = querySequence;
352
- setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
353
-
354
332
  let retryableAuthChanged = false;
333
+ const guard = async () => {
334
+ if (signal.aborted || requestContextChanged()) throw abortError();
335
+ if (!requiresRequestBoundaryGuard) return;
336
+ const revalidated = await awaitWithDeadline(
337
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
338
+ signal,
339
+ Math.max(1, deadlineAt - Date.now()),
340
+ `revalidating ${providerName} runtime auth`,
341
+ );
342
+ if (signal.aborted || requestContextChanged()) throw abortError();
343
+ if (revalidated?.fingerprint !== auth.fingerprint) {
344
+ if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
345
+ retryableAuthChanged = true;
346
+ throw new Error(`${providerName} runtime credential changed during the usage query.`);
347
+ }
348
+ throw abortError();
349
+ }
350
+ };
351
+ let queryFingerprint = adapter.targets
352
+ ? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
353
+ : auth.fingerprint;
354
+ let failureKey = `${adapter.id}:${queryFingerprint}`;
355
+ let queryId: number | undefined;
355
356
  try {
356
- const remainingMs = Math.max(1, deadlineAt - Date.now());
357
- const guard = requiresRequestBoundaryGuard
358
- ? async () => {
359
- if (signal.aborted || requestContextChanged()) throw abortError();
360
- const revalidated = await awaitWithDeadline(
361
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
362
- signal,
363
- Math.max(1, deadlineAt - Date.now()),
364
- `revalidating ${adapter.displayName} runtime auth`,
365
- );
366
- if (signal.aborted || requestContextChanged()) throw abortError();
367
- if (revalidated?.fingerprint !== auth.fingerprint) {
368
- if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
369
- retryableAuthChanged = true;
370
- throw new Error(
371
- `${adapter.displayName} runtime credential changed during the usage query.`,
372
- );
373
- }
374
- throw abortError();
375
- }
376
- }
377
- : undefined;
378
- const report = await queryProviderUsage(
357
+ const previousDiscoveryFailure = failureBackoff.get(failureKey);
358
+ if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
359
+ return {
360
+ state: {
361
+ providerId: adapter.id,
362
+ providerName,
363
+ displayState,
364
+ status: "query-failed",
365
+ message: previousDiscoveryFailure.message,
366
+ },
367
+ fingerprint: auth.fingerprint,
368
+ rememberedTargetId: expectedTargetId,
369
+ };
370
+ }
371
+ const target = await resolveUsageTarget(
379
372
  adapter,
380
373
  auth,
374
+ expectedTargetId,
381
375
  signal,
382
- remainingMs,
376
+ Math.max(1, deadlineAt - Date.now()),
383
377
  guard,
384
- querySettings,
385
378
  );
386
- if (guard) await guard();
379
+ if (target.kind === "selection-required") {
380
+ if (displayState === "current") {
381
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
382
+ }
383
+ return {
384
+ state: {
385
+ providerId: adapter.id,
386
+ providerName,
387
+ displayState,
388
+ status: "selection-required",
389
+ singularLabel: adapter.targets?.singularLabel ?? "target",
390
+ pluralLabel: adapter.targets?.pluralLabel ?? "targets",
391
+ choices: target.choices,
392
+ },
393
+ fingerprint: auth.fingerprint,
394
+ rememberedTargetId: expectedTargetId,
395
+ };
396
+ }
397
+ queryFingerprint = adapter.targets
398
+ ? `${auth.fingerprint}:target:${target.targetId ?? "none"}`
399
+ : auth.fingerprint;
400
+ failureKey = `${adapter.id}:${queryFingerprint}`;
401
+ if (displayState === "current") {
402
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
403
+ }
404
+ const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
405
+ if (cached) {
406
+ return {
407
+ state: {
408
+ providerId: adapter.id,
409
+ providerName,
410
+ displayState,
411
+ status: "ready",
412
+ report: cached,
413
+ },
414
+ fingerprint: auth.fingerprint,
415
+ rememberedTargetId: expectedTargetId,
416
+ };
417
+ }
418
+ const previousFailure = failureBackoff.get(failureKey);
419
+ if (!force && previousFailure && previousFailure.until > Date.now()) {
420
+ return {
421
+ state: {
422
+ providerId: adapter.id,
423
+ providerName,
424
+ displayState,
425
+ status: "query-failed",
426
+ message: previousFailure.message,
427
+ },
428
+ fingerprint: auth.fingerprint,
429
+ rememberedTargetId: expectedTargetId,
430
+ };
431
+ }
432
+ failureBackoff.delete(failureKey);
433
+ querySequence += 1;
434
+ queryId = querySequence;
435
+ setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
436
+ const report = await queryProviderUsage(
437
+ adapter,
438
+ auth,
439
+ signal,
440
+ Math.max(1, deadlineAt - Date.now()),
441
+ requiresRequestBoundaryGuard ? guard : undefined,
442
+ target.targetId,
443
+ );
444
+ if (requiresRequestBoundaryGuard) await guard();
445
+ const effectiveReport = { ...report, providerName };
387
446
  if (latestQueries.get(failureKey) === queryId) {
388
- cache.set(adapter.id, queryFingerprint, report);
447
+ cache.set(adapter.id, queryFingerprint, effectiveReport);
389
448
  failureBackoff.delete(failureKey);
390
449
  }
391
450
  return {
392
451
  state: {
393
452
  providerId: adapter.id,
394
- providerName: adapter.displayName,
453
+ providerName,
395
454
  displayState,
396
455
  status: "ready",
397
- report,
456
+ report: effectiveReport,
398
457
  },
399
458
  fingerprint: auth.fingerprint,
459
+ rememberedTargetId: expectedTargetId,
400
460
  };
401
461
  } catch (error) {
402
462
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
@@ -407,7 +467,9 @@ export default function usageExtension(
407
467
  !requestContextChanged() &&
408
468
  Date.now() < deadlineAt
409
469
  ) {
410
- if (latestQueries.get(failureKey) === queryId) latestQueries.delete(failureKey);
470
+ if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
471
+ latestQueries.delete(failureKey);
472
+ }
411
473
  return queryAdapterState(
412
474
  ctx,
413
475
  adapter,
@@ -423,7 +485,7 @@ export default function usageExtension(
423
485
  for (const [key, failure] of failureBackoff) {
424
486
  if (failure.until <= now) failureBackoff.delete(key);
425
487
  }
426
- if (latestQueries.get(failureKey) === queryId) {
488
+ if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
427
489
  setBoundedMap(
428
490
  failureBackoff,
429
491
  failureKey,
@@ -434,16 +496,62 @@ export default function usageExtension(
434
496
  return {
435
497
  state: {
436
498
  providerId: adapter.id,
437
- providerName: adapter.displayName,
499
+ providerName,
438
500
  displayState,
439
501
  status: "query-failed",
440
502
  message,
441
503
  },
442
504
  fingerprint: auth.fingerprint,
505
+ rememberedTargetId: expectedTargetId,
443
506
  };
444
507
  }
445
508
  };
446
509
 
510
+ const loadTargetChoices = async (
511
+ ctx: ExtensionContext,
512
+ adapter: UsageProviderAdapter,
513
+ signal: AbortSignal,
514
+ ) => {
515
+ if (!adapter.targets) throw new Error("Provider does not support usage targets.");
516
+ const expectedSessionGeneration = sessionGeneration;
517
+ const expectedSessionId = ctx.sessionManager.getSessionId();
518
+ const expectedModel = modelIdentity(ctx.model);
519
+ const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
520
+ const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
521
+ const changed = () =>
522
+ expectedSessionGeneration !== sessionGeneration ||
523
+ ctx.sessionManager.getSessionId() !== expectedSessionId ||
524
+ modelIdentity(ctx.model) !== expectedModel ||
525
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
526
+ const auth = await awaitWithDeadline(
527
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
528
+ signal,
529
+ Math.max(1, deadlineAt - Date.now()),
530
+ `resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
531
+ );
532
+ if (!auth || signal.aborted || changed()) throw abortError();
533
+ const guard = async () => {
534
+ if (signal.aborted || changed()) throw abortError();
535
+ const revalidated = await awaitWithDeadline(
536
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
537
+ signal,
538
+ Math.max(1, deadlineAt - Date.now()),
539
+ `revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
540
+ );
541
+ if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
542
+ throw abortError();
543
+ }
544
+ };
545
+ const choices = await listUsageTargets(
546
+ adapter,
547
+ auth,
548
+ signal,
549
+ Math.max(1, deadlineAt - Date.now()),
550
+ guard,
551
+ );
552
+ return { choices, fingerprint: auth.fingerprint };
553
+ };
554
+
447
555
  const queryCurrentState = async (
448
556
  ctx: ExtensionContext,
449
557
  model: PiModel | undefined,
@@ -558,6 +666,10 @@ export default function usageExtension(
558
666
  return false;
559
667
  }
560
668
  const adapter = adapterForProvider(model?.provider);
669
+ const selectionStillCurrent =
670
+ !adapter?.targets ||
671
+ settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
672
+ if (!selectionStillCurrent) return false;
561
673
  if (outcome.authState === "unavailable") {
562
674
  if (!adapter) return false;
563
675
  try {
@@ -570,6 +682,9 @@ export default function usageExtension(
570
682
  return (
571
683
  generation === statusGeneration &&
572
684
  modelIdentity(ctx.model) === modelIdentity(model) &&
685
+ (!adapter.targets ||
686
+ settingsRuntime.get().settings.selectedTargets[adapter.id] ===
687
+ outcome.rememberedTargetId) &&
573
688
  auth === undefined
574
689
  );
575
690
  } catch (error) {
@@ -589,6 +704,9 @@ export default function usageExtension(
589
704
  return (
590
705
  generation === statusGeneration &&
591
706
  modelIdentity(ctx.model) === modelIdentity(model) &&
707
+ (!adapter.targets ||
708
+ settingsRuntime.get().settings.selectedTargets[adapter.id] ===
709
+ outcome.rememberedTargetId) &&
592
710
  auth?.fingerprint === outcome.fingerprint
593
711
  );
594
712
  } catch (error) {
@@ -654,6 +772,121 @@ export default function usageExtension(
654
772
  let redemptionId: string | undefined;
655
773
  let resetOutcome: CodexResetOutcome | undefined;
656
774
  let resetFailure: string | undefined;
775
+ const actionableTargetState = (): ProviderUsageState | undefined => {
776
+ if (visibleStates.length !== 1) return undefined;
777
+ const state = visibleStates[0];
778
+ return state &&
779
+ (state.status === "ready" || state.status === "selection-required") &&
780
+ adapterForProvider(state.providerId)?.targets
781
+ ? state
782
+ : undefined;
783
+ };
784
+ const promptForTarget = async (
785
+ state: ProviderUsageState,
786
+ stateFingerprint?: string,
787
+ ): Promise<boolean> => {
788
+ const adapter = adapterForProvider(state.providerId);
789
+ if (!adapter?.targets) return false;
790
+ const expectedRememberedTargetId =
791
+ settingsRuntime.get().settings.selectedTargets[adapter.id];
792
+ const snapshot =
793
+ state.status === "selection-required" && stateFingerprint
794
+ ? { choices: state.choices, fingerprint: stateFingerprint }
795
+ : await runMenuOperation(
796
+ ctx,
797
+ `Loading ${adapter.targets.pluralLabel}…`,
798
+ controller.signal,
799
+ (signal) => loadTargetChoices(ctx, adapter, signal),
800
+ );
801
+ if (!snapshot || controller.signal.aborted || statusGeneration !== menuGeneration) {
802
+ return false;
803
+ }
804
+ const selectOptions = createUsageTargetSelectOptions(snapshot.choices);
805
+ const selected = await ctx.ui.select(
806
+ `Select ${adapter.targets.singularLabel} for ${providerDisplayName(ctx, adapter.id)}`,
807
+ [...selectOptions.options],
808
+ { signal: controller.signal },
809
+ );
810
+ if (
811
+ selected === undefined ||
812
+ controller.signal.aborted ||
813
+ statusGeneration !== menuGeneration ||
814
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
815
+ ) {
816
+ return false;
817
+ }
818
+ const targetId = selectOptions.targetIdFor(selected);
819
+ if (!targetId) return false;
820
+ const revalidated = await runMenuOperation(
821
+ ctx,
822
+ `Revalidating ${adapter.targets.singularLabel}…`,
823
+ controller.signal,
824
+ (signal) => loadTargetChoices(ctx, adapter, signal),
825
+ );
826
+ if (
827
+ !revalidated ||
828
+ controller.signal.aborted ||
829
+ statusGeneration !== menuGeneration ||
830
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
831
+ ) {
832
+ return false;
833
+ }
834
+ if (
835
+ revalidated.fingerprint !== snapshot.fingerprint ||
836
+ !revalidated.choices.some((choice) => choice.id === targetId)
837
+ ) {
838
+ ctx.ui.notify(
839
+ `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
840
+ "warning",
841
+ );
842
+ return false;
843
+ }
844
+ let saved: Readonly<UsageSettingsState> | undefined;
845
+ try {
846
+ saved = await runMenuOperation(
847
+ ctx,
848
+ `Saving ${adapter.targets.singularLabel}…`,
849
+ controller.signal,
850
+ (signal) =>
851
+ settingsRuntime.updateSelectedTarget(adapter.id, targetId, signal, async () => {
852
+ let published: Awaited<ReturnType<typeof loadTargetChoices>>;
853
+ try {
854
+ published = await loadTargetChoices(ctx, adapter, signal);
855
+ } catch (error) {
856
+ if (
857
+ signal.aborted ||
858
+ controller.signal.aborted ||
859
+ statusGeneration !== menuGeneration ||
860
+ isStaleExtensionContextError(error)
861
+ ) {
862
+ throw error;
863
+ }
864
+ throw new UsageTargetSelectionChangedError();
865
+ }
866
+ if (
867
+ published.fingerprint !== snapshot.fingerprint ||
868
+ !published.choices.some((choice) => choice.id === targetId)
869
+ ) {
870
+ throw new UsageTargetSelectionChangedError();
871
+ }
872
+ }),
873
+ );
874
+ } catch (error) {
875
+ if (error instanceof UsageTargetSelectionChangedError) {
876
+ ctx.ui.notify(
877
+ `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
878
+ "warning",
879
+ );
880
+ return false;
881
+ }
882
+ throw error;
883
+ }
884
+ if (!saved || controller.signal.aborted || statusGeneration !== menuGeneration) {
885
+ return false;
886
+ }
887
+ invalidateProviderState(adapter.id);
888
+ return true;
889
+ };
657
890
  const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
658
891
  if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
659
892
  type Screen =
@@ -666,6 +899,7 @@ export default function usageExtension(
666
899
  type Action =
667
900
  | "refresh"
668
901
  | "settings"
902
+ | "target"
669
903
  | "toggle-fast"
670
904
  | "another"
671
905
  | "all"
@@ -681,6 +915,8 @@ export default function usageExtension(
681
915
  screens: {
682
916
  main: () => {
683
917
  const fastAvailability = fastRuntime.availability(ctx.model);
918
+ const targetState = actionableTargetState();
919
+ const targetAdapter = adapterForProvider(targetState?.providerId);
684
920
  const fastLines =
685
921
  fastAvailability.kind === "available"
686
922
  ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
@@ -694,6 +930,15 @@ export default function usageExtension(
694
930
  items: [
695
931
  { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
696
932
  { id: "settings", label: SETTINGS, action: "settings" },
933
+ ...(targetState && targetAdapter?.targets
934
+ ? [
935
+ {
936
+ id: "target",
937
+ label: `${targetState.status === "selection-required" ? "Select" : "Change"} ${targetAdapter.targets.singularLabel}…`,
938
+ action: "target" as const,
939
+ },
940
+ ]
941
+ : []),
697
942
  ...(fastAvailability.kind === "available"
698
943
  ? [
699
944
  {
@@ -735,7 +980,7 @@ export default function usageExtension(
735
980
  .filter((adapter) => adapter.id !== ctx.model?.provider)
736
981
  .map((adapter) => ({
737
982
  id: adapter.id,
738
- label: adapter.displayName,
983
+ label: providerDisplayName(ctx, adapter.id),
739
984
  action: "provider" as const,
740
985
  })),
741
986
  hint: "back",
@@ -799,6 +1044,61 @@ export default function usageExtension(
799
1044
  }),
800
1045
  },
801
1046
  actions: {
1047
+ target: async () => {
1048
+ const targetState = actionableTargetState();
1049
+ if (!targetState) return { kind: "rejected" };
1050
+ try {
1051
+ const stateFingerprint =
1052
+ targetState === current.state ? current.fingerprint : undefined;
1053
+ if (!(await promptForTarget(targetState, stateFingerprint))) {
1054
+ return { kind: "stay" };
1055
+ }
1056
+ const adapter = adapterForProvider(targetState.providerId);
1057
+ if (!adapter) return { kind: "rejected" };
1058
+ if (targetState.providerId === ctx.model?.provider) {
1059
+ const refreshed = await queryStableCurrent(
1060
+ ctx,
1061
+ true,
1062
+ controller,
1063
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1064
+ );
1065
+ if (!refreshed) return { kind: "stay" };
1066
+ stableCurrent = refreshed;
1067
+ current = refreshed.outcome;
1068
+ visibleStates = [current.state];
1069
+ publishStableCurrent(ctx, refreshed);
1070
+ return { kind: "stay" };
1071
+ }
1072
+ const outcome = await runMenuOperation(
1073
+ ctx,
1074
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1075
+ controller.signal,
1076
+ (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1077
+ );
1078
+ if (!outcome) return { kind: "stay" };
1079
+ const revalidated = await queryStableCurrent(
1080
+ ctx,
1081
+ false,
1082
+ controller,
1083
+ "Revalidating current usage…",
1084
+ );
1085
+ if (!revalidated) return { kind: "stay" };
1086
+ stableCurrent = revalidated;
1087
+ current = revalidated.outcome;
1088
+ visibleStates = [
1089
+ outcome.state.providerId === current.state.providerId
1090
+ ? current.state
1091
+ : { ...outcome.state, displayState: "configured" },
1092
+ ];
1093
+ return { kind: "stay" };
1094
+ } catch (error) {
1095
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
1096
+ return { kind: "stay" };
1097
+ }
1098
+ ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
1099
+ return { kind: "stay" };
1100
+ }
1101
+ },
802
1102
  settings: async () => {
803
1103
  await showUsageSettings(
804
1104
  ctx,
@@ -1023,13 +1323,25 @@ export default function usageExtension(
1023
1323
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
1024
1324
  );
1025
1325
  if (!adapter) return { kind: "back" };
1026
- const outcome = await runMenuOperation(
1326
+ let outcome = await runMenuOperation(
1027
1327
  ctx,
1028
- `Checking ${adapter.displayName} usage…`,
1328
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1029
1329
  controller.signal,
1030
1330
  (signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
1031
1331
  );
1032
1332
  if (!outcome) return { kind: "back" };
1333
+ if (outcome.state.status === "selection-required") {
1334
+ if (!(await promptForTarget(outcome.state, outcome.fingerprint))) {
1335
+ return { kind: "back" };
1336
+ }
1337
+ outcome = await runMenuOperation(
1338
+ ctx,
1339
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1340
+ controller.signal,
1341
+ (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1342
+ );
1343
+ if (!outcome) return { kind: "back" };
1344
+ }
1033
1345
  const revalidated = await queryStableCurrent(
1034
1346
  ctx,
1035
1347
  false,
@@ -1076,7 +1388,7 @@ export default function usageExtension(
1076
1388
  const adapter = adapters[index] as UsageProviderAdapter;
1077
1389
  return {
1078
1390
  providerId: adapter.id,
1079
- providerName: adapter.displayName,
1391
+ providerName: providerDisplayName(ctx, adapter.id),
1080
1392
  displayState: "configured",
1081
1393
  status: "query-failed",
1082
1394
  message: errorMessage(result.reason),