authorgravatar for 75276669+Gigioxx@users.noreply.github.comGuillermo Casanova <75276669+Gigioxx@users.noreply.github.com> 2026-09-26 00:41:48-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-25 22:41:48-05:00
log94c42162b443072ca59a0733c02ee54229a61fef
tree7543a044f0a4f8c9a318ced4d5cbc63154c58426
parent999161ef844a7a38c8a6d08195c1240759169832
signature Signed by PGP key B5690EEEBB952194

fix(usage): hide the Cursor keychain prompt when Cursor isn't set up (#13714)


7 files changed, 88 insertions(+), 10 deletions(-)

apps/mobile/src/features/usage/UsageRouteScreen.tsx+2-2
......@@ -101,8 +101,8 @@ export function UsageRouteScreen() {
101101 );
102102 const isFocused = useIsFocused();
103103 const limits = useRefreshLimits(selectedEnvironmentIds, isFocused && tab === "limits");
104 const cursorAccessEnvironments = selectedEnvironments.filter((environment) =>
105 environment.summary?.sources.some((source) => source.action === "enableCursorKeychain"),
104 const cursorAccessEnvironments = selectedEnvironments.filter(
105 (environment) => environment.needsCursorKeychainAccess,
106106 );
107107 const refreshAfterCursorEnable = () => {
108108 void refresh();
apps/mobile/src/state/usage.ts+8-2
......@@ -16,7 +16,7 @@ import {
1616 type UsageSummary,
1717 type UsageSummaryInput,
1818} from "@t3tools/contracts";
19import { refreshUsage } from "@t3tools/client-runtime/state/usage";
19import { needsCursorKeychainAccess, refreshUsage } from "@t3tools/client-runtime/state/usage";
2020import { mergeUsage, type EnvironmentUsage, type MergedUsage } from "@t3tools/shared/usageMerge";
2121import * as Option from "effect/Option";
2222import { AsyncResult, Atom } from "effect/unstable/reactivity";
......@@ -33,6 +33,7 @@ export interface EnvironmentUsageStatus {
3333 readonly isConnected: boolean;
3434 readonly error: string | null;
3535 readonly summary: UsageSummary | null;
36 readonly needsCursorKeychainAccess: boolean;
3637}
3738
3839/**
......@@ -50,13 +51,18 @@ const usageByWindowAtom = Atom.family((windowKey: string) =>
5051 const statuses: EnvironmentUsageStatus[] = [];
5152 for (const [environmentId, presentation] of presentations) {
5253 const result = get(serverEnvironment.usageSummary({ environmentId, input }));
54 const summary = Option.getOrNull(AsyncResult.value(result));
5355 statuses.push({
5456 environmentId,
5557 label: presentation.entry.target.label,
5658 isPending: result.waiting,
5759 isConnected: presentation.connection.phase === "connected",
5860 error: result._tag === "Failure" ? "This environment could not report usage." : null,
59 summary: Option.getOrNull(AsyncResult.value(result)),
61 summary,
62 needsCursorKeychainAccess: needsCursorKeychainAccess(
63 summary,
64 get(serverEnvironment.providersValueAtom(environmentId)),
65 ),
6066 });
6167 }
6268 return statuses;
apps/web/src/components/usage/UsagePage.tsx+2-2
......@@ -121,8 +121,8 @@ export function UsagePage() {
121121 selectedEnvironmentIds,
122122 );
123123 const presentations = useAtomValue(environmentPresentations.presentationsAtom);
124 const cursorAccessEnvironments = selectedEnvironments.filter((environment) =>
125 environment.summary?.sources.some((source) => source.action === "enableCursorKeychain"),
124 const cursorAccessEnvironments = selectedEnvironments.filter(
125 (environment) => environment.needsCursorKeychainAccess,
126126 );
127127 const sourceMessages = [
128128 ...new Set(
apps/web/src/state/usage.test.tsx+1
......@@ -23,6 +23,7 @@ function environment(id: string, cost: number | null, hostId = id): EnvironmentU
2323 label: id,
2424 isPending: cost === null,
2525 error: null,
26 needsCursorKeychainAccess: false,
2627 summary:
2728 cost === null
2829 ? null
apps/web/src/state/usage.ts+8-2
......@@ -13,7 +13,7 @@ import {
1313 type UsageSummary,
1414 type UsageSummaryInput,
1515} from "@t3tools/contracts";
16import { refreshUsage } from "@t3tools/client-runtime/state/usage";
16import { needsCursorKeychainAccess, refreshUsage } from "@t3tools/client-runtime/state/usage";
1717import * as Option from "effect/Option";
1818import { AsyncResult, Atom } from "effect/unstable/reactivity";
1919import { useCallback, useMemo } from "react";
......@@ -29,6 +29,7 @@ export interface EnvironmentUsageStatus {
2929 readonly isPending: boolean;
3030 readonly error: string | null;
3131 readonly summary: UsageSummary | null;
32 readonly needsCursorKeychainAccess: boolean;
3233}
3334
3435/**
......@@ -46,12 +47,17 @@ const usageByWindowAtom = Atom.family((windowKey: string) =>
4647 const statuses: EnvironmentUsageStatus[] = [];
4748 for (const [environmentId, presentation] of presentations) {
4849 const result = get(serverEnvironment.usageSummary({ environmentId, input }));
50 const summary = Option.getOrNull(AsyncResult.value(result));
4951 statuses.push({
5052 environmentId,
5153 label: presentation.entry.target.label,
5254 isPending: result.waiting,
5355 error: result._tag === "Failure" ? "This environment could not report usage." : null,
54 summary: Option.getOrNull(AsyncResult.value(result)),
56 summary,
57 needsCursorKeychainAccess: needsCursorKeychainAccess(
58 summary,
59 get(serverEnvironment.providersValueAtom(environmentId)),
60 ),
5561 });
5662 }
5763 return statuses;
packages/client-runtime/src/state/usage.test.ts+49-1
......@@ -1,7 +1,10 @@
11import {
22 EnvironmentId,
3 ProviderDriverKind,
4 ProviderInstanceId,
35 UsageDay,
46 USAGE_CONTRACT_VERSION,
7 type ServerProvider,
58 type UsageSummary,
69} from "@t3tools/contracts";
710import * as Effect from "effect/Effect";
......@@ -10,7 +13,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test";
1013
1114import type { EnvironmentPresentation } from "../connection/presentation.ts";
1215import { EnvironmentRpcUnavailableError } from "../rpc/client.ts";
13import { refreshUsage, refreshUsageLimits } from "./usage.ts";
16import { needsCursorKeychainAccess, refreshUsage, refreshUsageLimits } from "./usage.ts";
1417
1518const input = {
1619 sinceDay: UsageDay.make("2026-09-05"),
......@@ -238,3 +241,48 @@ describe("limits refresh cooldown", () => {
238241 }
239242 });
240243});
244
245describe("needsCursorKeychainAccess", () => {
246 const cursorPrompt: UsageSummary = {
247 ...summary,
248 sources: [
249 {
250 fingerprint: {
251 hostId: "host",
252 provider: "cursor",
253 resolvedHomePath: "/Users/me/.cursor/auth.json",
254 volumeId: "volume",
255 },
256 status: "ok",
257 scannedFiles: 0,
258 skippedFiles: 0,
259 malformedRecords: 0,
260 distinctSessions: 0,
261 message: "Cursor account usage is off on this environment.",
262 action: "enableCursorKeychain",
263 },
264 ],
265 };
266 const cursor = (status: ServerProvider["status"]): ServerProvider => ({
267 instanceId: ProviderInstanceId.make("cursor"),
268 driver: ProviderDriverKind.make("cursor"),
269 enabled: status !== "disabled",
270 installed: status === "ready",
271 version: null,
272 status,
273 auth: { status: "unknown" },
274 checkedAt: "2026-09-05T12:00:00.000Z",
275 models: [],
276 slashCommands: [],
277 skills: [],
278 });
279
280 it("offers access only when Cursor is ready on that environment", () => {
281 expect(needsCursorKeychainAccess(cursorPrompt, [cursor("ready")])).toBe(true);
282 expect(needsCursorKeychainAccess(cursorPrompt, [cursor("error")])).toBe(false);
283 expect(needsCursorKeychainAccess(cursorPrompt, [cursor("disabled")])).toBe(false);
284 expect(needsCursorKeychainAccess(cursorPrompt, [])).toBe(false);
285 expect(needsCursorKeychainAccess(cursorPrompt, null)).toBe(false);
286 expect(needsCursorKeychainAccess(summary, [cursor("ready")])).toBe(false);
287 });
288});
packages/client-runtime/src/state/usage.ts+18-1
......@@ -1,4 +1,9 @@
1import type { EnvironmentId, UsageSummaryInput } from "@t3tools/contracts";
1import type {
2 EnvironmentId,
3 ServerProvider,
4 UsageSummary,
5 UsageSummaryInput,
6} from "@t3tools/contracts";
27import * as Schema from "effect/Schema";
38import type { AtomRegistry } from "effect/unstable/reactivity";
49
......@@ -9,6 +14,18 @@ import type { createServerEnvironmentAtoms } from "./server.ts";
914
1015const isEnvironmentRpcUnavailable = Schema.is(EnvironmentRpcUnavailableError);
1116
17/** Offer the Cursor Keychain prompt only where a working Cursor provider could use it. */
18export function needsCursorKeychainAccess(
19 summary: UsageSummary | null,
20 providers: readonly ServerProvider[] | null,
21): boolean {
22 return (
23 summary?.sources.some((source) => source.action === "enableCursorKeychain") === true &&
24 providers?.some((provider) => provider.driver === "cursor" && provider.status === "ready") ===
25 true
26 );
27}
28
1229const limitsRefreshAfter = new Map<EnvironmentId, number>();
1330const limitsRefreshes = new Map<EnvironmentId, Promise<unknown>>();
1431