Skip to content
Draft
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
10 changes: 9 additions & 1 deletion apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
Expand All @@ -22,6 +23,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import * as PtyAdapter from "../../terminal/PtyAdapter.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
Expand Down Expand Up @@ -88,6 +90,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const { cwd } = yield* ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const httpClient = yield* HttpClient.HttpClient;
Expand Down Expand Up @@ -118,12 +121,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
});
const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).pipe(
// The usage probe drives the CLI's TUI, so it needs a PTY. The adapter
// is optional here: without it the probe reports limits unavailable.
const ptyAdapter = Option.getOrUndefined(yield* Effect.serviceOption(PtyAdapter.PtyAdapter));
const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv, cwd).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
(effect) =>
ptyAdapter ? Effect.provideService(effect, PtyAdapter.PtyAdapter, ptyAdapter) : effect,
Comment on lines +126 to +134

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This hand-rolls optional dependency plumbing around the layer boundary: PtyAdapter is not part of CursorDriverEnv, so the type system never requires it, and the ternary provideService is either redundant (a provided adapter is already visible to the probe's serviceOption through the inherited fiber context) or a silent no-op (nothing provides PtyAdapter to the driver layer today, so the probe always reports limits unavailable).

Consider acquiring it as a normal dependency — const ptyAdapter = yield* PtyAdapter.PtyAdapter; plus an unconditional Effect.provideService(PtyAdapter.PtyAdapter, ptyAdapter) — and adding PtyAdapter.PtyAdapter to the CursorDriverEnv union so the missing wiring becomes a type error instead of a runtime degradation.

Posted via Macroscope — Effect Service Conventions

);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
Expand All @@ -9,6 +10,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import * as PtyAdapter from "../../terminal/PtyAdapter.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
Expand Down Expand Up @@ -100,10 +102,15 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
});
const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);

// The usage probe drives the CLI's TUI, so it needs a PTY. The adapter
// is optional here: without it the probe reports limits unavailable.
const ptyAdapter = Option.getOrUndefined(yield* Effect.serviceOption(PtyAdapter.PtyAdapter));
const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
(effect) =>
ptyAdapter ? Effect.provideService(effect, PtyAdapter.PtyAdapter, ptyAdapter) : effect,
Comment on lines +107 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same conditional-provide bypass as in CursorDriver.ts: PtyAdapter is absent from GrokDriverEnv, so the ternary silently skips providing it and the usage probe degrades to probeFailed with no compile-time signal. Consider const ptyAdapter = yield* PtyAdapter.PtyAdapter;, an unconditional Effect.provideService(...), and adding PtyAdapter.PtyAdapter to GrokDriverEnv.

Posted via Macroscope — Effect Service Conventions

);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ServerProviderAuth,
ServerProviderModel,
ServerProviderState,
ServerProviderUsageLimits,
} from "@t3tools/contracts";
import type * as EffectAcpSchema from "effect-acp/schema";
import { causeErrorTag } from "@t3tools/shared/observability";
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
type ProviderMaintenanceCapabilities,
} from "../providerMaintenance.ts";
import * as AcpSessionRuntime from "../acp/AcpSessionRuntime.ts";
import { probeCursorUsageLimits } from "../cursorUsageProbe.ts";
import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts";

const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect(
Expand Down Expand Up @@ -628,6 +630,7 @@ export function buildCursorProviderSnapshot(input: {
readonly parsed: CursorAboutResult;
readonly discoveredModels?: ReadonlyArray<ServerProviderModel>;
readonly discoveryWarning?: string;
readonly usageLimits?: ServerProviderUsageLimits;
}): ServerProviderDraft {
const message = joinProviderMessages(input.parsed.message, input.discoveryWarning);
return buildServerProvider({
Expand All @@ -645,6 +648,7 @@ export function buildCursorProviderSnapshot(input: {
status:
input.discoveryWarning && input.parsed.status === "ready" ? "warning" : input.parsed.status,
auth: input.parsed.auth,
...(input.usageLimits ? { usageLimits: input.usageLimits } : {}),
...(message ? { message } : {}),
},
});
Expand Down Expand Up @@ -987,6 +991,7 @@ const runCursorAboutCommand = (cursorSettings: CursorSettings, environment?: Nod
export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(function* (
cursorSettings: CursorSettings,
environment?: NodeJS.ProcessEnv,
cwd = process.cwd(),
): Effect.fn.Return<
ServerProviderDraft,
never,
Expand Down Expand Up @@ -1101,6 +1106,19 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
discoveredModels = discoveryExit.value;
}
}
// The `/usage` panel is the only place Cursor reports its plan quota, so
// this drives the TUI in a PTY. An unauthenticated account has no panel.
const usageLimits =
parsed.auth.status === "unauthenticated"
? undefined
: yield* probeCursorUsageLimits({
binaryPath: cursorSettings.binaryPath,
...(cursorSettings.apiEndpoint ? { apiEndpoint: cursorSettings.apiEndpoint } : {}),
cwd,
checkedAt,
...(environment ? { environment } : {}),
}).pipe(Effect.map((result) => result.usageLimits));

return buildCursorProviderSnapshot({
checkedAt,
cursorSettings,
Expand All @@ -1110,6 +1128,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
() => [] as const,
),
...(discoveryWarning ? { discoveryWarning } : {}),
...(usageLimits ? { usageLimits } : {}),
});
});

Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
} from "../acp/GrokAcpSupport.ts";
import { sessionModelStateFromInitialize } from "../acp/AcpRuntimeModel.ts";
import { discoverGrokSkills } from "../Drivers/GrokSkills.ts";
import { probeGrokUsageLimits } from "../grokTuiUsageProbe.ts";
import { makeUnavailableUsageLimits } from "../providerUsageLimits.ts";

const GROK_PRESENTATION = {
displayName: "Grok",
Expand Down Expand Up @@ -493,6 +495,18 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func
});
}

// The weekly limit only exists in the TUI's `/usage` panel, and only on a
// signed-in account; an API-key session has no plan quota to report.
const usageLimits =
auth.type === "api_key"
? makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" })
: yield* probeGrokUsageLimits({
binaryPath: grokSettings.binaryPath || "grok",
cwd: cwd ?? process.cwd(),
checkedAt,
environment,
}).pipe(Effect.map((result) => result.usageLimits));

return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
Expand All @@ -505,6 +519,7 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func
// A failed metadata probe degrades the model picker, it does not make chats fail.
status: acpFailed ? "warning" : "ready",
auth,
usageLimits,
...(acpFailed
? {
message:
Expand Down
Loading
Loading