Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion apps/mobile/src/features/threads/ComposerUsageLimits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { AccountLimits, ResetCredits } from "../usage/UsageLimitsSection";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const DRIVER_LABEL: Partial<Record<string, string>> = {
codex: "Codex",
claudeAgent: "Claude",
copilot: "GitHub Copilot",
antigravity: "Antigravity",
};

/**
* The /usage-limits result, docked above the composer. It is the Usage → Limits
Expand Down
17 changes: 15 additions & 2 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { environmentPresentations } from "../../state/presentation";
import { ResetCredits } from "./UsageLimitsSection";
import { useProviderColors } from "./usageProviders";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const DRIVER_LABEL: Partial<Record<string, string>> = {
codex: "Codex",
claudeAgent: "Claude",
copilot: "GitHub Copilot",
antigravity: "Antigravity",
};
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;

function accountName(account: LimitAccount) {
Expand Down Expand Up @@ -236,7 +241,15 @@ export function UsageLimitsSection({
<PoolWindowCard
key={`${window.kind}:${window.id}`}
pool={window}
color={pool.driver === "claudeAgent" ? colors.claude : colors.codex}
color={
pool.driver === "claudeAgent"
? colors.claude
: pool.driver === "copilot"
? colors.copilot
: pool.driver === "antigravity"
? colors.antigravity
: colors.codex
}
Comment on lines +244 to +252

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be a switch statement in a shared helper. not a nested ternary.

now={now}
environmentIds={selectedEnvironmentIds === null ? null : [...selectedEnvironmentIds]}
/>
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/src/features/usage/UsageLimitsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ type Driver = ServerProvider["driver"];
function useBarColor(driver: Driver): string | null {
const colors = useProviderColors();
const kind: UsageProviderKind | null =
driver === "codex" ? "codex" : driver === "claudeAgent" ? "claude" : null;
driver === "codex"
? "codex"
: driver === "claudeAgent"
? "claude"
: driver === "copilot"
? "copilot"
: driver === "antigravity"
? "antigravity"
: null;
Comment on lines +37 to +45

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also use the shared helper as described in the usage limits pooled file.

return kind ? colors[kind] : null;
}

Expand Down
12 changes: 11 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,20 @@ 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",
"copilot",
"grok",
"antigravity",
];

export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
copilot: "GitHub Copilot",
grok: "Grok Build",
antigravity: "Antigravity",
};

/**
Expand All @@ -22,6 +30,8 @@ export function useProviderColors(): Record<UsageProviderKind, string> {
return {
claude: "#d97757",
codex: scheme === "dark" ? "#e6e6e6" : "#3c3c43",
copilot: scheme === "dark" ? "#58a6ff" : "#1f6feb",
grok: scheme === "dark" ? "#a1a1aa" : "#52525b",
antigravity: scheme === "dark" ? "#8ab4f8" : "#1a73e8",
};
}
17 changes: 17 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,27 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
.pipe(Effect.provideService(Scope.Scope, processScope));
}).pipe(Effect.scoped);

const checkAuth = Effect.gen(function* () {
const tokenPath = path.join(profileDirectory, "antigravity-acp", "acp_token.json");
const exists = yield* fileSystem.exists(tokenPath).pipe(Effect.orElseSucceed(() => false));
if (exists) {
return {
status: "authenticated" as const,
type: auth.authMethod,
label: antigravityAuthLabel(auth.authMethod),
};
}
return {
status: "unknown" as const,
type: auth.authMethod,
};
});

const provider = yield* makeAntigravityProvider(settings, {
stampIdentity: classifyModels,
probe,
auth: { type: auth.authMethod, label: antigravityAuthLabel(auth.authMethod) },
checkAuth,
supportsTextGeneration: isAntigravityTextGenerationAvailable(profileDirectory).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Expand Down
154 changes: 154 additions & 0 deletions apps/server/src/provider/Drivers/CopilotDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { CopilotSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeCopilotTextGeneration } from "../../textGeneration/CopilotTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeCopilotAdapter } from "../Layers/CopilotAdapter.ts";
import {
buildInitialCopilotProviderSnapshot,
checkCopilotProviderStatus,
enrichCopilotSnapshot,
} from "../Layers/CopilotProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";

const decodeCopilotSettings = Schema.decodeSync(CopilotSettings);

const DRIVER_KIND = ProviderDriverKind.make("copilot");
const MAINTENANCE_CAPABILITIES = makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
});

export type CopilotDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const CopilotDriver: ProviderDriver<CopilotSettings, CopilotDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "GitHub Copilot",
supportsMultipleInstances: true,
},
configSchema: CopilotSettings,
defaultConfig: (): CopilotSettings => decodeCopilotSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies CopilotSettings;

const adapter = yield* makeCopilotAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeCopilotTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkCopilotProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CopilotSettings>>({
resolveMaintenance: () => Effect.succeed(MAINTENANCE_CAPABILITIES),
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialCopilotProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichCopilotSnapshot({
snapshot: currentSnapshot,
maintenanceCapabilities: MAINTENANCE_CAPABILITIES,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
httpClient,
}),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build GitHub Copilot snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
15 changes: 12 additions & 3 deletions apps/server/src/provider/Layers/AntigravityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ interface AntigravityProviderOptions {
readonly maintenanceCapabilities?: ProviderMaintenanceCapabilities;
/** Auth type and label published once a session authenticates. */
readonly auth?: { readonly type: string; readonly label: string };
readonly checkAuth?: Effect.Effect<ServerProvider["auth"]>;
}

