Skip to content

Commit 5e30b3a

Browse files
committed
🤖 fix: add downgrade compatibility for incompatible runtime configs
When users upgrade to a version with new runtime types (like 'local' without srcBaseDir for project-dir mode, or new 'worktree' type) and then downgrade, the old version shows a clear error instead of crashing. Changes: - Add IncompatibleRuntimeError for workspaces from newer mux versions - Add isIncompatibleRuntimeConfig helper to detect future configs - Add incompatibleRuntime field to FrontendWorkspaceMetadata - Show error in AIView when workspace is incompatible - Add 'incompatible_workspace' SendMessageError type as fallback - Mark incompatible_workspace as non-retryable This prepares for #824 (Local/Worktree runtime distinction) by ensuring users can safely downgrade without losing access to their other workspaces.
1 parent 284dbc7 commit 5e30b3a

File tree

14 files changed

+243
-2
lines changed

14 files changed

+243
-2
lines changed

src/browser/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ function AppInner() {
574574
runtimeConfig={
575575
workspaceMetadata.get(selectedWorkspace.workspaceId)?.runtimeConfig
576576
}
577+
incompatibleRuntime={
578+
workspaceMetadata.get(selectedWorkspace.workspaceId)?.incompatibleRuntime
579+
}
577580
/>
578581
</ErrorBoundary>
579582
) : creationProjectPath ? (

src/browser/components/AIView.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ interface AIViewProps {
4949
namedWorkspacePath: string; // User-friendly path for display and terminal
5050
runtimeConfig?: RuntimeConfig;
5151
className?: string;
52+
/** If set, workspace is incompatible (from newer mux version) and this error should be displayed */
53+
incompatibleRuntime?: string;
5254
}
5355

5456
const AIViewInner: React.FC<AIViewProps> = ({
@@ -607,8 +609,37 @@ const AIViewInner: React.FC<AIViewProps> = ({
607609
);
608610
};
609611

612+
/**
613+
* Incompatible workspace error display.
614+
* Shown when a workspace was created with a newer version of mux.
615+
*/
616+
const IncompatibleWorkspaceView: React.FC<{ message: string; className?: string }> = ({
617+
message,
618+
className,
619+
}) => (
620+
<div className={cn("flex h-full flex-col items-center justify-center p-8", className)}>
621+
<div className="max-w-md text-center">
622+
<div className="mb-4 text-4xl">⚠️</div>
623+
<h2 className="mb-2 text-xl font-semibold text-[var(--color-text-primary)]">
624+
Incompatible Workspace
625+
</h2>
626+
<p className="mb-4 text-[var(--color-text-secondary)]">{message}</p>
627+
<p className="text-sm text-[var(--color-text-tertiary)]">
628+
You can delete this workspace and create a new one, or upgrade mux to use it.
629+
</p>
630+
</div>
631+
</div>
632+
);
633+
610634
// Wrapper component that provides the mode and thinking contexts
611635
export const AIView: React.FC<AIViewProps> = (props) => {
636+
// Early return for incompatible workspaces - no hooks called in this path
637+
if (props.incompatibleRuntime) {
638+
return (
639+
<IncompatibleWorkspaceView message={props.incompatibleRuntime} className={props.className} />
640+
);
641+
}
642+
612643
return (
613644
<ModeProvider workspaceId={props.workspaceId}>
614645
<ProviderOptionsProvider>

src/browser/components/ChatInputToasts.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,18 @@ describe("ChatInputToasts", () => {
6969
expect(toast.title).toBe("Message Send Failed");
7070
expect(toast.message).toContain("unexpected error");
7171
});
72+
73+
test("should create toast for incompatible_workspace error", () => {
74+
const error: SendMessageError = {
75+
type: "incompatible_workspace",
76+
message: "This workspace uses a runtime configuration from a newer version of mux.",
77+
};
78+
79+
const toast = createErrorToast(error);
80+
81+
expect(toast.type).toBe("error");
82+
expect(toast.title).toBe("Incompatible Workspace");
83+
expect(toast.message).toContain("newer version");
84+
});
7285
});
7386
});

src/browser/components/ChatInputToasts.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ export const createErrorToast = (error: SendMessageErrorType): Toast => {
195195
};
196196
}
197197

