Skip to content

Commit 894a0d4

Browse files
authored
🤖 fix: add downgrade compatibility for incompatible runtime configs (#827)
## Summary Prepares for #824 by ensuring users can safely upgrade→downgrade without losing access to their other workspaces. When users upgrade to a version with new runtime types (like `type: "local"` without `srcBaseDir` for project-dir mode, or new `type: "worktree"`) and then downgrade, the old version now shows a clear error instead of crashing. ## Changes | File | Change | |------|--------| | `src/common/utils/runtimeCompatibility.ts` | New: `isIncompatibleRuntimeConfig()` helper (in common/ to avoid circular deps) | | `src/common/types/workspace.ts` | Added `incompatibleRuntime` field to `FrontendWorkspaceMetadata` | | `src/node/config.ts` | Set `incompatibleRuntime` when loading workspace metadata | | `src/browser/components/AIView.tsx` | Display error view for incompatible workspaces | | `src/browser/App.tsx` | Pass `incompatibleRuntime` to AIView | | `src/node/runtime/runtimeFactory.ts` | Re-export helper + throw `IncompatibleRuntimeError` as safety net | | `src/node/services/ipcMain.ts` | Handle `IncompatibleRuntimeError` in sendMessage (fallback) | | `src/common/types/errors.ts` | Added `incompatible_workspace` SendMessageError type | ## User Experience When clicking into an incompatible workspace, users see a centered error view: > ⚠️ **Incompatible Workspace** > This workspace was created with a newer version of mux. > Please upgrade mux to use this workspace. > > You can delete this workspace and create a new one, or upgrade mux to use it. ## Testing - Added unit tests for `isIncompatibleRuntimeConfig()` - Added unit tests for `createRuntime()` throwing on incompatible configs - Added tests for error toast display (fallback path) - Added tests for non-retryable error classification _Generated with `mux`_
1 parent 99d3070 commit 894a0d4

File tree

15 files changed

+320
-2
lines changed

15 files changed

+320
-2
lines changed

src/browser/App.stories.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,83 @@ export const ManyWorkspaces: Story = {
377377
},
378378
};
379379

380+
/**
381+
* Story demonstrating the incompatible workspace error view.
382+
*
383+
* When a user downgrades to an older version of mux that doesn't support
384+
* a workspace's runtime configuration, the workspace shows an error message
385+
* instead of crashing. This ensures graceful degradation.
386+
*/
387+
export const IncompatibleWorkspace: Story = {
388+
render: () => {
389+
const AppWithIncompatibleWorkspace = () => {
390+
const initialized = useRef(false);
391+
392+
if (!initialized.current) {
393+
const workspaceId = "incompatible-ws";
394+
395+
const workspaces: FrontendWorkspaceMetadata[] = [
396+
{
397+
id: "my-app-main",
398+
name: "main",
399+
projectPath: "/home/user/projects/my-app",
400+
projectName: "my-app",
401+
namedWorkspacePath: "/home/user/.mux/src/my-app/main",
402+
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
403+
},
404+
{
405+
id: workspaceId,
406+
name: "incompatible",
407+
projectPath: "/home/user/projects/my-app",
408+
projectName: "my-app",
409+
namedWorkspacePath: "/home/user/.mux/src/my-app/incompatible",
410+
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
411+
// This field is set when a workspace has an incompatible runtime config
412+
incompatibleRuntime:
413+
"This workspace was created with a newer version of mux.\nPlease upgrade mux to use this workspace.",
414+
},
415+
];
416+
417+
setupMockAPI({
418+
projects: new Map([
419+
[
420+
"/home/user/projects/my-app",
421+
{
422+
workspaces: [
423+
{ path: "/home/user/.mux/src/my-app/main", id: "my-app-main", name: "main" },
424+
{
425+
path: "/home/user/.mux/src/my-app/incompatible",
426+
id: workspaceId,
427+
name: "incompatible",
428+
},
429+
],
430+
},
431+
],
432+
]),
433+
workspaces,
434+
});
435+
436+
// Set initial workspace selection to the incompatible workspace
437+
localStorage.setItem(
438+
"selectedWorkspace",
439+
JSON.stringify({
440+
workspaceId: workspaceId,
441+
projectPath: "/home/user/projects/my-app",
442+
projectName: "my-app",
443+
namedWorkspacePath: "/home/user/.mux/src/my-app/incompatible",
444+
})
445+
);
446+
447+
initialized.current = true;
448+
}
449+
450+
return <AppLoader />;
451+
};
452+
453+
return <AppWithIncompatibleWorkspace />;
454+
},
455+
};
456+
380457
/**
381458
* Story demonstrating all possible UI indicators in the project sidebar.
382459
*

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> = ({
@@ -610,8 +612,37 @@ const AIViewInner: React.FC<AIViewProps> = ({
610612
);
611613
};
612614

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

0 commit comments

Comments
 (0)