- Update available
+ {versionAdvisory.title}
- {onRunUpdate ? (
+ {onRunVersionAction ? (
) : null}
- {onRunUpdate && updateCommand ? (
+ {onRunVersionAction && updateCommand ? (
or, update manually using
@@ -798,7 +866,14 @@ export function ProviderInstanceCard({
{editorStatusNode}
+
+
+ {editorStatusNode}
+
+
}
control={
{
+ async (
+ candidate: Pick
,
+ targetVersion?: string,
+ ) => {
// Ref-based re-entry guard, mirroring refreshProviders: a state updater
// may run after this function returns, so it cannot gate the dispatch.
if (updatingInstanceIdsRef.current.has(candidate.instanceId)) {
@@ -663,6 +666,7 @@ export function EnvironmentProviderSettings({
input: {
provider: candidate.driver,
instanceId: candidate.instanceId,
+ ...(targetVersion ? { targetVersion } : {}),
},
});
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
@@ -887,9 +891,8 @@ export function EnvironmentProviderSettings({
);
const updateCandidate = providerUpdateCandidateByInstanceId.get(row.instanceId);
const isInstanceUpdateRunning =
- updateCandidate !== undefined &&
- (updatingProviderInstanceIds.has(updateCandidate.instanceId) ||
- isProviderUpdateActive(updateCandidate));
+ updatingProviderInstanceIds.has(row.instanceId) ||
+ (liveProvider !== undefined && isProviderUpdateActive(liveProvider));
const showInlineUpdateButton = updateCandidate !== undefined;
const canRunInlineUpdate = updateCandidate !== undefined && !isInstanceUpdateRunning;
const modelPreferences = settings.providerModelPreferences?.[row.instanceId] ?? {
@@ -971,6 +974,19 @@ export function EnvironmentProviderSettings({
modelOrder,
})
}
+ onInstallRecommended={
+ mode === "editor" &&
+ liveProvider?.compatibilityAdvisory?.message &&
+ liveProvider.compatibilityAdvisory.recommendedVersion &&
+ liveProvider.versionAdvisory?.canInstallVersion
+ ? () => {
+ void runProviderUpdate(
+ liveProvider,
+ liveProvider.compatibilityAdvisory?.recommendedVersion ?? undefined,
+ );
+ }
+ : undefined
+ }
onRunUpdate={
mode === "editor" && showInlineUpdateButton && updateCandidate
? () => {
@@ -978,9 +994,7 @@ export function EnvironmentProviderSettings({
}
: undefined
}
- isUpdating={
- mode === "editor" && showInlineUpdateButton ? isInstanceUpdateRunning : undefined
- }
+ isUpdating={mode === "editor" ? isInstanceUpdateRunning : undefined}
/>
);
};
diff --git a/apps/web/src/components/settings/providerStatus.test.ts b/apps/web/src/components/settings/providerStatus.test.ts
index 46dc7e262512b2019e11967f97428552d69c6e2d..5e2a93f48ca998387476521f12025848914cf397 100644
--- a/apps/web/src/components/settings/providerStatus.test.ts
+++ b/apps/web/src/components/settings/providerStatus.test.ts
@@ -1,7 +1,7 @@
import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
-import { getProviderSummary } from "./providerStatus";
+import { getProviderSummary, getProviderVersionAdvisoryPresentation } from "./providerStatus";
const provider: ServerProvider = {
instanceId: ProviderInstanceId.make("codex"),
@@ -69,3 +69,73 @@ describe("getProviderSummary", () => {
expect(getProviderSummary({ ...provider, status: "disabled" }).headline).toBe("Disabled");
});
});
+
+it("does not suggest copying a command that installs an incompatible latest version", () => {
+ const advisory = {
+ status: "behind_latest" as const,
+ currentVersion: "1.0.0",
+ latestVersion: "2.0.0",
+ updateCommand: "npm install -g fixture@latest",
+ canUpdate: true,
+ checkedAt: provider.checkedAt,
+ message: null,
+ };
+ const compatibility = {
+ status: "supported" as const,
+ latestVersionStatus: "broken" as const,
+ message: null,
+ recommendedRange: null,
+ recommendedVersion: null,
+ };
+ expect(getProviderVersionAdvisoryPresentation(advisory, compatibility)).toBeNull();
+ expect(
+ getProviderVersionAdvisoryPresentation(advisory, { ...compatibility, status: "broken" }, false),
+ ).toBeNull();
+ expect(
+ getProviderVersionAdvisoryPresentation(advisory, {
+ ...compatibility,
+ latestVersionStatus: "supported",
+ }),
+ ).not.toBeNull();
+});
+
+it("shows compatibility in the version popover even when the installed version is current", () => {
+ const advisory = {
+ status: "current" as const,
+ currentVersion: "2.0.0",
+ latestVersion: "2.0.0",
+ updateCommand: "npm install -g fixture@latest",
+ canUpdate: true,
+ checkedAt: provider.checkedAt,
+ message: null,
+ };
+ const compatibility = {
+ status: "broken" as const,
+ latestVersionStatus: "broken" as const,
+ message: "This version drops turns. Use 1.9.0.",
+ recommendedRange: null,
+ recommendedVersion: "1.9.0",
+ };
+ expect(getProviderVersionAdvisoryPresentation(advisory, compatibility)).toEqual({
+ title: "Known broken version",
+ detail: compatibility.message,
+ updateCommand: null,
+ emphasis: "strong",
+ targetVersion: "1.9.0",
+ });
+ expect(
+ getProviderVersionAdvisoryPresentation(undefined, {
+ ...compatibility,
+ status: "graceful",
+ recommendedVersion: null,
+ recommendedRange: ">=2.1.0",
+ message: null,
+ }),
+ ).toEqual({
+ title: "Limited support",
+ detail: "Use >=2.1.0 for full support.",
+ updateCommand: null,
+ emphasis: "normal",
+ targetVersion: null,
+ });
+});
diff --git a/apps/web/src/components/settings/providerStatus.ts b/apps/web/src/components/settings/providerStatus.ts
index 90c618f5daa7e5a8e99414d36019f35e890e15a4..8b6b95479686aaf74c2a543eb0aa54dbea5661b4 100644
--- a/apps/web/src/components/settings/providerStatus.ts
+++ b/apps/web/src/components/settings/providerStatus.ts
@@ -1,4 +1,8 @@
-import type { ServerProvider, ServerProviderVersionAdvisory } from "@t3tools/contracts";
+import type {
+ ServerProvider,
+ ServerProviderVersionAdvisory,
+ ServerProviderCompatibilityAdvisory,
+} from "@t3tools/contracts";
/**
* Visual treatment for each server-reported provider status. Centralized so
@@ -99,14 +103,53 @@ export function getProviderVersionLabel(version: string | null | undefined) {
return /^\d/.test(version) ? `v${version}` : version;
}
+const COMPATIBILITY_TITLES = {
+ graceful: "Limited support",
+ unsupported: "Unsupported version",
+ broken: "Known broken version",
+} as const;
+
+/** Compatibility guidance shares the version popover, with safe install actions. */
export function getProviderVersionAdvisoryPresentation(
advisory: ServerProviderVersionAdvisory | undefined,
+ compatibility?: ServerProviderCompatibilityAdvisory | undefined,
+ showCompatibility = true,
): {
+ readonly title: string;
readonly detail: string;
readonly updateCommand: string | null;
readonly emphasis: "normal" | "strong";
+ readonly targetVersion: string | null;
} | null {
- if (!advisory || advisory.status === "current" || advisory.status === "unknown") {
+ const latestIsIncompatible =
+ compatibility?.latestVersionStatus === "broken" ||
+ compatibility?.latestVersionStatus === "unsupported";
+ if (
+ showCompatibility &&
+ compatibility &&
+ (compatibility.status === "graceful" ||
+ compatibility.status === "unsupported" ||
+ compatibility.status === "broken")
+ ) {
+ const targetVersion = compatibility.recommendedVersion;
+ const recommendation = getProviderVersionLabel(targetVersion) ?? compatibility.recommendedRange;
+ return {
+ title: COMPATIBILITY_TITLES[compatibility.status],
+ detail:
+ compatibility.message ??
+ (recommendation ? `Use ${recommendation} for full support.` : "Update for full support."),
+ updateCommand:
+ targetVersion || latestIsIncompatible ? null : (advisory?.updateCommand ?? null),
+ emphasis: compatibility.status === "graceful" ? "normal" : "strong",
+ targetVersion,
+ };
+ }
+ if (
+ !advisory ||
+ advisory.status === "current" ||
+ advisory.status === "unknown" ||
+ latestIsIncompatible
+ ) {
return null;
}
@@ -115,6 +158,7 @@ export function getProviderVersionAdvisoryPresentation(
const versionLabel = getProviderVersionLabel(version);
return {
+ title: label,
detail:
advisory.message ??
(versionLabel
@@ -122,5 +166,6 @@ export function getProviderVersionAdvisoryPresentation(
: `${label}: install the latest provider version.`),
updateCommand: advisory.updateCommand,
emphasis: "normal" as const,
+ targetVersion: null,
};
}
diff --git a/docs/user/install.md b/docs/user/install.md
index 2a7e7dc7e749c6769bca2097cccc992fd02c4820..773979e7730200ee8a246ce2704009055b75ffa7 100644
--- a/docs/user/install.md
+++ b/docs/user/install.md
@@ -120,6 +120,12 @@ Provider CLIs must be on the server's `PATH`. If T3 Code cannot find one, set it
Cursor's executable is `cursor-agent`, although its login command is
`agent login`. Antigravity can use its managed runtime without a `PATH` entry.
+T3 Code warns when a provider version has known compatibility problems with your
+release. Check **Settings → Providers** on that environment for the recommended
+version or range. When its package manager supports installing a specific version,
+you can install the recommendation there. Otherwise use the provider's installer
+on the environment's machine. An unlisted version is unverified.
+
When a provider CLI is behind its latest release, its provider card shows the
available version. **Update now** appears only when T3 Code can tell which
installer owns the CLI (its own update command, Homebrew, or a global npm, pnpm,
diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts
index 49a377208799fe9c65d03fd86ab8c82c913fe406..c137cac9ac7a7dcdf232475bf37378a71d3df266 100644
--- a/packages/contracts/src/server.ts
+++ b/packages/contracts/src/server.ts
@@ -148,6 +148,22 @@ export const ServerProviderContinuation = Schema.Struct({
});
export type ServerProviderContinuation = typeof ServerProviderContinuation.Type;
+export const ServerProviderCompatibilityStatus = Schema.Literals([
+ "unknown",
+ "supported",
+ "graceful",
+ "unsupported",
+ "broken",
+]);
+export const ServerProviderCompatibilityAdvisory = Schema.Struct({
+ status: ServerProviderCompatibilityStatus,
+ latestVersionStatus: Schema.optionalKey(ServerProviderCompatibilityStatus),
+ message: Schema.NullOr(TrimmedNonEmptyString),
+ recommendedVersion: Schema.NullOr(TrimmedNonEmptyString),
+ recommendedRange: Schema.NullOr(TrimmedNonEmptyString),
+});
+export type ServerProviderCompatibilityAdvisory = typeof ServerProviderCompatibilityAdvisory.Type;
+
export const ServerProviderVersionAdvisoryStatus = Schema.Literals([
"unknown",
"current",
@@ -161,6 +177,7 @@ export const ServerProviderVersionAdvisory = Schema.Struct({
latestVersion: Schema.NullOr(TrimmedNonEmptyString),
updateCommand: Schema.NullOr(TrimmedNonEmptyString),
canUpdate: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
+ canInstallVersion: Schema.optionalKey(Schema.Boolean),
checkedAt: Schema.NullOr(IsoDateTime),
message: Schema.NullOr(TrimmedNonEmptyString),
});
@@ -234,6 +251,7 @@ export const ServerProvider = Schema.Struct({
// Absent when the driver has no notion of subscription usage.
usageLimits: Schema.optional(ServerProviderUsageLimits),
versionAdvisory: Schema.optionalKey(ServerProviderVersionAdvisory),
+ compatibilityAdvisory: Schema.optionalKey(ServerProviderCompatibilityAdvisory),
updateState: Schema.optionalKey(ServerProviderUpdateState),
});
export type ServerProvider = typeof ServerProvider.Type;
@@ -799,6 +817,7 @@ export type ServerProviderUpdatedPayload = typeof ServerProviderUpdatedPayload.T
export const ServerProviderUpdateInput = Schema.Struct({
provider: ProviderDriverKind,
+ targetVersion: Schema.optionalKey(TrimmedNonEmptyString),
instanceId: Schema.optionalKey(ProviderInstanceId),
});
export type ServerProviderUpdateInput = typeof ServerProviderUpdateInput.Type;