Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4cba149
feat(usage): read cursor, opencode, and antigravity history
maria-rcks Sep 6, 2026
2574209
fix(usage): preserve bubble totals and merge antigravity aliases
maria-rcks Sep 6, 2026
6e632a4
fix(usage): read headless cursor account history
maria-rcks Sep 6, 2026
0d1357f
fix(usage): reconcile cursor pagination and remove overlapping fallback
maria-rcks Sep 6, 2026
d26a54e
Merge remote-tracking branch 'origin/main' into t3code/add-harness-us…
Yash-Singh1 Sep 25, 2026
aa20d44
fix(usage): keep OpenCode parser internal
Yash-Singh1 Sep 25, 2026
bb67896
fix(usage): honor active Cursor credential store
Yash-Singh1 Sep 25, 2026
9e1ed75
chore(usage): explain Node reader imports
Yash-Singh1 Sep 25, 2026
f9c6a28
fix(usage): use shared Crypto service for profile lookup
Yash-Singh1 Sep 25, 2026
393a8f3
fix(usage): handle paginated and aliased history
Yash-Singh1 Sep 25, 2026
62a1eac
fix(usage): keep large and partial histories accurate
Yash-Singh1 Sep 25, 2026
ced8da6
fix(usage): retain new cells from partial scans
Yash-Singh1 Sep 25, 2026
25426df
fix(usage): reconcile overlapping partial cells
Yash-Singh1 Sep 25, 2026
e06cad6
fix(usage): preserve complete overlapping cells
Yash-Singh1 Sep 25, 2026
d49bba0
feat(usage): add opt-in Cursor Keychain access
Yash-Singh1 Sep 25, 2026
d99c43b
Merge remote-tracking branch 'origin/main' into t3code/add-harness-us…
Yash-Singh1 Sep 25, 2026
73854af
fix(usage): keep Cursor Keychain reads responsive
Yash-Singh1 Sep 25, 2026
f91fa81
fix(usage): require HTTPS for Cursor Keychain limits
Yash-Singh1 Sep 25, 2026
b5e2544
fix(usage): keep Cursor Keychain token on its API origin
Yash-Singh1 Sep 25, 2026
40854e1
fix(usage): show Cursor enable action in provider list
Yash-Singh1 Sep 25, 2026
4e5a7bb
Merge remote-tracking branch 'origin/main' into t3code/add-harness-us…
Yash-Singh1 Sep 25, 2026
c06aa01
fix(usage): align Cursor opt-in with provider rows
Yash-Singh1 Sep 25, 2026
58123ef
Merge remote-tracking branch 'origin/main' into t3code/add-harness-us…
Yash-Singh1 Sep 25, 2026
0979931
fix(usage): mark new provider records as standard speed
Yash-Singh1 Sep 25, 2026
6135e1a
fix(usage): refresh Cursor limits after opt-in
Yash-Singh1 Sep 25, 2026
23d8b0e
fix(usage): match Antigravity step models by generation
Yash-Singh1 Sep 25, 2026
0bd2f00
fix(usage): ignore unused large Antigravity varints
Yash-Singh1 Sep 25, 2026
a60e3bc
fix(usage): explain Cursor monthly allowances
Yash-Singh1 Sep 25, 2026
fe90238
fix(usage): keep Cursor window ranking private
Yash-Singh1 Sep 25, 2026
f24f90f
fix(usage): focus Cursor limits on usable pools
Yash-Singh1 Sep 25, 2026
3f510e0
fix(usage): simplify single-account limit bars
Yash-Singh1 Sep 25, 2026
003f3c8
fix(usage): show tiny positive shares as less than 0.1 percent
Yash-Singh1 Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/mobile/src/features/usage/UsageRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ export function UsageRouteScreen() {
selectedEnvironmentIds,
);
const limits = useRefreshLimits(selectedEnvironmentIds);
const sourceMessages = [
...new Set(
selectedEnvironments.flatMap(
(environment) =>
environment.summary?.sources.flatMap((source) =>
source.message &&
(source.status === "partial" ||
source.status === "failed" ||
source.fingerprint.provider === "cursor")
? [source.message]
: [],
) ?? [],
),
),
];