198+
case "incompatible_workspace": {
199+
return {
200+
id: Date.now().toString(),
201+
type: "error",
202+
title: "Incompatible Workspace",
203+
message: error.message,
204+
solution: (
205+
<>
206+
<SolutionLabel>Solution:</SolutionLabel>
207+
Upgrade mux to use this workspace, or delete it and create a new one.
208+
</>
209+
),
210+
};
211+
}
212+
198213
case "unknown":
199214
default: {
200215
const formatted = formatSendMessageError(error);

src/browser/utils/messages/retryEligibility.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,4 +594,12 @@ describe("isNonRetryableSendError", () => {
594594
};
595595
expect(isNonRetryableSendError(error)).toBe(false);
596596
});
597+
598+
it("returns true for incompatible_workspace error", () => {
599+
const error: SendMessageError = {
600+
type: "incompatible_workspace",
601+
message: "This workspace uses a runtime configuration from a newer version of mux.",
602+
};
603+
expect(isNonRetryableSendError(error)).toBe(true);
604+
});
597605
});

src/browser/utils/messages/retryEligibility.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function isNonRetryableSendError(error: SendMessageError): boolean {
5252
case "api_key_not_found": // Missing API key - user must configure
5353
case "provider_not_supported": // Unsupported provider - user must switch
5454
case "invalid_model_string": // Bad model format - user must fix
55+
case "incompatible_workspace": // Workspace from newer mux version - user must upgrade
5556
return true;
5657
case "unknown":
5758
return false; // Unknown errors might be transient

src/common/types/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type SendMessageError =
1212
| { type: "api_key_not_found"; provider: string }
1313
| { type: "provider_not_supported"; provider: string }
1414
| { type: "invalid_model_string"; message: string }
15+
| { type: "incompatible_workspace"; message: string }
1516
| { type: "unknown"; raw: string };
1617

1718
/**

src/common/types/workspace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ export interface GitStatus {
7474
export interface FrontendWorkspaceMetadata extends WorkspaceMetadata {
7575
/** Worktree path (uses workspace name as directory) */
7676
namedWorkspacePath: string;
77+
78+
/**
79+
* If set, this workspace has an incompatible runtime configuration
80+
* (e.g., from a newer version of mux). The workspace should be displayed
81+
* but interactions should show this error message.
82+
*/
83+
incompatibleRuntime?: string;
7784
}
7885

7986
export interface WorkspaceActivitySnapshot {

src/common/utils/errors/formatSendError.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export function formatSendMessageError(error: SendMessageError): FormattedError
3232
message: error.message,
3333
};
3434

35+
case "incompatible_workspace":
36+
return {
37+
message: error.message,
38+
};
39+
3540
case "unknown":
3641
return {
3742
message: error.raw || "An unexpected error occurred",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Runtime configuration compatibility checks.
3+
*
4+
* This module is intentionally in common/ to avoid circular dependencies
5+
* with runtime implementations (LocalRuntime, SSHRuntime, etc.).
6+
*/
7+
8+
import type { RuntimeConfig } from "@/common/types/runtime";
9+
10+
/**
11+
* Check if a runtime config is from a newer version and incompatible.
12+
*
13+
* This handles downgrade compatibility: if a user upgrades to a version
14+
* with new runtime types (e.g., "local" without srcBaseDir for project-dir mode),
15+
* then downgrades, those workspaces should show a clear error rather than crashing.
16+
*/
17+
export function isIncompatibleRuntimeConfig(config: RuntimeConfig | undefined): boolean {
18+
if (!config) {
19+
return false;
20+
}
21+
// Future versions may add "local" without srcBaseDir (project-dir mode)
22+
// or new types like "worktree". Detect these as incompatible.
23+
if (config.type === "local" && !("srcBaseDir" in config && config.srcBaseDir)) {
24+
return true;
25+
}
26+
// Unknown types from future versions
27+
if (config.type !== "local" && config.type !== "ssh") {
28+
return true;
29+
}
30+
return false;
31+
}

0 commit comments

Comments
 (0)