From fd36da45c27d16de60c618d949d73feee4f0fdf4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 3 Nov 2026 16:21:32 -0800 Subject: [PATCH] Cloud start: a start refused on this Mac says why, and marks no step failed A Cloud start the pane refused before it reached Cloud (its folder is not one of the tab's, a Send without a gesture) showed "The app refused the agent connection." under a failed "Set up a Cloud machine" step, and logged nothing under app.cloud. - cloudStartEnded words a transport.* refusal by its code: a folder outside the tab's reads "This folder isn't open in this tab; pick it to send it to Cloud.", any other through transportErrors.errorMessage. - Such a refusal sets refusedHere, and the checklist marks no step failed. - AgentPaneHandOff.cloud logs the pane's own refusals under app.cloud. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- .../AgentPaneCloudStart.swift | 21 +++++++++++++++++++ .../acpmux/CloudStartView.test.tsx | 6 ++++++ .../agent-session/acpmux/CloudStartView.tsx | 3 ++- .../acpmux/Localizable.xcstrings | 1 + .../acpmux/cloudStartProgress.test.ts | 19 ++++++++++++++++- .../acpmux/cloudStartProgress.ts | 20 ++++++++++++++++-- 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift index 1d0feb8f7204..cf7efac62ea5 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift @@ -2,6 +2,7 @@ public import CmuxNextCloud import CmuxNextCompat public import CmuxNextSettings public import Foundation +import os /// `cloud.startChat {task, cwd?, harness?, model?, key}`: a new chat's first send with Cloud picked /// as where it runs (the environment picker's Cloud row, Labs). The App picks or makes a Cloud @@ -174,7 +175,27 @@ struct AgentPaneHandOff { } } + private static let cloudLog = Logger(subsystem: "com.cmuxterm.app.next", category: "app.cloud") + + /// `cloud.startChat`, and a refusal made here, before the App's start ran (the folder check, the + /// gesture, an attachment), logged under `app.cloud` beside the App's own start failures: such a + /// refusal never reaches CloudStartChat, so without this the log said nothing at all. func cloud(_ start: AgentPaneCloudStart) async -> [String: Any] { + let reply = await cloudChecked(start) + if let code = Self.refusedHere(reply) { + Self.cloudLog.error("cloud start chat refused on this Mac before it started: \(code, privacy: .public)") + } + return reply + } + + /// The reply's code when this pane refused the start itself (`transport.*`, `cloud_attachments`, + /// `unsupported`); nil for a success or the App's own failure. + nonisolated static func refusedHere(_ reply: [String: Any]) -> String? { + guard reply["ok"] as? Bool == false, let code = (reply["error"] as? [String: Any])?["code"] as? String else { return nil } + return code.hasPrefix("transport.") || code == "cloud_attachments" || code == "unsupported" ? code : nil + } + + private func cloudChecked(_ start: AgentPaneCloudStart) async -> [String: Any] { // The folder is a path on this Mac: a chat on another machine never names one to copy. guard let run = model.onCloudStartChat, !model.chatIsElsewhere else { return AgentPaneModel.unsupported("cloud.startChat") } guard AgentPaneCloudAttachments(start.attachments) != nil else { diff --git a/webviews/src/agent-session/acpmux/CloudStartView.test.tsx b/webviews/src/agent-session/acpmux/CloudStartView.test.tsx index bfed68ddef90..efca59053442 100644 --- a/webviews/src/agent-session/acpmux/CloudStartView.test.tsx +++ b/webviews/src/agent-session/acpmux/CloudStartView.test.tsx @@ -202,3 +202,9 @@ test("row state words are screen-reader only, hidden by the card's own sheet", a ); expect(html).toContain(''); }); + +test("a start this Mac refused before it reached Cloud marks no step failed", () => { + const failed = { key: "k", task: "hey", state: "failed" as const, folder: "paint" }; + expect(startupChecklist(failed)[0]?.status).toBe("error"); + expect(startupChecklist({ ...failed, refusedHere: true }).map((row) => row.status)).not.toContain("error"); +}); diff --git a/webviews/src/agent-session/acpmux/CloudStartView.tsx b/webviews/src/agent-session/acpmux/CloudStartView.tsx index daedfdbd1efe..8044bef58262 100644 --- a/webviews/src/agent-session/acpmux/CloudStartView.tsx +++ b/webviews/src/agent-session/acpmux/CloudStartView.tsx @@ -130,8 +130,9 @@ export function startupChecklist( language: string = paneLanguage(), ): ChecklistStep[] { const oldStep = start.step ? CLOUD_START_STEPS.indexOf(start.step) : -1; + // A start this Mac refused before it reached Cloud ran no step: none is marked failed. const failedStep = - start.state === "failed" + start.state === "failed" && !start.refusedHere ? oldStep === 0 ? "machine" : oldStep === 1 diff --git a/webviews/src/agent-session/acpmux/Localizable.xcstrings b/webviews/src/agent-session/acpmux/Localizable.xcstrings index 821431b7c24a..b817919a45ab 100644 --- a/webviews/src/agent-session/acpmux/Localizable.xcstrings +++ b/webviews/src/agent-session/acpmux/Localizable.xcstrings @@ -251354,6 +251354,7 @@ "chiefWeb.notSent": {"comment":"Chief web: a message was rejected and can be retried","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Message not sent"}},"de":{"stringUnit":{"state":"translated","value":"Nachricht nicht gesendet"}},"fr":{"stringUnit":{"state":"translated","value":"Message non envoyé"}},"ar":{"stringUnit":{"state":"translated","value":"لم تُرسل الرسالة"}},"es":{"stringUnit":{"state":"translated","value":"Mensaje no enviado"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"訊息未傳送"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"消息未发送"}},"ko":{"stringUnit":{"state":"translated","value":"메시지를 보내지 못했습니다"}},"ja":{"stringUnit":{"state":"translated","value":"メッセージは送信されませんでした"}},"bs":{"stringUnit":{"state":"translated","value":"Poruka nije poslana"}},"da":{"stringUnit":{"state":"translated","value":"Beskeden blev ikke sendt"}},"it":{"stringUnit":{"state":"translated","value":"Messaggio non inviato"}},"km":{"stringUnit":{"state":"translated","value":"សារមិនត្រូវបានផ្ញើទេ"}},"nb":{"stringUnit":{"state":"translated","value":"Meldingen ble ikke sendt"}},"pl":{"stringUnit":{"state":"translated","value":"Wiadomość nie została wysłana"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Mensagem não enviada"}},"ru":{"stringUnit":{"state":"translated","value":"Сообщение не отправлено"}},"th":{"stringUnit":{"state":"translated","value":"ไม่ได้ส่งข้อความ"}},"tr":{"stringUnit":{"state":"translated","value":"Mesaj gönderilmedi"}},"uk":{"stringUnit":{"state":"translated","value":"Повідомлення не надіслано"}},"vi":{"stringUnit":{"state":"translated","value":"Tin nhắn chưa được gửi"}}}}, "context.cacheWarm":{"comment":"Agent pane composer: the context ring hover card's prompt cache line while the cache is warm; {left} is the time until it expires (3:12, or 52:10 for an hour's cache)","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"ذاكرة التخزين المؤقت دافئة · متبقٍ {left}"}},"bs":{"stringUnit":{"state":"needs_review","value":"Keš topao · još {left}"}},"da":{"stringUnit":{"state":"needs_review","value":"Cache varm · {left} tilbage"}},"de":{"stringUnit":{"state":"needs_review","value":"Cache warm · noch {left}"}},"en":{"stringUnit":{"state":"translated","value":"Cache warm · {left} left"}},"es":{"stringUnit":{"state":"needs_review","value":"Caché activa · quedan {left}"}},"fr":{"stringUnit":{"state":"needs_review","value":"Cache actif · {left} restantes"}},"it":{"stringUnit":{"state":"needs_review","value":"Cache attiva · {left} rimanenti"}},"ja":{"stringUnit":{"state":"needs_review","value":"キャッシュ有効 · 残り {left}"}},"km":{"stringUnit":{"state":"needs_review","value":"ឃ្លាំងសម្ងាត់នៅក្តៅ · នៅសល់ {left}"}},"ko":{"stringUnit":{"state":"needs_review","value":"캐시 유지 중 · {left} 남음"}},"nb":{"stringUnit":{"state":"needs_review","value":"Hurtigbuffer varm · {left} igjen"}},"pl":{"stringUnit":{"state":"needs_review","value":"Pamięć podręczna aktywna · zostało {left}"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Cache ativo · faltam {left}"}},"ru":{"stringUnit":{"state":"needs_review","value":"Кэш активен · осталось {left}"}},"th":{"stringUnit":{"state":"needs_review","value":"แคชยังอุ่น · เหลือ {left}"}},"tr":{"stringUnit":{"state":"needs_review","value":"Önbellek sıcak · {left} kaldı"}},"uk":{"stringUnit":{"state":"needs_review","value":"Кеш активний · залишилось {left}"}},"vi":{"stringUnit":{"state":"needs_review","value":"Bộ nhớ đệm còn ấm · còn {left}"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"缓存有效 · 剩余 {left}"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"快取有效 · 剩餘 {left}"}}}}, "context.cacheExpired":{"comment":"Agent pane composer: the context ring hover card's prompt cache line once the cache has expired (the next turn reads the whole context again, uncached)","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"انتهت صلاحية ذاكرة التخزين المؤقت"}},"bs":{"stringUnit":{"state":"needs_review","value":"Keš je istekao"}},"da":{"stringUnit":{"state":"needs_review","value":"Cache udløbet"}},"de":{"stringUnit":{"state":"needs_review","value":"Cache abgelaufen"}},"en":{"stringUnit":{"state":"translated","value":"Cache expired"}},"es":{"stringUnit":{"state":"needs_review","value":"Caché caducada"}},"fr":{"stringUnit":{"state":"needs_review","value":"Cache expiré"}},"it":{"stringUnit":{"state":"needs_review","value":"Cache scaduta"}},"ja":{"stringUnit":{"state":"needs_review","value":"キャッシュ期限切れ"}},"km":{"stringUnit":{"state":"needs_review","value":"ឃ្លាំងសម្ងាត់បានផុតកំណត់"}},"ko":{"stringUnit":{"state":"needs_review","value":"캐시 만료됨"}},"nb":{"stringUnit":{"state":"needs_review","value":"Hurtigbuffer utløpt"}},"pl":{"stringUnit":{"state":"needs_review","value":"Pamięć podręczna wygasła"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Cache expirado"}},"ru":{"stringUnit":{"state":"needs_review","value":"Кэш истёк"}},"th":{"stringUnit":{"state":"needs_review","value":"แคชหมดอายุแล้ว"}},"tr":{"stringUnit":{"state":"needs_review","value":"Önbelleğin süresi doldu"}},"uk":{"stringUnit":{"state":"needs_review","value":"Кеш закінчився"}},"vi":{"stringUnit":{"state":"needs_review","value":"Bộ nhớ đệm đã hết hạn"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"缓存已过期"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"快取已過期"}}}}, + "cloudStartup.folderNotOpen": {"comment":"Agent pane: Cloud startup checklist. A Cloud start refused on this Mac because its folder is not one of the tab's folders.","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"This folder isn't open in this tab; pick it to send it to Cloud."}},"ar":{"stringUnit":{"state":"translated","value":"هذا المجلد غير مفتوح في علامة التبويب هذه؛ اختره لإرساله إلى Cloud."}},"bs":{"stringUnit":{"state":"translated","value":"Ovaj folder nije otvoren u ovoj kartici; odaberite ga da ga pošaljete u Cloud."}},"da":{"stringUnit":{"state":"translated","value":"Denne mappe er ikke åben i denne fane; vælg den for at sende den til Cloud."}},"de":{"stringUnit":{"state":"translated","value":"Dieser Ordner ist in diesem Tab nicht geöffnet; wähle ihn aus, um ihn an Cloud zu senden."}},"es":{"stringUnit":{"state":"translated","value":"Esta carpeta no está abierta en esta pestaña; elígela para enviarla a Cloud."}},"fr":{"stringUnit":{"state":"translated","value":"Ce dossier n'est pas ouvert dans cet onglet ; choisissez-le pour l'envoyer vers Cloud."}},"it":{"stringUnit":{"state":"translated","value":"Questa cartella non è aperta in questa scheda; sceglila per inviarla a Cloud."}},"ja":{"stringUnit":{"state":"translated","value":"このフォルダはこのタブで開かれていません。Cloud に送るにはフォルダを選んでください。"}},"km":{"stringUnit":{"state":"translated","value":"ថតនេះមិនបានបើកនៅក្នុងផ្ទាំងនេះទេ។ ជ្រើសរើសវាដើម្បីផ្ញើវាទៅ Cloud។"}},"ko":{"stringUnit":{"state":"translated","value":"이 폴더는 이 탭에서 열려 있지 않습니다. Cloud로 보내려면 폴더를 선택하세요."}},"nb":{"stringUnit":{"state":"translated","value":"Denne mappen er ikke åpen i denne fanen; velg den for å sende den til Cloud."}},"pl":{"stringUnit":{"state":"translated","value":"Ten folder nie jest otwarty w tej karcie; wybierz go, aby wysłać go do Cloud."}},"pt-BR":{"stringUnit":{"state":"translated","value":"Esta pasta não está aberta nesta aba; escolha-a para enviá-la ao Cloud."}},"ru":{"stringUnit":{"state":"translated","value":"Эта папка не открыта в этой вкладке; выберите её, чтобы отправить в Cloud."}},"th":{"stringUnit":{"state":"translated","value":"โฟลเดอร์นี้ไม่ได้เปิดอยู่ในแท็บนี้ เลือกโฟลเดอร์เพื่อส่งไปยัง Cloud"}},"tr":{"stringUnit":{"state":"translated","value":"Bu klasör bu sekmede açık değil; Cloud'a göndermek için klasörü seçin."}},"uk":{"stringUnit":{"state":"translated","value":"Ця папка не відкрита в цій вкладці; виберіть її, щоб надіслати в Cloud."}},"vi":{"stringUnit":{"state":"translated","value":"Thư mục này chưa được mở trong tab này; hãy chọn nó để gửi lên Cloud."}},"zh-Hans":{"stringUnit":{"state":"translated","value":"此文件夹未在此标签页中打开;请选择它以发送到 Cloud。"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"此資料夾未在此分頁中開啟;請選擇它以傳送到 Cloud。"}}}}, "cloudStartup.harnessStarting": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Starting {harness}"}},"ar":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"bs":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"da":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"de":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"es":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"fr":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"it":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"ja":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"km":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"ko":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"nb":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"pl":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"ru":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"th":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"tr":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"uk":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"vi":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Starting {harness}"}}}} }, "version": "1.0" diff --git a/webviews/src/agent-session/acpmux/cloudStartProgress.test.ts b/webviews/src/agent-session/acpmux/cloudStartProgress.test.ts index 884391b7c53f..7c8e5eff0d7b 100644 --- a/webviews/src/agent-session/acpmux/cloudStartProgress.test.ts +++ b/webviews/src/agent-session/acpmux/cloudStartProgress.test.ts @@ -1,6 +1,6 @@ // l10n-allow-file: test fixture prompts and host words, not shipped UI. import { expect, test } from "bun:test"; -import { cloudStartBegun, cloudStartEnded, cloudStartPushed } from "./cloudStartProgress"; +import { cloudRefusalText, cloudStartBegun, cloudStartEnded, cloudStartPushed } from "./cloudStartProgress"; const begun = () => cloudStartBegun("k-1", "fix the build", "/Users/me/src/paint/"); @@ -138,3 +138,20 @@ test("forwards the environment id and setup ran or cached state", () => { }); expect(cached?.setup).toEqual({ state: "done", status: "ready" }); }); + +test("a start this Mac's pane refused says why by its code, never the generic connection refusal", () => { + const generic = "The app refused the agent connection."; + const outside = cloudStartEnded(begun(), "k-1", { ok: false, code: "transport.path_outside_roots", message: generic }); + expect(outside).toEqual({ + ...begun(), + state: "failed", + message: "This folder isn't open in this tab; pick it to send it to Cloud.", + refusedHere: true, + }); + const gesture = cloudStartEnded(begun(), "k-1", { ok: false, code: "transport.gesture_required", message: generic }); + expect(gesture?.message).toBe(cloudRefusalText("transport.gesture_required")); + expect(gesture?.message).not.toBe(generic); + expect(gesture?.refusedHere).toBe(true); + // The App's own failure keeps the host's words and its checklist step. + expect(cloudStartEnded(begun(), "k-1", { ok: false, code: "cloud_start_failed", message: "No machine" })?.refusedHere).toBeUndefined(); +}); diff --git a/webviews/src/agent-session/acpmux/cloudStartProgress.ts b/webviews/src/agent-session/acpmux/cloudStartProgress.ts index 83e899c8e7b3..28a1d9a6d50f 100644 --- a/webviews/src/agent-session/acpmux/cloudStartProgress.ts +++ b/webviews/src/agent-session/acpmux/cloudStartProgress.ts @@ -1,4 +1,6 @@ import type { CloudStartAnswer } from "./cloudRun"; +import { translate } from "./i18n"; +import { errorMessage } from "./transportErrors"; import { sliceWhole } from "../../ui/wholeChars"; /// A chat starting in Cloud, as the chat shows it (CloudStartView; cloud work mode, leo-workspace @@ -56,6 +58,9 @@ export type CloudStartProgress = { setup?: CloudStartupSetup; /// True after the harness has started. The checklist stays available but folds to one quiet line. initialized?: boolean; + /// This Mac's pane refused the start before it reached Cloud (a `transport.*` code: a folder that + /// is not the tab's own, a Send without a gesture): no step ran, so the checklist marks none failed. + refusedHere?: boolean; }; export type CloudStartupOperation = { @@ -329,6 +334,17 @@ export function cloudStartEnded( ): CloudStartProgress | undefined { if (current?.key !== key) return current; if (answer.ok) return undefined; - const message = line(answer.message, 240); - return { ...current, state: "failed", ...(message ? { message } : {}) }; + // The pane's own refusal (AgentPaneCloudStart: the folder check, the gesture) is worded by its + // code, never the host's generic "The app refused the agent connection." (cloudRefusalText). + const refusedHere = answer.code.startsWith("transport."); + const message = refusedHere ? cloudRefusalText(answer.code) : line(answer.message, 240); + return { ...current, state: "failed", ...(message ? { message } : {}), ...(refusedHere ? { refusedHere } : {}) }; +} + +/// The words for a Cloud start this Mac's pane refused: a folder that is not one of the tab's own +/// asks the person to pick it (a pick grants it, `workspace.useFolder`); any other `transport.*` +/// code reads as the pane's refusal text (transportErrors.ts). +export function cloudRefusalText(code: string): string { + if (code === "transport.path_outside_roots") return translate("cloudStartup.folderNotOpen"); + return errorMessage({ code }); }