From 6c9f2a753ae996388a56af1d42feeb0bbc99b78e Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 13 May 2026 12:54:33 -0400 Subject: [PATCH 1/5] revert: remove plugin-side session restore cmux 0.64.5 ships native Amp session restore via PR #3710: - `.amp` is a built-in RestorableAgentKind with its own sanitizer + env allowlist - `cmux hooks setup` (or `cmux hooks amp install`) drops a bridge plugin at ~/.config/amp/plugins/cmux-session.ts that wires Amp's session lifecycle into cmux's standard session store That obsoletes the plugin-side restore added in #10. Keeping both means: - the .custom("amp") vault entry duplicates the built-in .amp kind (cmux's process scanner can match both, risking duplicate restorable entries) - the legacy ~/.cmuxterm/amp-hook-sessions.json writes are dead bytes on 0.64.5+ - two plugins (cmux-status.ts + cmux-session.ts) both fire on session.start and record into competing stores Removes: - HOOK_SESSIONS_PATH / CMUX_CONFIG_PATH constants and VAULT_REGISTRATION - writeHookSession, ensureVaultRegistration, stripJsonComments helpers - the two restore calls inside session.start - the `cmux: Register Amp for cmux session restore` command - node:fs / node:os / node:path imports (no longer needed) Status bar, workspace title tracking, handoff suffixing, cmux_notify, and the rename commands are untouched. Users on cmux 0.64.5+ should run `cmux hooks setup` once. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e223e-687c-74f9-b41b-049f1ee86a85 --- README.md | 13 +-- plugin/cmux-status.ts | 241 +----------------------------------------- 2 files changed, 10 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 276df9d..08211c9 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,11 @@ and subject to change. - `Rename workspace…` (prompts for a new title) - `Clear Amp status` - **Macro notification** when an agent turn ends with an error. -- **Session restore** — when cmux relaunches, Amp threads resume in place via - `amp threads continue `. The plugin auto-registers Amp as a custom - vault agent in `~/.config/cmux/cmux.json` and writes a hook-session record - for each thread. Requires cmux nightly or - [≥ 0.64.4 once released](https://github.com/manaflow-ai/cmux/commit/744521d) - (custom vault registry support). Older cmux silently ignores the files. - - Manual fallback: `cmux: Register Amp for cmux session restore` in the - command palette. + +> Session restore (resume Amp threads after a cmux relaunch) ships natively +> in cmux ≥ 0.64.5 — see +> [manaflow-ai/cmux#3710](https://github.com/manaflow-ai/cmux/pull/3710). +> Run `cmux hooks setup` (or `cmux hooks amp install`) to enable it. ## Installation diff --git a/plugin/cmux-status.ts b/plugin/cmux-status.ts index baef927..db28c78 100644 --- a/plugin/cmux-status.ts +++ b/plugin/cmux-status.ts @@ -15,44 +15,10 @@ import type { // doesn't expose `.nothrow()` / `.text()` from the underlying ShellPromise. // `Bun.$` properly escapes interpolated values and exposes the full API. import { $ as bunShell } from "bun"; -import { homedir } from "node:os"; -import { join, dirname } from "node:path"; -import { - existsSync, - mkdirSync, - readFileSync, - writeFileSync, -} from "node:fs"; const STATUS_KEY = "amp"; const LOG_SOURCE = "amp"; -// Path cmux reads on relaunch to restore agent sessions. -// See cmux Sources/RestorableAgentSession.swift → RestorableAgentSessionIndex.load -const HOOK_SESSIONS_PATH = join( - homedir(), - ".cmuxterm", - "amp-hook-sessions.json", -); - -// Path cmux loads vault agent registrations from (JSONC). -// See cmux Sources/VaultAgentRegistry.swift → CmuxVaultAgentRegistry.load -const CMUX_CONFIG_PATH = join(homedir(), ".config", "cmux", "cmux.json"); - -// Vault registration that teaches cmux about Amp as a `.custom` RestorableAgentKind. -// Once present, cmux reads HOOK_SESSIONS_PATH on relaunch and runs `amp threads continue `. -const VAULT_REGISTRATION = { - id: "amp", - name: "Amp", - detect: { processName: "amp", argvContains: ["amp"] }, - // sessionIdSource is required by the schema; argvOption is the simplest valid - // value. cmux's process scanner uses it for live-detection of running amp - // processes, but the hook-sessions.json path doesn't depend on it. - sessionIdSource: { type: "argvOption", argvOption: "--thread-id" }, - resumeCommand: "{{executable}} threads continue {{sessionId}}", - cwd: "preserve", -} as const; - // Short verbs shown in the cmux status bar for each Amp tool. function toolLabel(tool: string): string { switch (tool) { @@ -328,180 +294,15 @@ export default function (amp: PluginAPI) { process.on("SIGTERM", cleanup); process.on("exit", cleanup); - // ───────────────────────────────────────────────────────────────────────── - // Session restore. Two pieces, both written silently per session: - // 1. ~/.config/cmux/cmux.json — registers Amp as a custom vault agent - // so cmux knows how to resume it (`amp threads continue `). - // 2. ~/.cmuxterm/amp-hook-sessions.json — records this thread's id keyed - // by cmux workspace/panel so cmux can match it on relaunch. - // cmux re-reads both on every autosave tick (see RestorableAgentSessionIndex - // .load → CmuxVaultAgentRegistry.load), so changes apply without restart. - // - // Requires cmux nightly / >= 0.64.4 (custom vault registry support landed - // in manaflow-ai/cmux@744521d). Older cmux silently ignores both files. - // ───────────────────────────────────────────────────────────────────────── - - const writeHookSession = (threadId: string) => { - const workspaceId = process.env.CMUX_WORKSPACE_ID; - const surfaceId = process.env.CMUX_PANEL_ID; - if (!workspaceId || !surfaceId) { - log.log("CMUX_WORKSPACE_ID or CMUX_PANEL_ID missing; skipping restore"); - return; - } - - try { - mkdirSync(dirname(HOOK_SESSIONS_PATH), { recursive: true }); - let store: { version: number; sessions: Record } = { - version: 1, - sessions: {}, - }; - if (existsSync(HOOK_SESSIONS_PATH)) { - try { - const existing = JSON.parse(readFileSync(HOOK_SESSIONS_PATH, "utf8")); - if (existing && typeof existing === "object") { - store = existing; - if (!store.sessions) store.sessions = {}; - } - // If parsing fails we'd rather skip than nuke other agents' - // entries by overwriting with an empty store. - } catch { - log.log("amp-hook-sessions.json corrupt; skipping write"); - return; - } - } - // process.cwd() in a plugin is the plugin file's dir, not the user's - // shell cwd. PWD is inherited from amp's launch env which preserves it. - const cwd = process.env.PWD ?? process.cwd(); - // Key by panelId so the same surface only ever has one active session. - store.sessions[surfaceId] = { - sessionId: threadId, - workspaceId, - surfaceId, - cwd, - launchCommand: { - executablePath: process.env.AMP_EXECUTABLE_PATH ?? "amp", - arguments: ["amp"], - workingDirectory: cwd, - }, - updatedAt: Date.now() / 1000, - }; - writeFileSync(HOOK_SESSIONS_PATH, JSON.stringify(store, null, 2)); - } catch (err) { - log.log(`hook-sessions write failed: ${String(err)}`); - } - }; - - // Convert JSONC (cmux.json's format) to plain JSON so we can JSON.parse it. - // Strips // and /* */ comments and trailing commas before } or ]. - // (cmux uses JSONCParser.preprocess on read; we mirror that here.) - const stripJsonComments = (input: string): string => { - let out = ""; - let i = 0; - let inString = false; - let stringQuote = ""; - while (i < input.length) { - const ch = input[i]; - const next = input[i + 1]; - if (inString) { - out += ch; - if (ch === "\\" && i + 1 < input.length) { - out += input[i + 1]; - i += 2; - continue; - } - if (ch === stringQuote) inString = false; - i++; - continue; - } - if (ch === '"' || ch === "'") { - inString = true; - stringQuote = ch; - out += ch; - i++; - continue; - } - if (ch === "/" && next === "/") { - while (i < input.length && input[i] !== "\n") i++; - continue; - } - if (ch === "/" && next === "*") { - i += 2; - while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) - i++; - i += 2; - continue; - } - out += ch; - i++; - } - // Drop trailing commas before } or ] (JSONC allows them; JSON does not). - return out.replace(/,(\s*[}\]])/g, "$1"); - }; - - // Idempotently ensure ~/.config/cmux/cmux.json contains the Amp vault - // registration. Returns "added" | "present" | "skipped" | "failed". - const ensureVaultRegistration = (): - | "added" - | "present" - | "skipped" - | "failed" => { - try { - let raw = ""; - let parsed: { vault?: { agents?: unknown[] } } & Record = - {}; - if (existsSync(CMUX_CONFIG_PATH)) { - raw = readFileSync(CMUX_CONFIG_PATH, "utf8"); - try { - const decoded = JSON.parse(stripJsonComments(raw)); - if (decoded && typeof decoded === "object") parsed = decoded; - } catch { - // Don't risk corrupting a config we can't parse. User can fix - // manually or run the command palette command for an explicit - // error. - log.log( - "cmux.json is unparseable JSONC; vault registration skipped", - ); - return "skipped"; - } - } - - parsed.vault = parsed.vault ?? {}; - const agents = Array.isArray(parsed.vault.agents) - ? parsed.vault.agents - : []; - const present = agents.some( - (a: unknown) => - !!a && typeof a === "object" && (a as { id?: unknown }).id === "amp", - ); - if (present) return "present"; - - agents.push(VAULT_REGISTRATION); - parsed.vault.agents = agents; - - if (raw) { - const backup = `${CMUX_CONFIG_PATH}.${Date.now()}.bak`; - writeFileSync(backup, raw); - } - mkdirSync(dirname(CMUX_CONFIG_PATH), { recursive: true }); - writeFileSync( - CMUX_CONFIG_PATH, - JSON.stringify(parsed, null, 2) + "\n", - ); - log.log("registered Amp as custom vault agent in cmux.json"); - return "added"; - } catch (err) { - log.log(`vault registration failed: ${String(err)}`); - return "failed"; - } - }; + // Session restore is provided natively by cmux >= 0.64.5 (see + // manaflow-ai/cmux#3710). Run `cmux hooks setup` once to enable; it drops + // its own `~/.config/amp/plugins/cmux-session.ts` bridge plugin and uses + // the built-in `.amp` RestorableAgentKind. Nothing in this plugin file + // participates in restore anymore. amp.on("session.start", async (event: SessionStartEvent) => { await setStatus("idle", "circle", COLOR.idle); await bootstrapWorkspaceTitle(event.thread?.id); - if (event.thread?.id) { - ensureVaultRegistration(); - writeHookSession(event.thread.id); - } }); amp.on("agent.start", async (_event: AgentStartEvent) => { @@ -653,38 +454,6 @@ export default function (amp: PluginAPI) { }, ); - // Manual fallback. Registration normally happens automatically on - // session.start; this command is useful for explicit feedback when the - // automatic write was skipped (e.g. unparseable cmux.json). - amp.registerCommand( - "register-vault-amp", - { - title: "Register Amp for cmux session restore", - category: "cmux", - description: - "Re-add Amp as a custom vault agent in ~/.config/cmux/cmux.json. Normally runs automatically.", - }, - async (ctx) => { - const result = ensureVaultRegistration(); - switch (result) { - case "added": - await ctx.ui.notify("Amp registered for cmux session restore"); - break; - case "present": - await ctx.ui.notify("Amp already registered"); - break; - case "skipped": - await ctx.ui.notify( - "cmux.json is unparseable JSONC; add the `amp` vault entry manually", - ); - break; - case "failed": - await ctx.ui.notify("Registration failed; see Amp plugin log"); - break; - } - }, - ); - amp.registerCommand( "clear-status", { From 33620377cabb1b3a3192f1b5590a0506b76fc01c Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 13 May 2026 13:14:13 -0400 Subject: [PATCH 2/5] feat: warn when cmux native restore bridge plugin is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failure mode is silent today — if the user upgrades to cmux 0.64.5+ but forgets `cmux hooks amp install`, panes just don't restore and there's no obvious feedback. Adds a one-time `wsLog` warning per Amp session (only when running under cmux, i.e. CMUX_WORKSPACE_ID is set) when ~/.config/amp/plugins/cmux-session.ts is missing: > session restore disabled — run `cmux hooks amp install` (requires cmux ≥ 0.64.5) to enable Cheap to check (existsSync once on session.start) and cheap to ignore once the file is present. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e223e-687c-74f9-b41b-049f1ee86a85 --- README.md | 4 +++- plugin/cmux-status.ts | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 08211c9..3d5fc68 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ and subject to change. > Session restore (resume Amp threads after a cmux relaunch) ships natively > in cmux ≥ 0.64.5 — see > [manaflow-ai/cmux#3710](https://github.com/manaflow-ai/cmux/pull/3710). -> Run `cmux hooks setup` (or `cmux hooks amp install`) to enable it. +> Run `cmux hooks setup` (or `cmux hooks amp install`) to enable it; the +> plugin logs a one-time warning per Amp session if the bridge plugin +> (`~/.config/amp/plugins/cmux-session.ts`) is missing. ## Installation diff --git a/plugin/cmux-status.ts b/plugin/cmux-status.ts index db28c78..3d8a3b3 100644 --- a/plugin/cmux-status.ts +++ b/plugin/cmux-status.ts @@ -15,10 +15,23 @@ import type { // doesn't expose `.nothrow()` / `.text()` from the underlying ShellPromise. // `Bun.$` properly escapes interpolated values and exposes the full API. import { $ as bunShell } from "bun"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; const STATUS_KEY = "amp"; const LOG_SOURCE = "amp"; +// Bridge plugin that cmux >= 0.64.5's `cmux hooks amp install` writes here. +// Without it, native restore can't see Amp threads. +const CMUX_BRIDGE_PLUGIN_PATH = join( + homedir(), + ".config", + "amp", + "plugins", + "cmux-session.ts", +); + // Short verbs shown in the cmux status bar for each Amp tool. function toolLabel(tool: string): string { switch (tool) { @@ -298,11 +311,22 @@ export default function (amp: PluginAPI) { // manaflow-ai/cmux#3710). Run `cmux hooks setup` once to enable; it drops // its own `~/.config/amp/plugins/cmux-session.ts` bridge plugin and uses // the built-in `.amp` RestorableAgentKind. Nothing in this plugin file - // participates in restore anymore. + // participates in restore anymore — but we do nudge the user once per + // session if the bridge plugin is missing, since the failure mode is + // silent (panes just don't restore on relaunch). + const checkBridgePluginInstalled = async () => { + if (!WORKSPACE_REF) return; // not running under cmux at all + if (existsSync(CMUX_BRIDGE_PLUGIN_PATH)) return; + await wsLog( + "session restore disabled — run `cmux hooks amp install` (requires cmux ≥ 0.64.5) to enable", + "warning", + ); + }; amp.on("session.start", async (event: SessionStartEvent) => { await setStatus("idle", "circle", COLOR.idle); await bootstrapWorkspaceTitle(event.thread?.id); + await checkBridgePluginInstalled(); }); amp.on("agent.start", async (_event: AgentStartEvent) => { From 052fc051ded8760f37b76b373d64c5b1e24a874c Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 13 May 2026 13:18:18 -0400 Subject: [PATCH 3/5] feat: add rate-limited macOS notification when bridge plugin is missing The wsLog warning lands in the cmux activity feed, which is easy to miss. Adds a native macOS notification on top, gated to at most once per 24h via a tiny state file at ~/.cache/cmux-amp/bridge-warning.json: Title: "Amp session restore is off" Body: "Run `cmux hooks amp install` to enable cmux native restore." Once the bridge plugin exists, the existsSync check returns early and neither the wsLog nor the popup fire. After a fresh `cmux hooks amp install`, no further nag. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e223e-687c-74f9-b41b-049f1ee86a85 --- README.md | 8 ++++-- plugin/cmux-status.ts | 67 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3d5fc68..31aec1e 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ and subject to change. > Session restore (resume Amp threads after a cmux relaunch) ships natively > in cmux ≥ 0.64.5 — see > [manaflow-ai/cmux#3710](https://github.com/manaflow-ai/cmux/pull/3710). -> Run `cmux hooks setup` (or `cmux hooks amp install`) to enable it; the -> plugin logs a one-time warning per Amp session if the bridge plugin -> (`~/.config/amp/plugins/cmux-session.ts`) is missing. +> Run `cmux hooks setup` (or `cmux hooks amp install`) to enable it. If the +> bridge plugin (`~/.config/amp/plugins/cmux-session.ts`) is missing, the +> plugin logs a warning to the cmux activity feed every session and +> additionally fires a native macOS notification at most once per 24h +> (state in `~/.cache/cmux-amp/bridge-warning.json`). ## Installation diff --git a/plugin/cmux-status.ts b/plugin/cmux-status.ts index 3d8a3b3..4be9bc8 100644 --- a/plugin/cmux-status.ts +++ b/plugin/cmux-status.ts @@ -15,9 +15,14 @@ import type { // doesn't expose `.nothrow()` / `.text()` from the underlying ShellPromise. // `Bun.$` properly escapes interpolated values and exposes the full API. import { $ as bunShell } from "bun"; -import { existsSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; const STATUS_KEY = "amp"; const LOG_SOURCE = "amp"; @@ -32,6 +37,17 @@ const CMUX_BRIDGE_PLUGIN_PATH = join( "cmux-session.ts", ); +// Where we record the last time we surfaced a native macOS notification +// about the missing bridge plugin, so we don't nag every session. +const BRIDGE_WARNING_STATE_PATH = join( + homedir(), + ".cache", + "cmux-amp", + "bridge-warning.json", +); + +const BRIDGE_WARNING_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h + // Short verbs shown in the cmux status bar for each Amp tool. function toolLabel(tool: string): string { switch (tool) { @@ -311,16 +327,57 @@ export default function (amp: PluginAPI) { // manaflow-ai/cmux#3710). Run `cmux hooks setup` once to enable; it drops // its own `~/.config/amp/plugins/cmux-session.ts` bridge plugin and uses // the built-in `.amp` RestorableAgentKind. Nothing in this plugin file - // participates in restore anymore — but we do nudge the user once per - // session if the bridge plugin is missing, since the failure mode is - // silent (panes just don't restore on relaunch). + // participates in restore anymore — but we do nudge the user if the + // bridge plugin is missing, since the failure mode is otherwise silent + // (panes just don't restore on relaunch). + // + // Two channels: + // 1. wsLog (cmux activity feed) every session.start — cheap, useful for + // diagnostics, not in the user's face. + // 2. cmuxNotify (native macOS notification) at most once per 24h — + // visible enough to actually catch the user's attention without + // being annoying. const checkBridgePluginInstalled = async () => { if (!WORKSPACE_REF) return; // not running under cmux at all if (existsSync(CMUX_BRIDGE_PLUGIN_PATH)) return; + await wsLog( "session restore disabled — run `cmux hooks amp install` (requires cmux ≥ 0.64.5) to enable", "warning", ); + + // Rate-limit the popup. State file tracks last notification time; + // unparseable / missing file = treat as never-notified. + const now = Date.now(); + let lastNotifiedAt = 0; + if (existsSync(BRIDGE_WARNING_STATE_PATH)) { + try { + const parsed = JSON.parse( + readFileSync(BRIDGE_WARNING_STATE_PATH, "utf8"), + ); + if (typeof parsed?.lastNotifiedAt === "number") { + lastNotifiedAt = parsed.lastNotifiedAt; + } + } catch { + // Treat unparseable as never-notified; we'll overwrite below. + } + } + if (now - lastNotifiedAt < BRIDGE_WARNING_INTERVAL_MS) return; + + await cmuxNotify( + "Amp session restore is off", + "Run `cmux hooks amp install` to enable cmux native restore.", + ); + + try { + mkdirSync(dirname(BRIDGE_WARNING_STATE_PATH), { recursive: true }); + writeFileSync( + BRIDGE_WARNING_STATE_PATH, + JSON.stringify({ lastNotifiedAt: now }), + ); + } catch (err) { + log.log(`bridge-warning state write failed: ${String(err)}`); + } }; amp.on("session.start", async (event: SessionStartEvent) => { From 01473ac6f773251947eb78ccd3c9cb964083abe7 Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 13 May 2026 13:20:46 -0400 Subject: [PATCH 4/5] feat: add `cmux: Install cmux session restore` command palette action Saves users from dropping into a terminal when they hit the missing-bridge warning. Shells out to `cmux hooks amp install -y`, then verifies the bridge plugin actually appeared at ~/.config/amp/plugins/cmux-session.ts before reporting success (older cmux without the `hooks amp` subcommand can exit non-zero or print a stub error and still leave the file missing). Updates the wsLog warning + cmuxNotify body to point at the new command instead of the raw shell invocation, since most users will see one of those signals first. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e223e-687c-74f9-b41b-049f1ee86a85 --- README.md | 1 + plugin/cmux-status.ts | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31aec1e..1dea2fd 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ and subject to change. - **Command palette actions** under the `cmux:` category: - `Rename workspace to thread ID` - `Rename workspace…` (prompts for a new title) + - `Install cmux session restore` (one-click `cmux hooks amp install`) - `Clear Amp status` - **Macro notification** when an agent turn ends with an error. diff --git a/plugin/cmux-status.ts b/plugin/cmux-status.ts index 4be9bc8..d9783fd 100644 --- a/plugin/cmux-status.ts +++ b/plugin/cmux-status.ts @@ -342,7 +342,7 @@ export default function (amp: PluginAPI) { if (existsSync(CMUX_BRIDGE_PLUGIN_PATH)) return; await wsLog( - "session restore disabled — run `cmux hooks amp install` (requires cmux ≥ 0.64.5) to enable", + "session restore disabled — run `cmux: Install cmux session restore` (requires cmux ≥ 0.64.5) to enable", "warning", ); @@ -366,7 +366,7 @@ export default function (amp: PluginAPI) { await cmuxNotify( "Amp session restore is off", - "Run `cmux hooks amp install` to enable cmux native restore.", + "Run `cmux: Install cmux session restore` from the Amp command palette to enable.", ); try { @@ -535,6 +535,41 @@ export default function (amp: PluginAPI) { }, ); + // One-click installer for the cmux >= 0.64.5 bridge plugin. Saves users + // from having to drop into a terminal when they hit the missing-bridge + // warning — they can run this from the Amp command palette instead. + amp.registerCommand( + "install-cmux-restore", + { + title: "Install cmux session restore", + category: "cmux", + description: + "Run `cmux hooks amp install` to enable cmux ≥ 0.64.5's native Amp thread restore. Requires `plugins: reload` afterwards.", + }, + async (ctx) => { + try { + // -y skips the interactive confirm prompt cmux normally shows. + await bunShell`cmux hooks amp install -y`; + } catch (err) { + await ctx.ui.notify( + `Install failed (cmux ≥ 0.64.5 required): ${String(err)}`, + ); + return; + } + // Verify rather than trusting exit status — older cmux may print + // an unrecognized-subcommand error and still exit 0 in some shells. + if (!existsSync(CMUX_BRIDGE_PLUGIN_PATH)) { + await ctx.ui.notify( + `Install ran but ${CMUX_BRIDGE_PLUGIN_PATH} is missing — is cmux ≥ 0.64.5 on PATH?`, + ); + return; + } + await ctx.ui.notify( + "Installed cmux native restore. Run `plugins: reload` (or restart Amp) to activate.", + ); + }, + ); + amp.registerCommand( "clear-status", { From c9506b0ddecfc1bab9b8a27b73bb5b1d9dc3e26d Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 13 May 2026 14:03:25 -0400 Subject: [PATCH 5/5] fix: pin cmux notifications to the current surface so they don't auto-clear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without --surface, cmux drops the notification on whichever surface in the workspace is currently "default", and any UI activity on that surface fires notification.clear_requested → the popup vanishes within seconds (verified in ~/.cmuxterm/events.jsonl: every set_status from the cmux-status plugin's tool tracking was triggering a clear). Pinning to $CMUX_PANEL_ID keeps the notification in cmux's inbox until the user actually clicks into the Amp pane. Affects both the missing- bridge popup and the agent-error popup. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e223e-687c-74f9-b41b-049f1ee86a85 --- plugin/cmux-status.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugin/cmux-status.ts b/plugin/cmux-status.ts index d9783fd..4c92e44 100644 --- a/plugin/cmux-status.ts +++ b/plugin/cmux-status.ts @@ -154,6 +154,15 @@ export default function (amp: PluginAPI) { const WORKSPACE_REF = process.env.CMUX_WORKSPACE_ID || null; const wsArgs = WORKSPACE_REF ? ["--workspace", WORKSPACE_REF] : []; + // Pin notifications to *this* surface. Without --surface, cmux drops the + // notification on whichever surface in the workspace is currently + // "default", and any UI activity on that surface triggers + // notification.clear_requested → the popup vanishes within seconds. With + // --surface set to our own panel, the notification persists in cmux's + // inbox until the user actually interacts with the Amp pane. + const SURFACE_REF = process.env.CMUX_PANEL_ID || null; + const surfaceArgs = SURFACE_REF ? ["--surface", SURFACE_REF] : []; + // Track the last name we set so we can detect manual renames. let lastPluginSetName: string | null = null; @@ -188,9 +197,9 @@ export default function (amp: PluginAPI) { const cmuxNotify = async (title: string, body?: string) => { try { if (body) { - await $`cmux notify --title ${title} --body ${body} ${wsArgs}`; + await $`cmux notify --title ${title} --body ${body} ${wsArgs} ${surfaceArgs}`; } else { - await $`cmux notify --title ${title} ${wsArgs}`; + await $`cmux notify --title ${title} ${wsArgs} ${surfaceArgs}`; } } catch {} };