): s
return parts.length > 0 ? parts.join(" ") : undefined;
}
+function buildCursorCliCommandMissingMessage(binaryPath: string): string {
+ return [
+ `Cursor CLI command \`${binaryPath}\` was not found.`,
+ `Install or enable the Cursor CLI, make sure \`${binaryPath}\` is on PATH, then restart T3 Code.`,
+ `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
+ ].join(" ");
+}
+
export function buildCursorProviderSnapshot(input: {
readonly checkedAt: string;
readonly cursorSettings: CursorSettings;
@@ -1014,7 +1029,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
status: "error",
auth: { status: "unknown" },
message: isCommandMissingCause(error)
- ? "Cursor Agent CLI (`agent`) is not installed or not on PATH."
+ ? buildCursorCliCommandMissingMessage(cursorSettings.binaryPath)
: `Failed to execute Cursor Agent CLI health check: ${error instanceof Error ? error.message : String(error)}.`,
},
});
@@ -1073,7 +1088,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
yield* Effect.logWarning("Cursor ACP model discovery failed", {
cause: Cause.pretty(discoveryExit.cause),
});
- discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details.";
+ discoveryWarning = CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE;
} else if (Option.isNone(discoveryExit.value)) {
discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;
} else if (discoveryExit.value.value.length === 0) {
diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
index bf869d25c66..d826eca0063 100644
--- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
+++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
@@ -208,19 +208,17 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
>
);
return (
- {
- if (isResponding) return;
handleOptionSelection(activeQuestion.id, option.label);
}}
className={className}
>
{content}
-
+
);
})}
diff --git a/packages/client-runtime/src/shellSnapshotReducer.test.ts b/packages/client-runtime/src/shellSnapshotReducer.test.ts
index 69ae5e5d69f..6e5b8eab5aa 100644
--- a/packages/client-runtime/src/shellSnapshotReducer.test.ts
+++ b/packages/client-runtime/src/shellSnapshotReducer.test.ts
@@ -44,6 +44,26 @@ const stubThread = {
} as const;
describe("applyShellStreamEvent", () => {
+ it("ignores stale project upserts without mutating the snapshot", () => {
+ const snapshotWithProject: OrchestrationShellSnapshot = {
+ ...baseSnapshot,
+ snapshotSequence: 4,
+ projects: [stubProject],
+ };
+
+ for (const sequence of [3, 4]) {
+ const next = applyShellStreamEvent(snapshotWithProject, {
+ kind: "project-upserted",
+ sequence,
+ project: { ...stubProject, title: "Stale Title" },
+ });
+
+ expect(next).toBe(snapshotWithProject);
+ expect(next.snapshotSequence).toBe(4);
+ expect(next.projects[0]?.title).toBe("Test Project");
+ }
+ });
+
describe("project-upserted", () => {
it("adds a new project", () => {
const event: OrchestrationShellStreamEvent = {
diff --git a/packages/client-runtime/src/shellSnapshotReducer.ts b/packages/client-runtime/src/shellSnapshotReducer.ts
index a30eedb769b..0274e6539dd 100644
--- a/packages/client-runtime/src/shellSnapshotReducer.ts
+++ b/packages/client-runtime/src/shellSnapshotReducer.ts
@@ -13,6 +13,8 @@ export function applyShellStreamEvent(
snapshot: OrchestrationShellSnapshot,
event: OrchestrationShellStreamEvent,
): OrchestrationShellSnapshot {
+ if (event.sequence <= snapshot.snapshotSequence) return snapshot;
+
switch (event.kind) {
case "project-upserted": {
const projects = snapshot.projects.some((p) => p.id === event.project.id)
diff --git a/packages/shared/src/remote.test.ts b/packages/shared/src/remote.test.ts
index 54c78907421..24e78757009 100644
--- a/packages/shared/src/remote.test.ts
+++ b/packages/shared/src/remote.test.ts
@@ -72,6 +72,53 @@ describe("remote", () => {
});
});
+ it("rejects unsupported direct pairing URL protocols", () => {
+ let pairingUrlError: unknown;
+ try {
+ resolveRemotePairingTarget({
+ pairingUrl: "ftp://remote.example.com/pair#token=pairing-token",
+ });
+ } catch (cause) {
+ pairingUrlError = cause;
+ }
+
+ expect(pairingUrlError).toBeInstanceOf(RemotePairingUrlInvalidError);
+ expect(pairingUrlError).toMatchObject({ protocol: "ftp:" });
+ expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeUndefined();
+ });
+
+ it("rejects unsupported hosted pairing backend protocols", () => {
+ let hostError: unknown;
+ try {
+ resolveRemotePairingTarget({
+ pairingUrl:
+ "https://app.t3.codes/pair?host=ftp%3A%2F%2Fremote.example.com#token=pairing-token",
+ });
+ } catch (cause) {
+ hostError = cause;
+ }
+
+ expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
+ expect(hostError).toMatchObject({ source: "hosted-pairing-host", protocol: "ftp:" });
+ expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
+ });
+
+ it("rejects unsupported direct host protocols", () => {
+ let hostError: unknown;
+ try {
+ resolveRemotePairingTarget({
+ host: "ftp://remote.example.com",
+ pairingCode: "pairing-token",
+ });
+ } catch (cause) {
+ hostError = cause;
+ }
+
+ expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
+ expect(hostError).toMatchObject({ source: "direct-host", protocol: "ftp:" });
+ expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
+ });
+
it("uses distinct structural errors for missing pairing inputs", () => {
expect(() => resolveRemotePairingTarget({})).toThrowError(RemoteBackendUrlMissingError);
expect(() =>
diff --git a/packages/shared/src/remote.ts b/packages/shared/src/remote.ts
index 703811609b8..7347dbc74a1 100644
--- a/packages/shared/src/remote.ts
+++ b/packages/shared/src/remote.ts
@@ -3,6 +3,7 @@ import * as Schema from "effect/Schema";
const PAIRING_TOKEN_PARAM = "token";
const HOSTED_PAIRING_HOST_PARAM = "host";
const HOSTED_PAIRING_LABEL_PARAM = "label";
+const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]);
const readHashParams = (url: URL): URLSearchParams =>
new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
@@ -18,7 +19,10 @@ export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass()(
"RemotePairingUrlInvalidError",
- { cause: Schema.Defect() },
+ {
+ cause: Schema.optional(Schema.Defect()),
+ protocol: Schema.optional(Schema.String),
+ },
) {
override get message(): string {
return "Pairing URL is invalid.";
@@ -29,7 +33,8 @@ export class RemoteBackendUrlInvalidError extends Schema.TaggedErrorClass
+ SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol);
+
const normalizeRemoteBaseUrl = (
rawValue: string,
source: RemoteBackendUrlInvalidError["source"],
@@ -83,6 +91,12 @@ const normalizeRemoteBaseUrl = (
} catch (cause) {
throw new RemoteBackendUrlInvalidError({ source, cause });
}
+ if (!hasSupportedRemoteBackendProtocol(url)) {
+ throw new RemoteBackendUrlInvalidError({
+ source,
+ protocol: url.protocol,
+ });
+ }
url.pathname = "/";
url.search = "";
url.hash = "";
@@ -184,6 +198,11 @@ export const resolveRemotePairingTarget = (input: {
} catch (cause) {
throw new RemotePairingUrlInvalidError({ cause });
}
+ if (!hasSupportedRemoteBackendProtocol(url)) {
+ throw new RemotePairingUrlInvalidError({
+ protocol: url.protocol,
+ });
+ }
const hostedPairingRequest = readHostedPairingRequest(url);
if (hostedPairingRequest) {
const hostedBackendUrl = normalizeRemoteBaseUrl(