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
28 changes: 4 additions & 24 deletions gui/src/pages/integrations/CursorIntegrationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import { formatTokens } from "../../format-tokens";
import { navigateHash } from "../../hash-routing";
import { useI18n, useT, type TKey } from "../../i18n/shared";
import { Notice } from "../../ui";
import { formatRelativeTime, relativeTimeLabelsFromT } from "../../provider-workspace/usage";
import { CURSOR_SEEN_WINDOW_MS, loadCursorIntegrationStatus, type CursorIntegrationStatus } from "./cursor-api";
import { loadCursorIntegrationStatus, type CursorIntegrationStatus } from "./cursor-api";

/**
* The Cursor tab is a read-only companion, not a switch.
*
* Cursor Private Inference keeps its gateway settings in a SQLite database the running app
* rewrites and its API key in the OS keychain, both out of bounds for this proxy. So the page
* does the three things it can do honestly: say which Cursor builds are installed, hand the
* user the two values Cursor's own form wants, and report whether a Cursor client has called
* us since the proxy started. Everything shown is a GET of one status route.
* user the two values Cursor's own form wants, and predict the model controls it will render.
* Everything shown is a GET of one status route.
*/

function CopyValue({ value, label }: { value: string; label: string }) {
Expand Down Expand Up @@ -61,29 +60,23 @@ function DetectionRow({ labelKey, installed, path, version }: { labelKey: TKey;

export default function CursorIntegrationPage({ apiBase, active }: { apiBase: string; active: boolean }) {
const { t, locale } = useI18n();
// The clock is sampled when a payload arrives, never during render: the "seen within 24h"
// badge and the relative time must agree with each other and stay stable across re-renders.
const [sampledAt, setSampledAt] = useState(() => Date.now());
const fetchStatus = useCallback(
async (signal: AbortSignal) => {
const payload = await loadCursorIntegrationStatus(apiBase, signal);
// The overview paints a null read as "unknown"; the page has room to say why.
if (!payload) throw new Error("cursor status unavailable");
setSampledAt(Date.now());
return payload;
},
[apiBase],
);
// Polls while the tab is open so "Refresh model list" in Cursor shows up here within seconds.
// Poll while the tab is open so install and catalog changes show up without a reload.
const resource = useDataSurface<CursorIntegrationStatus>(
`integration-cursor-page:${apiBase}`,
[apiBase],
fetchStatus,
{ isEmpty: () => false, enabled: active, pollMs: 15_000, pauseWhenHidden: true },
);
const status = resource.state.data ?? null;
const labels = relativeTimeLabelsFromT(t);

return (
<section className="integration-native-page cursor-page" aria-labelledby="cursor-integration-title">
<h3 id="cursor-integration-title">{t("integrations.cursor.title")}</h3>
Expand Down Expand Up @@ -124,19 +117,6 @@ export default function CursorIntegrationPage({ apiBase, active }: { apiBase: st
)}
</div>

<div className="cursor-card" data-seen={status.lastSeen ? "true" : "false"}>
<h4>{t("integrations.cursor.connection")}</h4>
{status.lastSeen
? (
<p>
<span className={`badge ${sampledAt - status.lastSeen.at < CURSOR_SEEN_WINDOW_MS ? "badge-green" : "badge-muted"}`}>
{t("integrations.cursor.seen", { time: formatRelativeTime(status.lastSeen.at, labels, sampledAt), ua: status.lastSeen.userAgent })}
</span>
</p>
)
: <p className="muted">{t("integrations.cursor.neverSeen")}</p>}
</div>

<div className="cursor-card">
<h4>{t("integrations.cursor.models")}</h4>
<p className="muted">{t("integrations.cursor.modelsHint")}</p>
Expand Down
9 changes: 0 additions & 9 deletions gui/src/pages/integrations/cursor-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
*/
import { readJsonIfOk } from "../../fetch-json";

export interface CursorSeen {
at: number;
userAgent: string;
}

export interface CursorModelExpectation {
id: string;
reasoning: string[] | null;
Expand All @@ -19,7 +14,6 @@ export interface CursorIntegrationStatus {
privateInference: { installed: boolean; path: string | null; version: string | null };
regularCursor: { installed: boolean; path: string | null };
gateway: { baseUrl: string; apiKeyMode: "credential" | "placeholder"; placeholder: string };
lastSeen: CursorSeen | null;
models: CursorModelExpectation[];
guideUrl: string;
}
Expand All @@ -36,6 +30,3 @@ export async function loadCursorIntegrationStatus(apiBase: string, signal?: Abor
return null;
}
}

/** 24h is the window inside which a Cursor request counts as "connected". */
export const CURSOR_SEEN_WINDOW_MS = 24 * 60 * 60 * 1000;
15 changes: 7 additions & 8 deletions gui/src/pages/integrations/overview-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
type IntegrationStatus,
} from "./integration-api";
import type { NativeIntegrationClientId, NativeStatus } from "./native-api";
import { CURSOR_SEEN_WINDOW_MS, type CursorIntegrationStatus } from "./cursor-api";
import type { CursorIntegrationStatus } from "./cursor-api";

export type OverviewClientId =
| "codex"
Expand Down Expand Up @@ -438,10 +438,10 @@ function grokRow(


/**
* Cursor has no switch: its gateway is configured inside Cursor, and this proxy never
* writes there. "Applied" therefore means a Cursor client actually called us recently.
* Cursor has no switch: its gateway is configured inside Cursor, and this proxy cannot
* verify that configuration. Installation detection must not be reported as "applied".
*/
function cursorRow(payload: CursorIntegrationStatus | null, now = Date.now()): OverviewRow {
function cursorRow(payload: CursorIntegrationStatus | null): OverviewRow {
const base = {
id: "cursor" as const,
hash: "integrations/cursor",
Expand All @@ -457,13 +457,12 @@ function cursorRow(payload: CursorIntegrationStatus | null, now = Date.now()): O
if (!payload.privateInference.installed) {
return { ...base, state: "not-installed", installed: false, applied: false, detailKey: "integrations.detail.cursorAbsent" };
}
const seenRecently = payload.lastSeen !== null && now - payload.lastSeen.at < CURSOR_SEEN_WINDOW_MS;
return {
...base,
state: seenRecently ? "current" : "absent",
state: "absent",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Represent unverifiable Cursor wiring as unknown

Whenever Private Inference is installed, this unconditional absent state renders the literal “Not applied” badge and excludes Cursor from the applied count, including when Cursor is correctly configured. Since this code explicitly acknowledges that the configuration cannot be verified, the change replaces spoofable false positives with deterministic false negatives; use an unknown/unverifiable visual state while continuing not to count it as applied.

Useful? React with 👍 / 👎.

installed: true,
applied: seenRecently,
detailKey: seenRecently ? "integrations.detail.cursorSeen" : "integrations.detail.cursorNeverSeen",
applied: false,
detailKey: null,
};
}

Expand Down
33 changes: 2 additions & 31 deletions gui/tests/cursor-integration-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ function payload(overrides: Partial<CursorIntegrationStatus> = {}): CursorIntegr
privateInference: { installed: true, path: "/Applications/Cursor Private Inference.app", version: "3.18.25" },
regularCursor: { installed: true, path: "/Applications/Cursor.app" },
gateway: { baseUrl: "http://127.0.0.1:10100/v1", apiKeyMode: "placeholder", placeholder: "opencodex" },
lastSeen: null,
models: [
{ id: "gpt-5.6-sol", reasoning: ["low", "medium", "high", "xhigh"], context: { defaultWindow: 272_000, longWindow: 922_000 } },
{ id: "kimi/k3", reasoning: null, context: null },
Expand Down Expand Up @@ -113,29 +112,6 @@ test("reads its own status route and renders the gateway values with copy button
expect(copies.length).toBe(2);
});

test("a never-seen install tells the user to press Refresh model list", async () => {
await mount();
expect(textOf()).toContain("Refresh model list in Cursor");
expect(container.querySelector("[data-seen='false']")).not.toBeNull();
});

test("a recent request renders the relative time and the user agent", async () => {
statusResponse = () => json(payload({ lastSeen: { at: Date.now() - 3 * 60_000, userAgent: "Cursor/3.18.25" } }));
await mount();
const text = textOf();
expect(text).toContain("Cursor/3.18.25");
expect(text).toContain("3m ago");
expect(container.querySelector("[data-seen='true'] .badge-green")).not.toBeNull();
});

test("a stale request keeps the timestamp but drops the green badge", async () => {
statusResponse = () => json(payload({ lastSeen: { at: Date.now() - 3 * 86_400_000, userAgent: "Cursor/3.18.25" } }));
await mount();
expect(textOf()).toContain("3d ago");
expect(container.querySelector("[data-seen='true'] .badge-green")).toBeNull();
expect(container.querySelector("[data-seen='true'] .badge-muted")).not.toBeNull();
});

test("regular Cursor alone gets the tunnel explanation, not a gateway promise", async () => {
statusResponse = () => json(payload({ privateInference: { installed: false, path: null, version: null } }));
await mount();
Expand Down Expand Up @@ -273,17 +249,12 @@ test("overview: an unreadable source is unknown, not 'not installed'", () => {
expect(row.toggle).toBeNull();
});

test("overview: installed but never seen is absent; a recent request is current and applied", () => {
test("overview: an installed Cursor remains unapplied because its configuration cannot be verified", () => {
const idle = cursorRow(payload());
expect(idle.state).toBe("absent");
expect(idle.installed).toBe(true);
expect(idle.applied).toBe(false);
expect(idle.detailKey).toBe("integrations.detail.cursorNeverSeen");

const seen = cursorRow(payload({ lastSeen: { at: Date.now() - 60_000, userAgent: "Cursor/3.18.25" } }));
expect(seen.state).toBe("current");
expect(seen.applied).toBe(true);
expect(seen.detailKey).toBe("integrations.detail.cursorSeen");
expect(idle.detailKey).toBeNull();

const missing = cursorRow(payload({ privateInference: { installed: false, path: null, version: null } }));
expect(missing.state).toBe("not-installed");
Expand Down
5 changes: 2 additions & 3 deletions gui/tests/integrations-overview-rows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,14 @@ test("every client counts toward the summary, not just the file clients", () =>
privateInference: { installed: true, path: "/Applications/Cursor Private Inference.app", version: "3.18.25" },
regularCursor: { installed: false, path: null },
gateway: { baseUrl: "http://127.0.0.1:10100/v1", apiKeyMode: "placeholder", placeholder: "opencodex-loopback" },
lastSeen: { at: Date.now() - 60_000, userAgent: "Cursor/3.18.25" },
models: [],
guideUrl: "https://example.invalid/guide",
},
}));
const counts = countOverviewRows(rows.rows);
// codex + claude + desktop + grok + cursor + opencode. Keys are deliberately absent:
// codex + claude + desktop + grok + opencode. Keys and Cursor are deliberately absent:
// an issued credential is not an applied client.
expect(counts.applied).toBe(6);
expect(counts.applied).toBe(5);
expect(counts.stale).toBe(1);
expect(counts.unknown).toBe(0);
});
Expand Down
31 changes: 0 additions & 31 deletions src/integrations/cursor-seen.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ import {
import { detectInstall } from "../update/index";
import { readyProtocolMetadata } from "../remote/protocol";
import { modelCapabilityFields } from "./models-capabilities";
import { recordCursorSeen } from "../integrations/cursor-seen";

export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
Expand Down Expand Up @@ -1332,9 +1331,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
if (!isAllowedRequestOrigin(req, policy)) {
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
}
// The Integrations page reports whether a Cursor client has reached this proxy; the
// recorder keeps only a bounded User-Agent value and a timestamp, in memory.
recordCursorSeen(req.headers);
let goModels;
let modelEntitlements;
try {
Expand Down
6 changes: 1 addition & 5 deletions src/server/management/cursor-integration-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
* this proxy: its settings live in a SQLite database the running app rewrites and its API key
* in the OS keychain, both out of bounds for opencodex. So this route only answers the three
* questions the dashboard needs — which Cursor builds are installed, what to paste into the
* gateway form, and whether a Cursor client has actually called `/v1/models` since the proxy
* started — plus which active models will show Cursor's Reasoning and Context controls.
* gateway form, and which active models will show Cursor's Reasoning and Context controls.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update guidance after removing last-seen status

After this route stops returning the last-seen signal, docs-site/src/content/docs/guides/cursor-private-inference.md:74-93 still says the dashboard shows whether the gateway values took and that Refresh model list flips a Connection card, while docs-site/src/content/docs/guides/integrations.md:62-67 and src/cli/capabilities.ts:500-508 still promise the last request seen. Users and agents are therefore directed to a diagnostic that no longer exists; update the English and localized docs, change the capability summary, and regenerate skills/ocx/references/01_management_surface.md.

AGENTS.md reference: src/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

*/
import { readRuntimePort } from "../../config/process-state";
import { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextTier, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } from "../../codex/catalog";
import { cursorLastSeen, type CursorSeen } from "../../integrations/cursor-seen";
import { detectCursorInstalls, type CursorInstall } from "../../integrations/cursor-detect";
import { configuredApiAuthToken, isApiAuthRequired, jsonResponse } from "../auth-cors";
import { fetchAllModels } from "../management-api";
Expand All @@ -24,7 +22,6 @@ export interface CursorIntegrationStatus {
privateInference: { installed: boolean; path: string | null; version: string | null };
regularCursor: { installed: boolean; path: string | null };
gateway: { baseUrl: string; apiKeyMode: "credential" | "placeholder"; placeholder: string };
lastSeen: CursorSeen | null;
models: Array<{
id: string;
reasoning: string[] | null;
Expand Down Expand Up @@ -83,7 +80,6 @@ export async function buildCursorIntegrationStatus(
apiKeyMode,
placeholder: CURSOR_GATEWAY_PLACEHOLDER_KEY,
},
lastSeen: cursorLastSeen(),
models,
guideUrl: CURSOR_GUIDE_URL,
};
Expand Down
29 changes: 4 additions & 25 deletions tests/cursor-integration-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
seedCodexModelEntitlementsForTests,
} from "../src/codex/model-entitlements";
import { cursorProductJsonCandidates, detectCursorInstalls, type CursorDetectDeps } from "../src/integrations/cursor-detect";
import { cursorLastSeen, recordCursorSeen, resetCursorSeenForTests } from "../src/integrations/cursor-seen";
import { cursorEffortFamily } from "../src/server/models-capabilities";
import { startServer } from "../src/server";
import type { OcxConfig } from "../src/types";
Expand Down Expand Up @@ -64,22 +63,6 @@ describe("detectCursorInstalls", () => {
});
});

describe("cursor last-seen recorder", () => {
beforeEach(() => resetCursorSeenForTests());
afterEach(() => resetCursorSeenForTests());

test("records only a Cursor user agent, bounded and validated", () => {
recordCursorSeen(new Headers({ "user-agent": "curl/8.7.1" }), 1000);
expect(cursorLastSeen()).toBeNull();
recordCursorSeen(new Headers({ "user-agent": "Cursor/3.18.25" }), 2000);
expect(cursorLastSeen()).toEqual({ at: 2000, userAgent: "Cursor/3.18.25" });
// A padded or oversized value is not the shape Cursor sends and is ignored.
recordCursorSeen(new Headers({ "user-agent": `Cursor/${"x".repeat(60)}` }), 3000);
recordCursorSeen(new Headers({ "user-agent": "Cursor/3.18.25 <script>" }), 4000);
expect(cursorLastSeen()?.at).toBe(2000);
});
});

describe("cursorEffortFamily", () => {
test("mirrors Cursor's local picker table and strips provider prefixes", () => {
expect(cursorEffortFamily("gpt-5.6-sol")).toEqual(["low", "medium", "high", "xhigh"]);
Expand Down Expand Up @@ -119,19 +102,17 @@ describe("GET /api/native-integrations/cursor", () => {
beforeEach(() => {
testHome = mkdtempSync(join(tmpdir(), "ocx-cursor-status-"));
process.env.OPENCODEX_HOME = testHome;
resetCursorSeenForTests();
});

afterEach(() => {
resetCodexModelEntitlementCacheForTests();
resetCursorSeenForTests();
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
if (testHome) removeTreeWithRetry(testHome);
testHome = "";
});

test("reports gateway values, model expectations, and a last-seen Cursor request", async () => {
test("reports gateway values and model expectations without trusting Cursor user agents", async () => {
seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol"]);
saveConfig(statusConfig());
const server = startServer(0);
Expand All @@ -143,15 +124,14 @@ describe("GET /api/native-integrations/cursor", () => {
expect(before.status).toBe(200);
const first = await before.json() as {
gateway: { baseUrl: string; apiKeyMode: string; placeholder: string };
lastSeen: unknown;
models: Array<{ id: string; reasoning: string[] | null; context: { defaultWindow: number; longWindow: number } | null }>;
privateInference: { installed: boolean };
guideUrl: string;
};
expect(first.gateway.baseUrl).toBe(`http://127.0.0.1:${server.port}/v1`);
expect(first.gateway.apiKeyMode).toBe("placeholder");
expect(first.gateway.placeholder).toBe("opencodex-loopback");
expect(first.lastSeen).toBeNull();
expect("lastSeen" in first).toBe(false);
expect(typeof first.privateInference.installed).toBe("boolean");
expect(first.guideUrl).toContain("cursor-private-inference");
const k3 = first.models.find(model => model.id === "kimi/k3");
Expand All @@ -166,9 +146,8 @@ describe("GET /api/native-integrations/cursor", () => {
const discovery = await fetch(new URL("/v1/models", server.url), { headers: { "user-agent": "Cursor/3.18.25" } });
expect(discovery.status).toBe(200);
const after = await fetch(new URL("/api/native-integrations/cursor", server.url), { headers });
const second = await after.json() as { lastSeen: { at: number; userAgent: string } | null };
expect(second.lastSeen?.userAgent).toBe("Cursor/3.18.25");
expect(typeof second.lastSeen?.at).toBe("number");
const second = await after.json() as Record<string, unknown>;
expect("lastSeen" in second).toBe(false);
} finally {
await server.stop(true);
}
Expand Down
Loading