const days = useMemo(
() => enumerateDays(window.sinceDay, window.untilDay),
Expand Down Expand Up @@ -295,6 +310,11 @@ export function UsageRouteScreen() {
</Text>
) : (
<>
{sourceMessages.map((message) => (
<Text key={message} className="text-sm text-foreground-muted">
{message}
</Text>
))}
<ChartCard
merged={merged}
days={chartDays}
Expand Down
15 changes: 14 additions & 1 deletion apps/mobile/src/features/usage/usageProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import { useAppearancePreferences } from "../settings/appearance/AppearancePrefe
* Series and table order. The chart stacks providers from the bottom in this
* order, so it also fixes which band sits on top of the bars.
*/
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude", "grok"];
export const PROVIDER_ORDER: readonly UsageProviderKind[] = [
"codex",
"claude",
"grok",
"cursor",
"opencode",
"antigravity",
];

export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
grok: "Grok Build",
cursor: "Cursor",
opencode: "OpenCode",
antigravity: "Antigravity",
};

/**
Expand All @@ -23,5 +33,8 @@ export function useProviderColors(): Record<UsageProviderKind, string> {
claude: "#d97757",
codex: scheme === "dark" ? "#e6e6e6" : "#3c3c43",
grok: scheme === "dark" ? "#a1a1aa" : "#52525b",
cursor: "#8b8b8b",
opencode: "#5b9bbd",
antigravity: "#8c7bd1",
};
}
76 changes: 74 additions & 2 deletions apps/server/src/usage/UsageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeSqlite from "node:sqlite";

import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { UsageDay, type UsageSummaryInput } from "@t3tools/contracts";
import * as Duration from "effect/Duration";
import * as Deferred from "effect/Deferred";
Expand All @@ -16,13 +17,16 @@ import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Scheduler from "effect/Scheduler";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";

import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as UsageService from "./UsageService.ts";

const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));

function claudeLine(id: number, outputTokens: number, model = "claude-fable-5"): string {
return `${JSON.stringify({
type: "assistant",
Expand Down Expand Up @@ -74,6 +78,7 @@ const serviceLayers = (input: {
}) =>
ServerConfig.layerTest(process.cwd(), { prefix: input.prefix }).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(Layer.succeed(HostProcessPlatform, "linux")),
Layer.provideMerge(ServerSettings.layerTest(input.settings)),
Layer.provideMerge(
Layer.succeed(
Expand All @@ -89,7 +94,13 @@ const serviceLayers = (input: {
),
),
Layer.provideMerge(
Layer.succeed(HostProcessEnvironment, { GROK_HOME: NodePath.join(input.home, "grok") }),
Layer.succeed(HostProcessEnvironment, {
GROK_HOME: NodePath.join(input.home, "grok"),
OPENCODE_DATA_DIR: NodePath.join(input.home, "opencode"),
ANTIGRAVITY_DATA_DIR: NodePath.join(input.home, "antigravity"),
XDG_CONFIG_HOME: NodePath.join(input.home, "config"),
APPDATA: NodePath.join(input.home, "config"),
}),
),
);

Expand All @@ -98,6 +109,67 @@ function totalOutputTokens(summary: { buckets: readonly { totals: { outputTokens
}

describe("UsageService", () => {
it.live(
"includes OpenCode history but does not substitute desktop usage for an unavailable Cursor account",
() =>
Effect.gen(function* () {
const { settings, home } = yield* setup;
const root = NodePath.join(home, "opencode");
const message = yield* encodeUnknownJson({
id: "msg_1",
sessionID: "session-1",
role: "assistant",
modelID: "example-model",
time: { created: Date.parse("2026-08-01T10:00:00Z") },
tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 20, write: 3 } },
});
const bubble = yield* encodeUnknownJson({
type: 2,
createdAt: "2026-08-01T10:00:00Z",
modelInfo: { modelName: "example-model" },
tokenCount: { inputTokens: 100, outputTokens: 20 },
});
yield* Effect.promise(async () => {
const directory = NodePath.join(root, "storage", "message", "session-1");
await NodeFSP.mkdir(directory, { recursive: true });
await NodeFSP.writeFile(NodePath.join(directory, "msg_1.json"), message);
const desktop = NodePath.join(home, "config", "Cursor", "User", "globalStorage");
await NodeFSP.mkdir(desktop, { recursive: true });
const db = new NodeSqlite.DatabaseSync(NodePath.join(desktop, "state.vscdb"));
try {
db.exec("CREATE TABLE cursorDiskKV (key TEXT, value TEXT)");
db.prepare("INSERT INTO cursorDiskKV VALUES (?, ?)").run(
"bubbleId:session:assistant",
bubble,
);
} finally {
db.close();
}
});
const service = yield* UsageService.make.pipe(
Effect.provide(serviceLayers({ prefix: "usage-service-opencode", home, settings })),
);
const summary = yield* service.readSummary(WINDOW);
assert.strictEqual(summary.buckets[0]?.provider, "opencode");
assert.isFalse(summary.buckets.some((bucket) => bucket.provider === "cursor"));
assert.strictEqual(
summary.sources.find((source) => source.fingerprint.provider === "cursor")?.status,
"missing",
);
assert.strictEqual(summary.buckets[0]?.sourcePath, root);
assert.strictEqual(summary.buckets[0]?.totals.outputTokens, 7);
assert.strictEqual(
summary.sources.find((source) => source.fingerprint.provider === "opencode")
?.distinctSessions,
1,
);
assert.include(
summary.sources.find((source) => source.fingerprint.provider === "cursor")?.message ?? "",
"Cursor account history needs a Cursor CLI login",
);
}).pipe(Effect.scoped),
);

it.live("reprices unchanged transcripts when custom prices are added, edited, or removed", () =>
Effect.gen(function* () {
const { transcript, settings, home } = yield* setup;
Expand Down
Loading
Loading