Skip to content
Merged
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
40 changes: 40 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,46 @@ jobs:
release/*.exe.blockmap
if-no-files-found: error

notify-slack-failure:
name: Notify Slack on Failure
runs-on: ubuntu-latest
needs: [build-macos, build-linux, build-windows, build-vscode-extension]
if: failure()
steps:
- name: Send failure notification to Slack
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
RELEASE_TAG: ${{ env.RELEASE_TAG }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "SLACK_WEBHOOK_URL secret is not set; skipping Slack notification."
exit 0
fi

# Build JSON payload with proper escaping via jq
jq -n \
--arg text "🚨 Release build failed for \`$RELEASE_TAG\`" \
--arg workflow_url "$WORKFLOW_URL" \
'{
text: $text,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ("🚨 *Release build failed*\n\nTag: `" + $ARGS.named.tag + "`\n<" + $workflow_url + "|View workflow run>")
}
}
]
}' --arg tag "$RELEASE_TAG" > payload.json

# Send to Slack
curl -X POST \
-H "Content-Type: application/json" \
-d @payload.json \
"$SLACK_WEBHOOK_URL"

notify-discord:
name: Notify Discord
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { useFeatureFlags } from "./contexts/FeatureFlagsContext";
import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext";
import { ExperimentsProvider } from "./contexts/ExperimentsContext";
import { getWorkspaceSidebarKey } from "./utils/workspace";
import { RosettaBanner } from "./components/RosettaBanner";

const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high", "xhigh"];

Expand Down Expand Up @@ -600,6 +601,7 @@ function AppInner() {
workspaceRecency={workspaceRecency}
/>
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
<RosettaBanner />
<div className="mobile-layout flex flex-1 overflow-hidden">
{selectedWorkspace ? (
(() => {
Expand Down
92 changes: 92 additions & 0 deletions src/browser/components/RosettaBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useEffect, useState } from "react";
import { AlertTriangle, X } from "lucide-react";
import { cn } from "@/common/lib/utils";
import { usePersistedState } from "@/browser/hooks/usePersistedState";

const ROSETTA_BANNER_DISMISSED_KEY = "rosettaBannerDismissedAt";
const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days

/**
* Banner shown when Mux is running under Rosetta 2 translation.
* Users can dismiss it, but it will re-appear after 30 days.
*/
export const RosettaBanner: React.FC = () => {
const [dismissedAt, setDismissedAt] = usePersistedState<number | null>(
ROSETTA_BANNER_DISMISSED_KEY,
null
);

const [isRosetta, setIsRosetta] = useState<boolean | null>(() => {
if (window.api?.isRosetta === true) {
return true;
}
if (window.api?.isRosetta === false) {
return false;
}
return null;
});

useEffect(() => {
if (isRosetta !== null) {
return;
}

let cancelled = false;

const load = async () => {
try {
const result = await window.api?.getIsRosetta?.();
if (cancelled) return;
setIsRosetta(result === true);
} catch {
if (cancelled) return;
setIsRosetta(false);
}
};

void load();

return () => {
cancelled = true;
};
}, [isRosetta]);

// Check if dismissal has expired (30 days)
const isDismissed = dismissedAt !== null && Date.now() - dismissedAt < DISMISS_DURATION_MS;

if (isRosetta !== true || isDismissed) {
return null;
}

return (
<div
className={cn(
"bg-warning/10 border-warning/30 text-warning flex items-center justify-between gap-3 border-b px-4 py-2 text-sm"
)}
>
<div className="flex items-center gap-2">
<AlertTriangle className="text-warning size-4 shrink-0" />
<span>
Mux is running under Rosetta. For better performance,{" "}
<a
href="https://mux.coder.com/download"
target="_blank"
rel="noopener noreferrer"
className="underline hover:no-underline"
>
download the native Apple Silicon version
</a>
.
</span>
</div>
<button
type="button"
onClick={() => setDismissedAt(Date.now())}
className="hover:text-warning/80 shrink-0 p-1 transition-colors"
aria-label="Dismiss Rosetta warning"
>
<X className="size-4" />
</button>
</div>
);
};
52 changes: 52 additions & 0 deletions src/browser/stories/App.rosetta.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Rosetta banner story - demonstrates the warning shown when running under Rosetta 2
*/

import { appMeta, AppWithMocks, type AppStory } from "./meta.js";
import { setupSimpleChatStory } from "./storyHelpers";
import { STABLE_TIMESTAMP, createUserMessage, createAssistantMessage } from "./mockFactory";

export default {
...appMeta,
title: "App/Rosetta",
};

/** Rosetta banner shown at top of app when running under translation */
export const RosettaBanner: AppStory = {
render: () => (
<AppWithMocks
setup={() => {
// Clear any previously dismissed state
localStorage.removeItem("rosettaBannerDismissedAt");

// Set window.api to simulate Rosetta environment
window.api = {
platform: "darwin",
versions: {
node: "20.0.0",
chrome: "120.0.0",
electron: "28.0.0",
},
isRosetta: true,
};

return setupSimpleChatStory({
messages: [
createUserMessage("msg-1", "Hello! Can you help me with my code?", {
historySequence: 1,
timestamp: STABLE_TIMESTAMP - 60000,
}),
createAssistantMessage(
"msg-2",
"Of course! I'd be happy to help. What would you like to work on today?",
{
historySequence: 2,
timestamp: STABLE_TIMESTAMP - 50000,
}
),
],
});
}}
/>
),
};
4 changes: 4 additions & 0 deletions src/common/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ declare global {
enableTelemetryInDev?: boolean;
// E2E test mode flag - used to adjust UI behavior (e.g., longer toast durations)
isE2E?: boolean;
// True if running under Rosetta 2 translation on Apple Silicon (storybook/tests may set this)
isRosetta?: boolean;
// Async getter (used in Electron) for environments where preload cannot use Node builtins
getIsRosetta?: () => Promise<boolean>;
// Optional ORPC-backed API surfaces populated in tests/storybook mocks
tokenizer?: unknown;
providers?: unknown;
Expand Down
15 changes: 15 additions & 0 deletions src/desktop/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,21 @@ async function loadServices(): Promise<void> {
sessionUsageService: services.sessionUsageService,
};

electronIpcMain.handle("mux:get-is-rosetta", async () => {
if (process.platform !== "darwin") {
return false;
}

try {
// Intentionally lazy import to keep startup fast and avoid bundling concerns.
// eslint-disable-next-line no-restricted-syntax -- main-process-only builtin
const { execSync } = await import("node:child_process");
const result = execSync("sysctl -n sysctl.proc_translated", { encoding: "utf8" }).trim();
return result === "1";
} catch {
return false;
}
});
electronIpcMain.on("start-orpc-server", (event) => {
const [serverPort] = event.ports;
orpcHandler.upgrade(serverPort, {
Expand Down
3 changes: 3 additions & 0 deletions src/desktop/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ contextBridge.exposeInMainWorld("api", {
},
isE2E: process.env.MUX_E2E === "1",
enableTelemetryInDev: process.env.MUX_ENABLE_TELEMETRY_IN_DEV === "1",
// NOTE: This is intentionally async so the preload script does not rely on Node builtins
// like `child_process` (which can break in hardened/sandboxed environments).
getIsRosetta: () => ipcRenderer.invoke("mux:get-is-rosetta"),
});
Loading