/** Health uses initialize only. Session callbacks supply account-specific metadata. */
Expand Down Expand Up @@ -192,22 +193,30 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
: `Antigravity did not respond to its local health check within ${HEALTH_CHECK_TIMEOUT}.`;
const supportsTextGeneration =
initialized !== undefined ? yield* options.supportsTextGeneration : false;
const detectedAuth = options.checkAuth
? yield* options.checkAuth.pipe(Effect.orElseSucceed(() => undefined))
: undefined;
const updatedAt = DateTime.formatIso(yield* DateTime.now);
const next = yield* SubscriptionRef.updateAndGet(metadata, (state) => {
if (state.authRevision !== before.authRevision) return state;
const { message: _previousMessage, ...draft } = state.draft;
const authenticated = draft.auth.status === "authenticated";
const nextAuth =
draft.auth.status !== "authenticated" && detectedAuth?.status === "authenticated"
? detectedAuth
: draft.auth;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const authenticated = nextAuth.status === "authenticated";
const message =
errorMessage ??
(authenticated
? undefined
: draft.auth.status === "unauthenticated"
: nextAuth.status === "unauthenticated"
? SIGN_IN_MESSAGE
: AUTH_UNCHECKED_MESSAGE);
return {
...state,
draft: {
...draft,
auth: nextAuth,
installed: !missingInstallation,
version: initialized?.agentInfo?.version || draft.version,
status: errorMessage ? "error" : authenticated ? "ready" : "warning",
Expand All @@ -224,7 +233,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
...(initialized !== undefined
? {
supportsTextGeneration:
supportsTextGeneration && draft.auth.status !== "unauthenticated",
supportsTextGeneration && nextAuth.status !== "unauthenticated",
}
: {}),
...(message ? { message } : {}),
Expand Down
55 changes: 55 additions & 0 deletions apps/server/src/provider/Layers/CopilotAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { TurnId } from "@t3tools/contracts";
import { assert, it } from "@effect/vitest";

import { copilotPromptSettlementBelongsToContext } from "./CopilotAdapter.ts";

it("requires a settlement to match the live Copilot turn", () => {
const staleTurnId = TurnId.make("stale-turn");
const replacementTurnId = TurnId.make("replacement-turn");

assert.isFalse(
copilotPromptSettlementBelongsToContext({
liveAcpSessionId: "session-1",
expectedAcpSessionId: "session-1",
liveActiveTurnId: replacementTurnId,
liveSessionActiveTurnId: replacementTurnId,
turnId: staleTurnId,
}),
);
assert.isFalse(
copilotPromptSettlementBelongsToContext({
liveAcpSessionId: "replacement-session",
expectedAcpSessionId: "stale-session",
liveActiveTurnId: staleTurnId,
liveSessionActiveTurnId: staleTurnId,
turnId: staleTurnId,
}),
);
assert.isTrue(
copilotPromptSettlementBelongsToContext({
liveAcpSessionId: "session-1",
expectedAcpSessionId: "session-1",
liveActiveTurnId: staleTurnId,
liveSessionActiveTurnId: staleTurnId,
turnId: staleTurnId,
}),
);
assert.isTrue(
copilotPromptSettlementBelongsToContext({
liveAcpSessionId: "session-1",
expectedAcpSessionId: "session-1",
liveActiveTurnId: undefined,
liveSessionActiveTurnId: staleTurnId,
turnId: staleTurnId,
}),
);
assert.isTrue(
copilotPromptSettlementBelongsToContext({
liveAcpSessionId: "session-1",
expectedAcpSessionId: "session-1",
liveActiveTurnId: staleTurnId,
liveSessionActiveTurnId: undefined,
turnId: staleTurnId,
}),
);
});
Loading
Loading