From 67e4654ac936ad9e8da764145a31ae3c052b6c72 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 07:54:50 +0000 Subject: [PATCH 01/13] Connect the GitHub App from Settings and shared account actions Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- Packages/macOS/CmuxNext/Package.swift | 3 +- .../GitHubConnections+Page.swift | 46 +++++ .../CmuxNextAccounts/GitHubConnections.swift | 123 ++++++++++++++ .../Resources/GitHubConnection.xcstrings | 18 ++ .../CmuxNextActions/AccountsActions.xcstrings | 132 ++++++++++++++- .../ActionSurfaceCatalog+Exemptions.swift | 4 +- .../ActionSurfaceCatalog+MenuExemptions.swift | 2 +- .../Catalog/AccountActionCatalog.swift | 7 +- .../Accounts/AccountsService.swift | 29 ++++ .../Handlers/AccountsHandlers.swift | 10 ++ .../CmuxNextApp/Pages/PageFactory.swift | 15 +- .../SettingsSchema+Feed.generated.swift | 4 +- .../GitHubConnectionsTests.swift | 159 ++++++++++++++++++ schemas/settings/source/feed.json | 8 +- .../settings/components/AccountsSection.tsx | 2 + .../components/GitHubAccountsCard.tsx | 88 ++++++++++ webviews/src/pages/settings/ops.ts | 14 ++ .../src/pages/settings/settings.gallery.ts | 77 +++++++++ 18 files changed, 728 insertions(+), 13 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/Resources/GitHubConnection.xcstrings create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift create mode 100644 webviews/src/pages/settings/components/GitHubAccountsCard.tsx diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index c955d5aa4bcb..d54d81d1993b 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -425,12 +425,13 @@ let package = Package( // supplies `AccountsServices`. .target( name: "CmuxNextAccounts", - dependencies: ["CmuxNextCloud", "CmuxNextCodeRouter", "CmuxNextDesign", "CmuxNextIcons", .product(name: "CmuxAgentBrands", package: "CmuxAgentBrands"), + dependencies: ["CmuxNextDaemon", "CmuxNextCloud", "CmuxNextCodeRouter", "CmuxNextDesign", "CmuxNextIcons", .product(name: "CmuxAgentBrands", package: "CmuxAgentBrands"), .product(name: "CMUXAuthCore", package: "CMUXAuthCore")], resources: [ .process("Localizable.xcstrings"), // BillingStrings (table Billing): moved from the App with Billing/ in B1. .process("Resources/Billing.xcstrings"), + .process("Resources/GitHubConnection.xcstrings"), ], swiftSettings: uiSwiftSettings ), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift new file mode 100644 index 000000000000..119aad14bbf8 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift @@ -0,0 +1,46 @@ +public import CmuxNextDaemon +import Foundation + +extension GitHubConnections { + public var pageState: JSONValue { + let note: String + if scopeIsSignedOut { note = Self.signIn } + else if error { note = Self.failed } + else if pending { note = Self.pendingText } + else if connected { note = Self.status("active") } + else if !ready, !busy { note = Self.unavailable } + else { note = Self.detail } + return [ + "outcome": .string(scopeIsSignedOut ? "needs-action" : error ? "failed" : (pending || busy) ? "pending" : connected ? "connected" : "needs-action"), + "title": "GitHub", "detail": .string(note), "busy": .bool(busy), + "connect": .string(Self.connect), "disconnect": .string(Self.disconnect), + "confirm": .string(Self.confirm), "cancel": .string(Self.cancel), + "refresh": .string(Self.refresh), "canConnect": .bool(ready && !busy && !connected), + "connections": .array(currentConnections.map { row in + ["id": row["id"] ?? .null, + "account": row["account"]?["name"] ?? "GitHub", + "status": .string(Self.status(row["status"]?.stringValue)), + "canDisconnect": .bool(canDisconnect(row)), + "permissions": row["scopes_granted"] ?? .array([])] + }), + ] + } +} + +private extension GitHubConnections { + static var connect: String { String(localized: "github.connect", defaultValue: "Connect GitHub", table: "GitHubConnection", bundle: .module) } + static var disconnect: String { String(localized: "github.disconnect", defaultValue: "Disconnect", table: "GitHubConnection", bundle: .module) } + static var confirm: String { String(localized: "github.confirm", defaultValue: "Disconnect GitHub? Agents will lose access through this connection.", table: "GitHubConnection", bundle: .module) } + static var cancel: String { String(localized: "github.cancel", defaultValue: "Cancel", table: "GitHubConnection", bundle: .module) } + static var refresh: String { String(localized: "github.refresh", defaultValue: "Refresh", table: "GitHubConnection", bundle: .module) } + static var detail: String { String(localized: "github.detail", defaultValue: "Connect the cmux GitHub App. Choose repositories and review permissions in GitHub.", table: "GitHubConnection", bundle: .module) } + static var signIn: String { String(localized: "github.signIn", defaultValue: "Sign in to cmux to connect GitHub.", table: "GitHubConnection", bundle: .module) } + static var unavailable: String { String(localized: "github.unavailable", defaultValue: "GitHub connections are unavailable on this deployment.", table: "GitHubConnection", bundle: .module) } + static var pendingText: String { String(localized: "github.pending", defaultValue: "Finish connecting in your browser, then refresh.", table: "GitHubConnection", bundle: .module) } + static var failed: String { String(localized: "github.failed", defaultValue: "GitHub could not be updated. Refresh to try again.", table: "GitHubConnection", bundle: .module) } + static func status(_ status: String?) -> String { + if status == "active" { return String(localized: "github.connected", defaultValue: "Connected", table: "GitHubConnection", bundle: .module) } + if status == "pending" { return pendingText } + return String(localized: "github.reconnect", defaultValue: "Connect GitHub again to restore access.", table: "GitHubConnection", bundle: .module) + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift new file mode 100644 index 000000000000..bb6543fa097f --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -0,0 +1,123 @@ +public import CmuxNextDaemon +import CryptoKit +public import Foundation +import Observation + +/// The GitHub App connection for the account and team selected in Settings. +/// Calls the existing owner API as the person; never widens the app credential relay. +@MainActor @Observable +public final class GitHubConnections { + public struct Scope: Hashable, Sendable { + public let user: String + public let team: String? + public init(user: String, team: String?) { self.user = user; self.team = team } + } + public typealias Call = @MainActor (Scope, String, JSONValue, String?) async throws -> JSONValue + @ObservationIgnored private let scope: () -> Scope? + @ObservationIgnored private let call: Call + @ObservationIgnored private let open: (URL) -> Void + @ObservationIgnored private var connectKeys: [Scope: String] = [:] + private var loadedScope: Scope? + private var workingScope: Scope? + private var generation = 0 + @ObservationIgnored private var authorizations: [Scope: (id: String, url: URL)] = [:] + private(set) var connections: [JSONValue] = [] + private(set) var configured = false + private var errorScope: Scope? + public var error: Bool { scope() != nil && errorScope == scope() } + public var pending: Bool { currentConnections.contains { $0["status"]?.stringValue == "pending" } || scope().map { authorizations[$0] != nil } == true } + + public init(scope: @escaping () -> Scope?, call: @escaping Call, open: @escaping (URL) -> Void) { + self.scope = scope + self.call = call + self.open = open + } + + public var scopeIsSignedOut: Bool { scope() == nil } + public var busy: Bool { workingScope != nil && workingScope == scope() } + public var currentConnections: [JSONValue] { loadedScope == scope() ? connections : [] } + public var connected: Bool { currentConnections.contains { $0["status"]?.stringValue == "active" && canDisconnect($0) } } + public var ready: Bool { scope() != nil && loadedScope == scope() && configured } + + public func canDisconnect(_ row: JSONValue) -> Bool { + scope() != nil && row["created_by"]?.stringValue == scope()?.user + } + + /// One shared entry point for Settings, the Setup Guide, the palette and CLI. + public func connect() async { + let bound = scope() + await perform("githubRefresh") + guard scope() == bound, !error else { return } + if connected { return } + await perform("githubConnect") + } + + public func perform(_ action: String, connection: String? = nil) async { + guard let bound = scope(), workingScope != bound else { return } + generation += 1 + let ticket = generation + workingScope = bound + errorScope = nil + defer { if generation == ticket { workingScope = nil } } + do { + switch action { + case "githubConnect": + guard ready else { return } + if let authorization = authorizations[bound] { + open(authorization.url) + } else { + if connectKeys[bound] == nil { connectKeys[bound] = UUID().uuidString } + let value = try await call(bound, "integration.connect", ["provider": "github"], connectKeys[bound]) + guard scope() == bound, generation == ticket else { return } + guard let raw = value["authorize_url"]?.stringValue, let url = Self.installationURL(raw), + let id = value["connection"]?["id"]?.stringValue, !id.isEmpty else { throw GitHubFailure.invalidReply } + authorizations[bound] = (id, url) + open(url) + } + case "githubDisconnect": + guard let connection, currentConnections.contains(where: { $0["id"]?.stringValue == connection && canDisconnect($0) }) else { return } + _ = try await call(bound, "integration.revoke", ["connection": .string(connection)], UUID().uuidString) + default: break + } + // The same list after every gesture, and on Settings focus. No polling loop. + let value = try await call(bound, "integration.list", [:], nil) + guard scope() == bound, generation == ticket else { return } + guard let rows = value["connections"]?.arrayValue, let providers = value["providers"]?.arrayValue else { + throw GitHubFailure.invalidReply + } + connections = rows.filter { $0["provider"]?.stringValue == "github" && !["revoked", "expired"].contains($0["status"]?.stringValue ?? "") } + configured = providers.contains { $0["provider"]?.stringValue == "github" && $0["configured"]?.boolValue == true } + loadedScope = bound + if let authorization = authorizations[bound], + !connections.contains(where: { $0["id"]?.stringValue == authorization.id && $0["status"]?.stringValue == "pending" }) { + authorizations[bound] = nil + connectKeys[bound] = nil + } + } catch { + if scope() == bound, generation == ticket { errorScope = bound } + } + } + + /// Only the App installation page returned by integration.connect may open. + public nonisolated static func installationURL(_ raw: String) -> URL? { + guard let url = URL(string: raw), url.scheme == "https", url.host == "github.com", + url.user == nil, url.password == nil, url.port == nil, + url.path.range(of: #"^/apps/[A-Za-z0-9-]+/installations/new$"#, options: .regularExpression) != nil else { return nil } + return url + } + + /// The existing API identity contract (`domains/user.ts::stackTeamIdFor`). + public nonisolated static func backendTeam(project: String, stackTeam: String?) -> String? { + guard let stackTeam else { return nil } + let team = UUID(uuidString: stackTeam) == nil ? stackTeam : stackTeam.lowercased() + let digest = SHA256.hash(data: Data("stack-team:\(project):\(team)".utf8)) + return "team_" + digest.prefix(10).map { String(format: "%02x", $0) }.joined() + } + + public nonisolated static func backendUser(project: String, stackUser: String) -> String { + let digest = SHA256.hash(data: Data("stack:\(project):\(stackUser)".utf8)) + return "user_" + digest.prefix(10).map { String(format: "%02x", $0) }.joined() + } + + public enum GitHubFailure: Error { case changedAccount, invalidReply, refused } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/Resources/GitHubConnection.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/Resources/GitHubConnection.xcstrings new file mode 100644 index 000000000000..991a2fbe92e6 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/Resources/GitHubConnection.xcstrings @@ -0,0 +1,18 @@ +{ + "sourceLanguage": "en", + "strings": { + "github.connect": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Connect GitHub"}}, "de": {"stringUnit": {"state": "translated", "value": "GitHub verbinden"}}, "fr": {"stringUnit": {"state": "translated", "value": "Connecter GitHub"}}, "ar": {"stringUnit": {"state": "translated", "value": "ربط GitHub"}}, "es": {"stringUnit": {"state": "translated", "value": "Conectar GitHub"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "連接 GitHub"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "连接 GitHub"}}, "ko": {"stringUnit": {"state": "translated", "value": "GitHub 연결"}}, "ja": {"stringUnit": {"state": "translated", "value": "GitHub に接続"}}, "bs": {"stringUnit": {"state": "translated", "value": "Poveži GitHub"}}, "da": {"stringUnit": {"state": "translated", "value": "Forbind GitHub"}}, "it": {"stringUnit": {"state": "translated", "value": "Connetti GitHub"}}, "km": {"stringUnit": {"state": "translated", "value": "ភ្ជាប់ GitHub"}}, "nb": {"stringUnit": {"state": "translated", "value": "Koble til GitHub"}}, "pl": {"stringUnit": {"state": "translated", "value": "Połącz z GitHub"}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Conectar ao GitHub"}}, "ru": {"stringUnit": {"state": "translated", "value": "Подключить GitHub"}}, "th": {"stringUnit": {"state": "translated", "value": "เชื่อมต่อ GitHub"}}, "tr": {"stringUnit": {"state": "translated", "value": "GitHub'a bağlan"}}, "uk": {"stringUnit": {"state": "translated", "value": "Підключити GitHub"}}, "vi": {"stringUnit": {"state": "translated", "value": "Kết nối GitHub"}}}}, + "github.disconnect": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Disconnect"}}, "de": {"stringUnit": {"state": "translated", "value": "Trennen"}}, "fr": {"stringUnit": {"state": "translated", "value": "Déconnecter"}}, "ar": {"stringUnit": {"state": "translated", "value": "قطع الاتصال"}}, "es": {"stringUnit": {"state": "translated", "value": "Desconectar"}}, "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": "Prekini vezu"}}, "da": {"stringUnit": {"state": "translated", "value": "Afbryd forbindelsen"}}, "it": {"stringUnit": {"state": "translated", "value": "Disconnetti"}}, "km": {"stringUnit": {"state": "translated", "value": "ផ្តាច់ការភ្ជាប់"}}, "nb": {"stringUnit": {"state": "translated", "value": "Koble fra"}}, "pl": {"stringUnit": {"state": "translated", "value": "Rozłącz"}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Desconectar"}}, "ru": {"stringUnit": {"state": "translated", "value": "Отключить"}}, "th": {"stringUnit": {"state": "translated", "value": "ยกเลิกการเชื่อมต่อ"}}, "tr": {"stringUnit": {"state": "translated", "value": "Bağlantıyı kes"}}, "uk": {"stringUnit": {"state": "translated", "value": "Від’єднати"}}, "vi": {"stringUnit": {"state": "translated", "value": "Ngắt kết nối"}}}}, + "github.confirm": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Disconnect GitHub? Agents will lose access through this connection."}}, "de": {"stringUnit": {"state": "translated", "value": "GitHub trennen? Agenten verlieren den Zugriff über diese Verbindung."}}, "fr": {"stringUnit": {"state": "translated", "value": "Déconnecter GitHub ? Les agents perdront leur accès via cette connexion."}}, "ar": {"stringUnit": {"state": "translated", "value": "هل تريد قطع الاتصال بـ GitHub؟ سيفقد الوكلاء الوصول عبر هذا الاتصال."}}, "es": {"stringUnit": {"state": "translated", "value": "¿Desconectar GitHub? Los agentes perderán el acceso a través de esta conexión."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "要中斷 GitHub 連線嗎?代理程式將失去透過此連線的存取權。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "要断开 GitHub 连接吗?代理将失去通过此连接的访问权限。"}}, "ko": {"stringUnit": {"state": "translated", "value": "GitHub 연결을 해제할까요? 에이전트가 이 연결을 통한 액세스 권한을 잃습니다."}}, "ja": {"stringUnit": {"state": "translated", "value": "GitHub との接続を解除しますか?エージェントはこの接続を介してアクセスできなくなります。"}}, "bs": {"stringUnit": {"state": "translated", "value": "Prekinuti vezu s GitHubom? Agenti će izgubiti pristup putem ove veze."}}, "da": {"stringUnit": {"state": "translated", "value": "Vil du afbryde forbindelsen til GitHub? Agenter mister adgangen via denne forbindelse."}}, "it": {"stringUnit": {"state": "translated", "value": "Disconnettere GitHub? Gli agenti perderanno l'accesso tramite questa connessione."}}, "km": {"stringUnit": {"state": "translated", "value": "ផ្តាច់ការភ្ជាប់ GitHub? ភ្នាក់ងារនឹងបាត់បង់សិទ្ធិចូលប្រើតាមរយៈការភ្ជាប់នេះ។"}}, "nb": {"stringUnit": {"state": "translated", "value": "Koble fra GitHub? Agenter mister tilgang via denne tilkoblingen."}}, "pl": {"stringUnit": {"state": "translated", "value": "Rozłączyć GitHub? Agenci utracą dostęp przez to połączenie."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Desconectar do GitHub? Os agentes perderão o acesso por esta conexão."}}, "ru": {"stringUnit": {"state": "translated", "value": "Отключить GitHub? Агенты потеряют доступ через это подключение."}}, "th": {"stringUnit": {"state": "translated", "value": "ยกเลิกการเชื่อมต่อ GitHub ไหม นักพัฒนาจะเสียสิทธิ์เข้าถึงผ่านการเชื่อมต่อนี้"}}, "tr": {"stringUnit": {"state": "translated", "value": "GitHub bağlantısı kesilsin mi? Aracılar bu bağlantı üzerinden erişimi kaybedecek."}}, "uk": {"stringUnit": {"state": "translated", "value": "Від’єднати GitHub? Агенти втратять доступ через це підключення."}}, "vi": {"stringUnit": {"state": "translated", "value": "Ngắt kết nối GitHub? Các tác nhân sẽ mất quyền truy cập qua kết nối này."}}}}, + "github.cancel": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Cancel"}}, "de": {"stringUnit": {"state": "translated", "value": "Abbrechen"}}, "fr": {"stringUnit": {"state": "translated", "value": "Annuler"}}, "ar": {"stringUnit": {"state": "translated", "value": "إلغاء"}}, "es": {"stringUnit": {"state": "translated", "value": "Cancelar"}}, "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": "Otkaži"}}, "da": {"stringUnit": {"state": "translated", "value": "Annuller"}}, "it": {"stringUnit": {"state": "translated", "value": "Annulla"}}, "km": {"stringUnit": {"state": "translated", "value": "បោះបង់"}}, "nb": {"stringUnit": {"state": "translated", "value": "Avbryt"}}, "pl": {"stringUnit": {"state": "translated", "value": "Anuluj"}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Cancelar"}}, "ru": {"stringUnit": {"state": "translated", "value": "Отмена"}}, "th": {"stringUnit": {"state": "translated", "value": "ยกเลิก"}}, "tr": {"stringUnit": {"state": "translated", "value": "İptal"}}, "uk": {"stringUnit": {"state": "translated", "value": "Скасувати"}}, "vi": {"stringUnit": {"state": "translated", "value": "Hủy"}}}}, + "github.refresh": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Refresh"}}, "de": {"stringUnit": {"state": "translated", "value": "Aktualisieren"}}, "fr": {"stringUnit": {"state": "translated", "value": "Actualiser"}}, "ar": {"stringUnit": {"state": "translated", "value": "تحديث"}}, "es": {"stringUnit": {"state": "translated", "value": "Actualizar"}}, "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": "Osvježi"}}, "da": {"stringUnit": {"state": "translated", "value": "Opdater"}}, "it": {"stringUnit": {"state": "translated", "value": "Aggiorna"}}, "km": {"stringUnit": {"state": "translated", "value": "ផ្ទុកឡើងវិញ"}}, "nb": {"stringUnit": {"state": "translated", "value": "Oppdater"}}, "pl": {"stringUnit": {"state": "translated", "value": "Odśwież"}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Atualizar"}}, "ru": {"stringUnit": {"state": "translated", "value": "Обновить"}}, "th": {"stringUnit": {"state": "translated", "value": "รีเฟรช"}}, "tr": {"stringUnit": {"state": "translated", "value": "Yenile"}}, "uk": {"stringUnit": {"state": "translated", "value": "Оновити"}}, "vi": {"stringUnit": {"state": "translated", "value": "Làm mới"}}}}, + "github.detail": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Connect the cmux GitHub App. Choose repositories and review permissions in GitHub."}}, "de": {"stringUnit": {"state": "translated", "value": "Verbinde die cmux-GitHub-App. Wähle Repositorys und prüfe die Berechtigungen in GitHub."}}, "fr": {"stringUnit": {"state": "translated", "value": "Connectez l’application GitHub cmux. Choisissez les dépôts et vérifiez les autorisations dans GitHub."}}, "ar": {"stringUnit": {"state": "translated", "value": "اربط تطبيق cmux على GitHub. اختر المستودعات وراجع الأذونات في GitHub."}}, "es": {"stringUnit": {"state": "translated", "value": "Conecta la aplicación de GitHub de cmux. Elige los repositorios y revisa los permisos en GitHub."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "連接 cmux GitHub 應用程式。在 GitHub 中選擇儲存庫並檢查權限。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "连接 cmux GitHub 应用。在 GitHub 中选择仓库并检查权限。"}}, "ko": {"stringUnit": {"state": "translated", "value": "cmux GitHub 앱을 연결하세요. GitHub에서 저장소를 선택하고 권한을 검토하세요."}}, "ja": {"stringUnit": {"state": "translated", "value": "cmux GitHub App に接続します。GitHub でリポジトリを選び、権限を確認してください。"}}, "bs": {"stringUnit": {"state": "translated", "value": "Poveži cmux GitHub aplikaciju. Odaberi repozitorije i pregledaj dozvole na GitHubu."}}, "da": {"stringUnit": {"state": "translated", "value": "Forbind cmux GitHub-appen. Vælg repositories, og gennemgå tilladelserne på GitHub."}}, "it": {"stringUnit": {"state": "translated", "value": "Connetti l'app GitHub di cmux. Scegli i repository e controlla le autorizzazioni su GitHub."}}, "km": {"stringUnit": {"state": "translated", "value": "ភ្ជាប់កម្មវិធី GitHub របស់ cmux។ ជ្រើសរើសឃ្លាំង និងពិនិត្យសិទ្ធិនៅលើ GitHub។"}}, "nb": {"stringUnit": {"state": "translated", "value": "Koble til cmux GitHub-appen. Velg repositorier og gå gjennom tillatelser på GitHub."}}, "pl": {"stringUnit": {"state": "translated", "value": "Połącz aplikację GitHub cmux. Wybierz repozytoria i sprawdź uprawnienia w GitHub."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Conecte o app do GitHub do cmux. Escolha os repositórios e revise as permissões no GitHub."}}, "ru": {"stringUnit": {"state": "translated", "value": "Подключите приложение cmux для GitHub. Выберите репозитории и проверьте разрешения в GitHub."}}, "th": {"stringUnit": {"state": "translated", "value": "เชื่อมต่อแอป GitHub ของ cmux เลือกที่เก็บและตรวจสอบสิทธิ์ใน GitHub"}}, "tr": {"stringUnit": {"state": "translated", "value": "cmux GitHub uygulamasına bağlanın. Depoları seçin ve GitHub'daki izinleri inceleyin."}}, "uk": {"stringUnit": {"state": "translated", "value": "Підключіть застосунок cmux для GitHub. Виберіть репозиторії та перегляньте дозволи на GitHub."}}, "vi": {"stringUnit": {"state": "translated", "value": "Kết nối ứng dụng GitHub của cmux. Chọn kho lưu trữ và xem lại quyền trên GitHub."}}}}, + "github.signIn": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Sign in to cmux to connect GitHub."}}, "de": {"stringUnit": {"state": "translated", "value": "Melde dich bei cmux an, um GitHub zu verbinden."}}, "fr": {"stringUnit": {"state": "translated", "value": "Connectez-vous à cmux pour connecter GitHub."}}, "ar": {"stringUnit": {"state": "translated", "value": "سجّل الدخول إلى cmux لربط GitHub."}}, "es": {"stringUnit": {"state": "translated", "value": "Inicia sesión en cmux para conectar GitHub."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "登入 cmux 以連接 GitHub。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "登录 cmux 以连接 GitHub。"}}, "ko": {"stringUnit": {"state": "translated", "value": "GitHub를 연결하려면 cmux에 로그인하세요."}}, "ja": {"stringUnit": {"state": "translated", "value": "GitHub に接続するには cmux にサインインしてください。"}}, "bs": {"stringUnit": {"state": "translated", "value": "Prijavi se u cmux da povežeš GitHub."}}, "da": {"stringUnit": {"state": "translated", "value": "Log ind på cmux for at forbinde GitHub."}}, "it": {"stringUnit": {"state": "translated", "value": "Accedi a cmux per connettere GitHub."}}, "km": {"stringUnit": {"state": "translated", "value": "ចូល cmux ដើម្បីភ្ជាប់ GitHub។"}}, "nb": {"stringUnit": {"state": "translated", "value": "Logg inn på cmux for å koble til GitHub."}}, "pl": {"stringUnit": {"state": "translated", "value": "Zaloguj się do cmux, aby połączyć GitHub."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Entre no cmux para conectar ao GitHub."}}, "ru": {"stringUnit": {"state": "translated", "value": "Войдите в cmux, чтобы подключить GitHub."}}, "th": {"stringUnit": {"state": "translated", "value": "ลงชื่อเข้าใช้ cmux เพื่อเชื่อมต่อ GitHub"}}, "tr": {"stringUnit": {"state": "translated", "value": "GitHub'a bağlanmak için cmux'ta oturum açın."}}, "uk": {"stringUnit": {"state": "translated", "value": "Увійдіть у cmux, щоб підключити GitHub."}}, "vi": {"stringUnit": {"state": "translated", "value": "Đăng nhập cmux để kết nối GitHub."}}}}, + "github.unavailable": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "GitHub connections are unavailable on this deployment."}}, "de": {"stringUnit": {"state": "translated", "value": "GitHub-Verbindungen sind in dieser Bereitstellung nicht verfügbar."}}, "fr": {"stringUnit": {"state": "translated", "value": "Les connexions GitHub ne sont pas disponibles sur ce déploiement."}}, "ar": {"stringUnit": {"state": "translated", "value": "اتصالات GitHub غير متاحة في هذا النشر."}}, "es": {"stringUnit": {"state": "translated", "value": "Las conexiones de GitHub no están disponibles en este despliegue."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "此部署無法使用 GitHub 連線。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "此部署无法使用 GitHub 连接。"}}, "ko": {"stringUnit": {"state": "translated", "value": "이 배포에서는 GitHub 연결을 사용할 수 없습니다."}}, "ja": {"stringUnit": {"state": "translated", "value": "このデプロイでは GitHub に接続できません。"}}, "bs": {"stringUnit": {"state": "translated", "value": "GitHub veze nisu dostupne u ovoj instalaciji."}}, "da": {"stringUnit": {"state": "translated", "value": "GitHub-forbindelser er ikke tilgængelige i denne installation."}}, "it": {"stringUnit": {"state": "translated", "value": "Le connessioni GitHub non sono disponibili in questa distribuzione."}}, "km": {"stringUnit": {"state": "translated", "value": "ការភ្ជាប់ GitHub មិនអាចប្រើបាននៅលើការដាក់ពង្រាយនេះទេ។"}}, "nb": {"stringUnit": {"state": "translated", "value": "GitHub-tilkoblinger er utilgjengelige i denne distribusjonen."}}, "pl": {"stringUnit": {"state": "translated", "value": "Połączenia z GitHub są niedostępne w tym wdrożeniu."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "As conexões do GitHub não estão disponíveis nesta implantação."}}, "ru": {"stringUnit": {"state": "translated", "value": "Подключения к GitHub недоступны в этом развёртывании."}}, "th": {"stringUnit": {"state": "translated", "value": "การเชื่อมต่อ GitHub ไม่พร้อมใช้งานในการปรับใช้นี้"}}, "tr": {"stringUnit": {"state": "translated", "value": "GitHub bağlantıları bu dağıtımda kullanılamıyor."}}, "uk": {"stringUnit": {"state": "translated", "value": "Підключення до GitHub недоступні в цьому розгортанні."}}, "vi": {"stringUnit": {"state": "translated", "value": "Các kết nối GitHub không khả dụng trong bản triển khai này."}}}}, + "github.pending": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Finish connecting in your browser, then refresh."}}, "de": {"stringUnit": {"state": "translated", "value": "Schließe die Verbindung im Browser ab und aktualisiere dann."}}, "fr": {"stringUnit": {"state": "translated", "value": "Terminez la connexion dans votre navigateur, puis actualisez."}}, "ar": {"stringUnit": {"state": "translated", "value": "أكمل الربط في المتصفح، ثم حدّث."}}, "es": {"stringUnit": {"state": "translated", "value": "Completa la conexión en tu navegador y luego actualiza."}}, "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": "Dovrši povezivanje u pregledniku, a zatim osvježi."}}, "da": {"stringUnit": {"state": "translated", "value": "Færdiggør forbindelsen i din browser, og opdater derefter."}}, "it": {"stringUnit": {"state": "translated", "value": "Completa la connessione nel browser, poi aggiorna."}}, "km": {"stringUnit": {"state": "translated", "value": "បញ្ចប់ការភ្ជាប់នៅក្នុងកម្មវិធីរុករក រួចផ្ទុកឡើងវិញ។"}}, "nb": {"stringUnit": {"state": "translated", "value": "Fullfør tilkoblingen i nettleseren, og oppdater deretter."}}, "pl": {"stringUnit": {"state": "translated", "value": "Dokończ łączenie w przeglądarce, a następnie odśwież."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Conclua a conexão no navegador e depois atualize."}}, "ru": {"stringUnit": {"state": "translated", "value": "Завершите подключение в браузере, затем обновите."}}, "th": {"stringUnit": {"state": "translated", "value": "เชื่อมต่อให้เสร็จในเบราว์เซอร์ แล้วรีเฟรช"}}, "tr": {"stringUnit": {"state": "translated", "value": "Tarayıcınızda bağlantıyı tamamlayın, ardından yenileyin."}}, "uk": {"stringUnit": {"state": "translated", "value": "Завершіть підключення у браузері, потім оновіть."}}, "vi": {"stringUnit": {"state": "translated", "value": "Hoàn tất kết nối trong trình duyệt, sau đó làm mới."}}}}, + "github.failed": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "GitHub could not be updated. Refresh to try again."}}, "de": {"stringUnit": {"state": "translated", "value": "GitHub konnte nicht aktualisiert werden. Aktualisiere, um es erneut zu versuchen."}}, "fr": {"stringUnit": {"state": "translated", "value": "Impossible d’actualiser GitHub. Actualisez pour réessayer."}}, "ar": {"stringUnit": {"state": "translated", "value": "تعذّر تحديث GitHub. حدّث للمحاولة مجددًا."}}, "es": {"stringUnit": {"state": "translated", "value": "No se pudo actualizar GitHub. Actualiza para volver a intentarlo."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "無法更新 GitHub。請重新整理以重試。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "无法更新 GitHub。请刷新以重试。"}}, "ko": {"stringUnit": {"state": "translated", "value": "GitHub를 업데이트하지 못했습니다. 새로 고침하여 다시 시도하세요."}}, "ja": {"stringUnit": {"state": "translated", "value": "GitHub を更新できませんでした。更新してもう一度お試しください。"}}, "bs": {"stringUnit": {"state": "translated", "value": "GitHub nije moguće ažurirati. Osvježi i pokušaj ponovo."}}, "da": {"stringUnit": {"state": "translated", "value": "GitHub kunne ikke opdateres. Opdater for at prøve igen."}}, "it": {"stringUnit": {"state": "translated", "value": "Impossibile aggiornare GitHub. Aggiorna per riprovare."}}, "km": {"stringUnit": {"state": "translated", "value": "មិនអាចធ្វើបច្ចុប្បន្នភាព GitHub បានទេ។ ផ្ទុកឡើងវិញដើម្បីព្យាយាមម្ដងទៀត។"}}, "nb": {"stringUnit": {"state": "translated", "value": "GitHub kunne ikke oppdateres. Oppdater for å prøve på nytt."}}, "pl": {"stringUnit": {"state": "translated", "value": "Nie udało się zaktualizować GitHub. Odśwież, aby spróbować ponownie."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Não foi possível atualizar o GitHub. Atualize para tentar novamente."}}, "ru": {"stringUnit": {"state": "translated", "value": "Не удалось обновить GitHub. Обновите и попробуйте снова."}}, "th": {"stringUnit": {"state": "translated", "value": "อัปเดต GitHub ไม่ได้ รีเฟรชเพื่อลองอีกครั้ง"}}, "tr": {"stringUnit": {"state": "translated", "value": "GitHub güncellenemedi. Yenileyerek tekrar deneyin."}}, "uk": {"stringUnit": {"state": "translated", "value": "Не вдалося оновити GitHub. Оновіть і спробуйте ще раз."}}, "vi": {"stringUnit": {"state": "translated", "value": "Không thể cập nhật GitHub. Hãy làm mới để thử lại."}}}}, + "github.connected": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Connected"}}, "de": {"stringUnit": {"state": "translated", "value": "Verbunden"}}, "fr": {"stringUnit": {"state": "translated", "value": "Connecté"}}, "ar": {"stringUnit": {"state": "translated", "value": "متصل"}}, "es": {"stringUnit": {"state": "translated", "value": "Conectado"}}, "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": "Povezano"}}, "da": {"stringUnit": {"state": "translated", "value": "Forbundet"}}, "it": {"stringUnit": {"state": "translated", "value": "Connesso"}}, "km": {"stringUnit": {"state": "translated", "value": "បានភ្ជាប់"}}, "nb": {"stringUnit": {"state": "translated", "value": "Tilkoblet"}}, "pl": {"stringUnit": {"state": "translated", "value": "Połączono"}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Conectado"}}, "ru": {"stringUnit": {"state": "translated", "value": "Подключено"}}, "th": {"stringUnit": {"state": "translated", "value": "เชื่อมต่อแล้ว"}}, "tr": {"stringUnit": {"state": "translated", "value": "Bağlandı"}}, "uk": {"stringUnit": {"state": "translated", "value": "Підключено"}}, "vi": {"stringUnit": {"state": "translated", "value": "Đã kết nối"}}}}, + "github.reconnect": {"localizations": {"en": {"stringUnit": {"state": "translated", "value": "Connect GitHub again to restore access."}}, "de": {"stringUnit": {"state": "translated", "value": "Verbinde GitHub erneut, um den Zugriff wiederherzustellen."}}, "fr": {"stringUnit": {"state": "translated", "value": "Reconnectez GitHub pour rétablir l’accès."}}, "ar": {"stringUnit": {"state": "translated", "value": "اربط GitHub مجددًا لاستعادة الوصول."}}, "es": {"stringUnit": {"state": "translated", "value": "Vuelve a conectar GitHub para restaurar el acceso."}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "重新連接 GitHub 以恢復存取權。"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "重新连接 GitHub 以恢复访问权限。"}}, "ko": {"stringUnit": {"state": "translated", "value": "액세스를 복원하려면 GitHub를 다시 연결하세요."}}, "ja": {"stringUnit": {"state": "translated", "value": "アクセスを復元するには GitHub に再接続してください。"}}, "bs": {"stringUnit": {"state": "translated", "value": "Ponovo poveži GitHub da vratiš pristup."}}, "da": {"stringUnit": {"state": "translated", "value": "Forbind GitHub igen for at gendanne adgangen."}}, "it": {"stringUnit": {"state": "translated", "value": "Connetti nuovamente GitHub per ripristinare l'accesso."}}, "km": {"stringUnit": {"state": "translated", "value": "ភ្ជាប់ GitHub ម្តងទៀត ដើម្បីស្ដារសិទ្ធិចូលប្រើ។"}}, "nb": {"stringUnit": {"state": "translated", "value": "Koble til GitHub på nytt for å gjenopprette tilgang."}}, "pl": {"stringUnit": {"state": "translated", "value": "Połącz GitHub ponownie, aby przywrócić dostęp."}}, "pt-BR": {"stringUnit": {"state": "translated", "value": "Conecte o GitHub novamente para restaurar o acesso."}}, "ru": {"stringUnit": {"state": "translated", "value": "Подключите GitHub снова, чтобы восстановить доступ."}}, "th": {"stringUnit": {"state": "translated", "value": "เชื่อมต่อ GitHub อีกครั้งเพื่อกู้คืนสิทธิ์เข้าถึง"}}, "tr": {"stringUnit": {"state": "translated", "value": "Erişimi geri yüklemek için GitHub'a yeniden bağlanın."}}, "uk": {"stringUnit": {"state": "translated", "value": "Підключіть GitHub знову, щоб відновити доступ."}}, "vi": {"stringUnit": {"state": "translated", "value": "Kết nối lại GitHub để khôi phục quyền truy cập."}}}} + }, + "version": "1.0" +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/AccountsActions.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/AccountsActions.xcstrings index 01f902d0ce18..d69708314e64 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/AccountsActions.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/AccountsActions.xcstrings @@ -917,7 +917,137 @@ } } } + }, + "action.accounts.githubConnect": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connect GitHub" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GitHub verbinden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecter GitHub" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ربط GitHub" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectar GitHub" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "連接 GitHub" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "连接 GitHub" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "GitHub 연결" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "GitHub に接続" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Poveži GitHub" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Forbind GitHub" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Connetti GitHub" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ភ្ជាប់ GitHub" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Koble til GitHub" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Połącz z GitHub" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conectar ao GitHub" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Подключить GitHub" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เชื่อมต่อ GitHub" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "GitHub'a bağlan" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Підключити GitHub" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối GitHub" + } + } + } } }, "version": "1.0" -} \ No newline at end of file +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+Exemptions.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+Exemptions.swift index 9d248d826c35..964b239d1a7f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+Exemptions.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+Exemptions.swift @@ -104,7 +104,7 @@ nonisolated extension ActionSurfaceCatalog { "cloudTunnelAttach", "cloudTunnelDetach", "cloudTunnelRotateKey", "cloudNetworkList", "cloudFirewallList", "cloudFirewallGet", "cloudFirewallCreate", "cloudFirewallDelete", "cloudNewTerminal", "cloudRenameMachine", "cloudKillMachine", "cloudResizeMachine", "cloudDiagnostics", "cloudPauseMachine", "cloudResumeMachine", "cloudSetIdlePause", "cloudDomainList", "cloudPublicationList", "cloudListMachines", "palette.cloud.deleteSnapshot", "palette.auth.signIn", "palette.auth.signOut", "accounts.show", "palette.mobileConnect", "accounts.refresh", - "accounts.reauthenticate", "accounts.connect", "accounts.remove", "remote.connect", "remote.newGroup", "remote.removeGroup", "machine.reconnect", "remote.newWorkspace", + "accounts.reauthenticate", "accounts.connect", "accounts.remove", "accounts.githubConnect", "remote.connect", "remote.newGroup", "remote.removeGroup", "machine.reconnect", "remote.newWorkspace", "remote.openTerminalHere", "remote.reconnect", "remote.disconnect", "remote.install", "remote.forget", "device.add", "machine.add", "machine.doctor", "reloadConfiguration", "palette.makeDefaultBrowser", "palette.makeDefaultTerminal", "palette.toggleSetting", @@ -328,7 +328,7 @@ nonisolated extension ActionSurfaceCatalog { ], .credentials: [ "palette.auth.signIn", "palette.auth.signOut", "accounts.reauthenticate", "accounts.connect", - "accounts.remove", + "accounts.remove", "accounts.githubConnect", ], .endsApp: [ "quit", "quitKeepSessions", "quitEndSessions", "quitEndEverything", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+MenuExemptions.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+MenuExemptions.swift index 1f3ad04d639d..74b5717b4e73 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+MenuExemptions.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionSurfaceCatalog+MenuExemptions.swift @@ -44,7 +44,7 @@ nonisolated extension ActionSurfaceCatalog { "palette.computerUse.accessibility", "palette.computerUse.screenRecording", "computerUseStop", "newCloudMachine", "cloudDiagnostics", "openTeamPicker", "palette.auth.signIn", "palette.auth.signOut", "palette.mobileConnect", "accounts.show", "accounts.refresh", "accounts.reauthenticate", - "accounts.connect", "accounts.remove", "reloadConfiguration", "palette.openCmuxSettingsFile", + "accounts.connect", "accounts.remove", "accounts.githubConnect", "reloadConfiguration", "palette.openCmuxSettingsFile", "palette.openGhosttySettings", "ghostty.showDiagnostics", "palette.makeDefaultBrowser", "palette.makeDefaultTerminal", "palette.toggleSetting", "palette.shortcutKeymap", "palette.searchShortcuts", "palette.installCLI", "palette.uninstallCLI", "palette.restartSocketListener", "palette.restartServer", "palette.checkForUpdates", "updates.whatsNew", "announcements.show", "announcements.hide", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AccountActionCatalog.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AccountActionCatalog.swift index f2d0ba31d341..e5051773b5f0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AccountActionCatalog.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AccountActionCatalog.swift @@ -6,6 +6,11 @@ nonisolated enum AccountActionCatalog: ActionCatalogGroup { static func descriptors() -> [ActionDescriptor] { var actions = [ + ActionDescriptor( + id: "accounts.githubConnect", title: text("action.accounts.githubConnect", "Connect GitHub"), + keywords: ["github", "repositories", "integration"], category: .cloud, symbol: "link", + surfaces: [.palette], cliName: "accounts github-connect" + ), ActionDescriptor( id: "accounts.show", title: text("action.accounts.show", "Accounts…"), @@ -43,7 +48,7 @@ nonisolated enum AccountActionCatalog: ActionCatalogGroup { ] // Connect and remove are CodeRouter round trips: the CLI waits for // the outcome and exits non-zero when it failed. - for index in actions.indices where ["accounts.connect", "accounts.remove"].contains(actions[index].id.rawValue) { + for index in actions.indices where ["accounts.connect", "accounts.remove", "accounts.githubConnect"].contains(actions[index].id.rawValue) { actions[index].waitsForResult = true } return actions diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index 936e45f4e42b..aadbfcf7c8b4 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -1,3 +1,4 @@ +import CmuxNextPages import AppKit public import CmuxNextAccounts import CmuxNextCloud @@ -16,6 +17,7 @@ import CmuxNextCompat final class AccountsService: AccountsServices { unowned let services: AppServices private(set) lazy var model = AccountsModel(services: self) + private(set) lazy var github = GitHubConnections.live(cloud: services.cloud, preview: services.environment.showcase || fixtureHome != nil) let keys: any ProviderKeyStoring /// The per-user salt behind every `acct_…` handle (Keychain, read once off the main actor). let labels: AccountLabelerStore @@ -174,3 +176,30 @@ extension CloudAuth: CmuxAccountSource {} private struct NoKeychain: KeychainProbing { func hasGenericPassword(service: String) -> Bool { false } } + +extension GitHubConnections { + static func live(cloud: CloudService, preview: Bool) -> GitHubConnections { + let auth = cloud.auth + let transport = InstallHTTPTransport(baseURL: cloud.configuration.ownerAPIBaseURL()) + let current: @MainActor () -> Scope? = { + guard !preview, auth.isSignedIn, let user = auth.user?.id else { return nil } + return Scope(user: backendUser(project: auth.configuration.stackProjectID, stackUser: user), team: auth.teamID) + } + return GitHubConnections(scope: current, call: { bound, op, params, mutationKey in + let access = try await auth.tokens().access + guard current() == bound else { throw GitHubFailure.changedAccount } + var body: [String: JSONValue] = ["op": .string(op), "params": params] + if let mutationKey { + body["origin"] = "user" + body["idempotency_key"] = .string(mutationKey) + } + var headers: [String: String] = [:] + if let team = backendTeam(project: auth.configuration.stackProjectID, stackTeam: bound.team) { headers["x-cmux-team"] = team } + let response = try await transport.post(mutationKey != nil ? "v1/ops" : "v1/read", json: JSONEncoder().encode(JSONValue.object(body)), bearer: access, headers: headers) + guard current() == bound else { throw GitHubFailure.changedAccount } + let reply = try JSONValue.parse(response.body) + guard (200..<300).contains(response.status), (reply["error"] == nil || reply["error"] == .null), reply["ok"] != .bool(false), let value = reply["value"] else { throw GitHubFailure.refused } + return value + }, open: { url in LinkRouter.shared.open(url) }) + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift index cfa42f870358..8c387c00481b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift @@ -12,6 +12,16 @@ enum AccountsHandlers { static func bind(into registry: ActionRegistry, context: AppActionContext) { let services = context.services registry.bind("accounts.show", run: { _ in try services.settingsWindow.show(section: .accounts) }) + registry.bind("accounts.githubConnect", run: { _ in + try services.settingsWindow.show(section: .accounts) + registry.track(result: Task { @MainActor in + let github = services.accounts.github + await github.connect() + let state = github.pageState + return .success(["status": .string(state["outcome"]?.stringValue ?? "failed"), + "detail": .string(state["detail"]?.stringValue ?? "")]) + }) + }) registry.bind("accounts.refresh", run: { _ in services.accounts.model.refresh() }) registry.bind("accounts.reauthenticate", run: { invocation in let provider = try provider(invocation) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/PageFactory.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/PageFactory.swift index 6ac6918cce28..7cb7c6819240 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/PageFactory.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/PageFactory.swift @@ -117,12 +117,25 @@ struct PageFactory { }, hostLists: { [weak services] in services?.settingsWindow.pageHostLists() ?? .null }) provider.removeDevice = { [weak services] id in await services?.devices.remove(id: id) } let accounts = services.accounts.model - provider.accountsState = { (try? JSONValue.parse(JSONEncoder().encode(accounts.pageState))) ?? .null } + let github = services.accounts.github + provider.accountsState = { + guard let encoded = try? JSONValue.parse(JSONEncoder().encode(accounts.pageState)), case .object(var state) = encoded else { return .null } + state["github"] = github.pageState + return .object(state) + } provider.accountsRun = { params in + let gesture = params["action"]?.stringValue ?? "" + if gesture.hasPrefix("github") { + guard ["githubConnect", "githubDisconnect", "githubRefresh"].contains(gesture) else { throw PageError.invalidParams("unknown GitHub action") } + if gesture == "githubConnect" { await github.connect() } + else { await github.perform(gesture, connection: params["account"]?.stringValue) } + return .object([:]) + } guard let action = AccountsPageAction(field: { params[$0]?.stringValue }) else { throw PageError.invalidParams("unknown accounts action") } if let error = await accounts.perform(action) { return ["error": .string(error)] } + if ["refresh", "focus", "selectTeam"].contains(gesture) { await github.perform("githubRefresh") } return .object([:]) } provider.agents = services.agentHarnesses diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift index 4cd3c85a2b3d..bab312def6c6 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift @@ -21,8 +21,8 @@ nonisolated enum FeedSettingsSchema { FeedGitHubSettings.enabledPath, section: .notifications, group: group, - title: SettingsText.keyed("settings.feed.github.enabled", "Connect GitHub"), - help: SettingsText.keyed("settings.feed.github.enabled.help", "Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first."), + title: SettingsText.keyed("settings.group.githubInbox", "GitHub Inbox"), + help: SettingsText.keyed("settings.feed.github.enabled.summary", "Inbox shows your GitHub notifications"), summary: SettingsText.keyed("settings.feed.github.enabled.summary", "Inbox shows your GitHub notifications"), kind: .toggle, default: .bool(FeedGitHubSettings.enabledDefault), diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift new file mode 100644 index 000000000000..401e24c18db3 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -0,0 +1,159 @@ +import CmuxNextActions +import CmuxNextAccounts +import CmuxNextDaemon +import Foundation +import Testing +@testable import CmuxNextApp + +@MainActor +@Suite struct GitHubConnectionsTests { + private let scope = GitHubConnections.Scope(user: "user", team: nil) + private var listing: JSONValue { + ["connections": .array([]), "providers": [["provider": "github", "configured": true]]] + } + + @Test func signedOutNeverCallsTheAPI() async { + var calls = 0 + let model = GitHubConnections(scope: { nil }, call: { _, _, _, _ in calls += 1; return self.listing }, open: { _ in }) + await model.perform("githubConnect") + await model.perform("githubRefresh") + await model.connect() + #expect(model.pageState["outcome"] == "needs-action") + #expect(calls == 0) + #expect(!model.ready) + } + + @Test func connectUsesTheAppInstallPageAndDoesNotPretendItIsConnected() async { + var operations: [String] = [] + var opened: [URL] = [] + var connected = false + let model = GitHubConnections(scope: { self.scope }, call: { _, op, params, mutation in + operations.append(op) + if op == "integration.connect" { + #expect(params == ["provider": "github"]) + #expect(mutation != nil) + connected = true + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=sample"] + } + if connected { return ["connections": [["id": "sample", "provider": "github", "status": "pending"]], "providers": [["provider": "github", "configured": true]]] } + return self.listing + }, open: { opened.append($0) }) + await model.perform("githubRefresh") + #expect(model.ready) + await model.perform("githubConnect") + #expect(operations == ["integration.list", "integration.connect", "integration.list"]) + #expect(opened.count == 1) + #expect(model.pending) + #expect(model.currentConnections.first?["status"] == "pending") + await model.perform("githubConnect") + #expect(operations.filter { $0 == "integration.connect" }.count == 1) + #expect(opened.count == 2) + } + + @Test func aResponseForTheOldAccountCannotPopulateTheNewAccount() async { + var current: GitHubConnections.Scope? = self.scope + let model = GitHubConnections(scope: { current }, call: { _, _, _, _ in + current = .init(user: "another-user", team: nil) + return ["connections": [["id": "old", "provider": "github", "status": "active"]], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in }) + await model.perform("githubRefresh") + #expect(model.currentConnections.isEmpty) + #expect(!model.ready) + #expect(!model.busy) + } + + @Test func unknownConnectionCannotBeRevoked() async { + var calls = 0 + let model = GitHubConnections(scope: { self.scope }, call: { _, _, _, _ in calls += 1; return self.listing }, open: { _ in }) + await model.perform("githubDisconnect", connection: "not-captured") + #expect(calls == 0) + } + + @Test func disconnectOnlyChangesAnOwnedConnectionAndRefreshesItsState() async { + var operations: [String] = [] + var revoked = false + let model = GitHubConnections(scope: { self.scope }, call: { _, op, params, mutation in + operations.append(op) + if op == "integration.revoke" { + #expect(params == ["connection": "mine"]) + #expect(mutation != nil) + revoked = true + return [:] + } + return ["connections": [ + ["id": "mine", "provider": "github", "created_by": "user", "status": revoked ? "revoked" : "active"], + ["id": "shared", "provider": "github", "created_by": "another-user", "status": "active"], + ], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in }) + await model.perform("githubRefresh") + await model.perform("githubDisconnect", connection: "shared") + #expect(operations == ["integration.list"]) + await model.perform("githubDisconnect", connection: "mine") + #expect(operations == ["integration.list", "integration.revoke", "integration.list"]) + #expect(model.currentConnections.count == 1) + #expect(model.currentConnections.first?["id"] == "shared") + } + + @Test func failedRefreshHasRecoveryAndDoesNotEraseKnownConnections() async { + var fail = false + var current = self.scope + let model = GitHubConnections(scope: { current }, call: { _, _, _, _ in + if fail { throw GitHubConnections.GitHubFailure.refused } + return self.listing + }, open: { _ in }) + await model.perform("githubRefresh") + fail = true + await model.perform("githubRefresh") + #expect(model.error) + #expect(model.pageState["refresh"]?.stringValue?.isEmpty == false) + current = .init(user: "another-user", team: nil) + #expect(!model.error) + #expect(!model.ready) + } + + @Test func lostConnectResponseReusesItsIdempotencyKey() async { + var keys: [String?] = [] + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in + if op == "integration.connect" { + keys.append(key) + if keys.count == 1 { throw GitHubConnections.GitHubFailure.refused } + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=sample"] + } + return self.listing + }, open: { _ in }) + await model.connect() + #expect(model.error) + await model.connect() + #expect(keys.count == 2) + #expect(keys.first! != nil) + #expect(keys[0] == keys[1]) + } + + @Test func sharedConnectDoesNotCreateAnotherActiveConnection() async { + var connects = 0 + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, _ in + if op == "integration.connect" { connects += 1 } + return ["connections": [["id": "mine", "provider": "github", "created_by": "user", "status": "active"]], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in }) + await model.connect() + await model.connect() + #expect(connects == 0) + #expect(model.pageState["outcome"] == "connected") + } + + @Test func theGuideAndCLIShareAWaitingConnectAction() throws { + let action = try #require(ActionRegistry.standard().descriptor(for: "accounts.githubConnect")) + #expect(action.cliName == "accounts github-connect") + #expect(action.waitsForResult) + } + + @Test(arguments: ["http://github.com/apps/cmux/installations/new", "https://example.test/apps/cmux/installations/new", "https://github.com@evil.test/apps/cmux/installations/new", "https://github.com/login", "https://github.com:443/apps/cmux/installations/new"]) + func onlyGitHubAppInstallationURLsOpen(_ url: String) { + #expect(GitHubConnections.installationURL(url) == nil) + } + + @Test func backendTeamUsesTheExistingIdentityContract() { + #expect(GitHubConnections.backendTeam(project: "project", stackTeam: "team") == "team_3051e6705a6a35841b74") + #expect(GitHubConnections.backendTeam(project: "project", stackTeam: nil) == nil) + } +} diff --git a/schemas/settings/source/feed.json b/schemas/settings/source/feed.json index 17ca844aa536..4d9d7a65b892 100644 --- a/schemas/settings/source/feed.json +++ b/schemas/settings/source/feed.json @@ -14,12 +14,12 @@ "key": "feed.github.enabled", "note": "The opt-in connection: this Mac reads GitHub notifications and review requests with the gh login.", "title": { - "key": "settings.feed.github.enabled", - "text": "Connect GitHub" + "key": "settings.group.githubInbox", + "text": "GitHub Inbox" }, "help": { - "key": "settings.feed.github.enabled.help", - "text": "Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first." + "key": "settings.feed.github.enabled.summary", + "text": "Inbox shows your GitHub notifications" }, "summary": { "key": "settings.feed.github.enabled.summary", diff --git a/webviews/src/pages/settings/components/AccountsSection.tsx b/webviews/src/pages/settings/components/AccountsSection.tsx index bdbf24cca49a..7231cc8e7479 100644 --- a/webviews/src/pages/settings/components/AccountsSection.tsx +++ b/webviews/src/pages/settings/components/AccountsSection.tsx @@ -8,6 +8,7 @@ import { useSettingsState, useStore } from "../context"; import { Select } from "../editors/Select"; import type { AccountsButton, AccountsCmux, AccountsRow, AccountsTeams } from "../ops"; import { t } from "../strings"; +import { GitHubAccountsCard } from "./GitHubAccountsCard"; export function AccountsSection() { const store = useStore(); @@ -67,6 +68,7 @@ function AccountsBody() { )} {accounts.teams && } + {accounts.groups.map((group) => (

{group.title}

diff --git a/webviews/src/pages/settings/components/GitHubAccountsCard.tsx b/webviews/src/pages/settings/components/GitHubAccountsCard.tsx new file mode 100644 index 000000000000..49ae939eaab6 --- /dev/null +++ b/webviews/src/pages/settings/components/GitHubAccountsCard.tsx @@ -0,0 +1,88 @@ +import { useState } from "react"; +import { useSettingsState, useStore } from "../context"; + +/** The host owns GitHub App authentication; this card never receives a credential. */ +export function GitHubAccountsCard() { + const github = useSettingsState().accounts?.github; + const store = useStore(); + const [confirming, setConfirming] = useState(null); + if (!github) return null; + return ( +
+

{github.title}

+
+
+
+

{github.detail}

+
+
+
+ + +
+
+
+ {github.connections.map((connection) => ( +
+
+ {connection.account} +

{connection.status}

+

{connection.permissions.join(" · ")}

+ {confirming === connection.id && ( +

+ {github.confirm} +

+ )} +
+
+
+ {confirming === connection.id ? ( + <> + + + + ) : ( + + )} +
+
+
+ ))} +
+
+ ); +} diff --git a/webviews/src/pages/settings/ops.ts b/webviews/src/pages/settings/ops.ts index 82413c47ccf7..0e76c987646f 100644 --- a/webviews/src/pages/settings/ops.ts +++ b/webviews/src/pages/settings/ops.ts @@ -307,7 +307,21 @@ export type AccountsCmux = { }; /** The Accounts part as the host draws it (texts already localized by the app). */ +export type GitHubConnectionState = { + title: string; + detail: string; + busy: boolean; + connect: string; + disconnect: string; + confirm: string; + cancel: string; + refresh: string; + canConnect: boolean; + connections: Array<{ id: string; account: string; status: string; permissions: string[]; canDisconnect: boolean }>; +}; + export type AccountsState = { + github?: GitHubConnectionState; teams: AccountsTeams | null; refresh: string; refreshing: boolean; diff --git a/webviews/src/pages/settings/settings.gallery.ts b/webviews/src/pages/settings/settings.gallery.ts index 8cfefcfbbc57..acf1a690a078 100644 --- a/webviews/src/pages/settings/settings.gallery.ts +++ b/webviews/src/pages/settings/settings.gallery.ts @@ -945,6 +945,83 @@ const resetAnchors = Object.values(RESET_SAMPLES).flatMap((key) => [ { selector: `${row(key)} + [data-row-key] .row-title` }, ]); +const githubConnection = { + title: "GitHub", + detail: "Connected", + busy: false, + connect: "Connect GitHub", + disconnect: "Disconnect", + confirm: "Disconnect GitHub? Agents will lose access through this connection.", + cancel: "Cancel", + refresh: "Refresh", + canConnect: false, + connections: [ + { + id: "sample-github", + canDisconnect: true, + account: "sample-team", + status: "Connected", + permissions: ["contents:write", "pull_requests:write"], + }, + ], +}; +for (const treatment of ["balanced", "compact", "inline"]) { + variants[`github-connect-${treatment}`] = variant("accounts", { + accounts: accounts({}, { github: githubConnection }), + note: `GitHub App connection: ${treatment} treatment. Balanced uses the existing Accounts spacing.`, + play: async (ctx) => { + await ctx.waitFor(() => ctx.document.querySelector('[data-card="github-connection"]')); + const card = ctx.document.querySelector('[data-card="github-connection"]')!; + card.scrollIntoView({ block: "center" }); + if (treatment !== "balanced") { + const style = ctx.document.createElement("style"); + style.textContent = + treatment === "compact" + ? '[data-card="github-connection"] .row { padding-block: 4px; } [data-card="github-connection"] .accounts-body { margin-top: 4px; }' + : '[data-card="github-connection"] .row { display: flex; align-items: center; gap: 16px; } [data-card="github-connection"] .accounts-body { margin: 0; padding: 0; }'; + card.append(style); + } + }, + }); +} +variants["github-connect-comparison"] = variant("accounts", { + accounts: accounts({}, { github: githubConnection }), +}); +variants["github-connect-empty"] = variant("accounts", { + accounts: accounts( + {}, + { + github: { + ...githubConnection, + connections: [], + canConnect: true, + detail: "Connect the cmux GitHub App. Choose repositories and review permissions in GitHub.", + }, + }, + ), +}); +variants["github-connect-pending"] = variant("accounts", { + accounts: accounts( + {}, + { + github: { + ...githubConnection, + connections: [], + canConnect: true, + detail: "Finish connecting in your browser, then refresh.", + }, + }, + ), +}); +variants["github-connect-confirm"] = variant("accounts", { + accounts: accounts({}, { github: githubConnection }), + play: async (ctx) => { + await ctx.waitFor(() => ctx.document.querySelector('[data-github-action="disconnect"]')); + await ctx.click({ selector: '[data-github-action="disconnect"]' }); + await ctx.waitFor(() => ctx.document.querySelector('[data-card="github-connection"] [role="status"]')); + }, +}); + export default settingsPageEntry({ id: "pages.settings", anchors: [{ selector: "[data-settings-search]" }, { selector: '[data-section-link="general"]' }, ...resetAnchors], From b2b1b126d2f5b05ee584251d43f05baf883a9a19 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 08:06:50 +0000 Subject: [PATCH 02/13] Return GitHub connection results through typed action effects Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- .../CmuxNextActions/ActionEffect.swift | 7 +++++++ .../ActionRegistry+Effects.swift | 10 ++++++++++ .../Handlers/AccountsHandlers.swift | 4 ++-- .../ActionEffectTests.swift | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionEffect.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionEffect.swift index 3a386b327270..776aea22fa28 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionEffect.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionEffect.swift @@ -17,6 +17,7 @@ public struct ActionEffect { enum Kind { case done case applied(ActionConfirmation) + case appliedResult(@MainActor () async -> Result<[String: ActionResultValue], ActionWorkFailure>) case waiting(ActionWait, @MainActor () async -> ActionWorkFailure?) case refused(String, quiet: Bool) } @@ -34,6 +35,12 @@ public struct ActionEffect { ActionEffect(kind: .applied(confirmation)) } + /// The local change is shown now; the registry starts `result` and returns its fields to a + /// capturing caller. A failure leaves that local recovery surface open and reports the error. + public static func applied(result: @escaping @MainActor () async -> Result<[String: ActionResultValue], ActionWorkFailure>) -> ActionEffect { + ActionEffect(kind: .appliedResult(result)) + } + /// Nothing shows until `work` answers. Counted per action (``ActionRegistry/waitingRuns``); the /// pre-push guardrail `action-wait` refuses a new use without a reviewed reason. /// diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionRegistry+Effects.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionRegistry+Effects.swift index ccc911111436..0bb9b283b50d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionRegistry+Effects.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionRegistry+Effects.swift @@ -55,6 +55,16 @@ extension ActionRegistry { return case .applied(let confirmation): start(id, invocation, confirmation.send, rollback: confirmation.rollback) + case .appliedResult(let result): + let reported = isCapturingWork + let title = title(for: id) ?? id.rawValue + track(result: Task { [weak self] in + let answer = await result() + if case .failure(let failure) = answer, !reported { + self?.remoteFailureObserver?(id, failedText(title, failure.message)) + } + return answer + }) case .waiting(let reason, let work): waitingRuns[canonicalID(for: id), default: [:]][reason, default: 0] += 1 start(id, invocation, work, rollback: nil) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift index 8c387c00481b..33bb844c409c 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift @@ -12,9 +12,9 @@ enum AccountsHandlers { static func bind(into registry: ActionRegistry, context: AppActionContext) { let services = context.services registry.bind("accounts.show", run: { _ in try services.settingsWindow.show(section: .accounts) }) - registry.bind("accounts.githubConnect", run: { _ in + registry.bindEffect("accounts.githubConnect", requires: nil, effect: { _ in try services.settingsWindow.show(section: .accounts) - registry.track(result: Task { @MainActor in + return .applied(result: { let github = services.accounts.github await github.connect() let state = github.pageState diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/ActionEffectTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/ActionEffectTests.swift index b421055d33c2..a64606d7e2c9 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/ActionEffectTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/ActionEffectTests.swift @@ -52,6 +52,25 @@ import Testing #expect(registry.effectBoundCount == 1) } + @Test func aResultEffectStartsAfterTheHandlerAndReturnsItsFields() async throws { + let world = World() + let registry = ActionRegistry(catalog: Self.catalog) + registry.bindEffect("test.rename", effect: { _ in + world.shown = "new" + return .applied(result: { + world.sent += 1 + return .success(["status": .string("pending")]) + }) + }) + let run = registry.capturingRun { registry.perform("test.rename") } + #expect(world.shown == "new") + #expect(world.sent == 0) + #expect(run.work.count == 1) + let result = try #require(run.results.first) + #expect(try await result.value.get()["status"] == .string("pending")) + #expect(world.sent == 1) + } + /// A failure the person did not wait on (a key or menu run) rolls back and shows the notice once. @Test func aFailureRollsBackAndIsShownOnce() async { let world = World() From 25f214c312373b5a4df9879f609953e349d73d6c Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 08:17:48 +0000 Subject: [PATCH 03/13] Renew expired GitHub authorizations and fail rejected setup commands Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- .../CmuxNextAccounts/GitHubConnections.swift | 23 +++++++++++++-- .../Handlers/AccountsHandlers.swift | 17 ++++++----- .../GitHubConnectionsTests.swift | 29 +++++++++++++++++-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index bb6543fa097f..f0b32987e76f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -16,21 +16,23 @@ public final class GitHubConnections { @ObservationIgnored private let scope: () -> Scope? @ObservationIgnored private let call: Call @ObservationIgnored private let open: (URL) -> Void + @ObservationIgnored private let now: () -> Date @ObservationIgnored private var connectKeys: [Scope: String] = [:] private var loadedScope: Scope? private var workingScope: Scope? private var generation = 0 - @ObservationIgnored private var authorizations: [Scope: (id: String, url: URL)] = [:] + @ObservationIgnored private var authorizations: [Scope: (id: String, url: URL, expires: Date)] = [:] private(set) var connections: [JSONValue] = [] private(set) var configured = false private var errorScope: Scope? public var error: Bool { scope() != nil && errorScope == scope() } public var pending: Bool { currentConnections.contains { $0["status"]?.stringValue == "pending" } || scope().map { authorizations[$0] != nil } == true } - public init(scope: @escaping () -> Scope?, call: @escaping Call, open: @escaping (URL) -> Void) { + public init(scope: @escaping () -> Scope?, call: @escaping Call, open: @escaping (URL) -> Void, now: @escaping () -> Date = Date.init) { self.scope = scope self.call = call self.open = open + self.now = now } public var scopeIsSignedOut: Bool { scope() == nil } @@ -63,6 +65,10 @@ public final class GitHubConnections { switch action { case "githubConnect": guard ready else { return } + if let authorization = authorizations[bound], authorization.expires <= now() { + authorizations[bound] = nil + connectKeys[bound] = nil + } if let authorization = authorizations[bound] { open(authorization.url) } else { @@ -71,7 +77,7 @@ public final class GitHubConnections { guard scope() == bound, generation == ticket else { return } guard let raw = value["authorize_url"]?.stringValue, let url = Self.installationURL(raw), let id = value["connection"]?["id"]?.stringValue, !id.isEmpty else { throw GitHubFailure.invalidReply } - authorizations[bound] = (id, url) + authorizations[bound] = (id, url, Self.authorizationExpiration(url) ?? now()) open(url) } case "githubDisconnect": @@ -106,6 +112,17 @@ public final class GitHubConnections { return url } + /// Expiry is only a cache hint; the API still verifies the signed state on completion. + private nonisolated static func authorizationExpiration(_ url: URL) -> Date? { + guard let token = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first(where: { $0.name == "state" })?.value else { return nil } + let parts = token.split(separator: ".") + guard parts.count == 3 else { return nil } + var payload = String(parts[1]).replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + payload += String(repeating: "=", count: (4 - payload.count % 4) % 4) + guard let data = Data(base64Encoded: payload), let value = try? JSONValue.parse(data), let expires = value["exp"]?.doubleValue else { return nil } + return Date(timeIntervalSince1970: expires) + } + /// The existing API identity contract (`domains/user.ts::stackTeamIdFor`). public nonisolated static func backendTeam(project: String, stackTeam: String?) -> String? { guard let stackTeam else { return nil } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift index 33bb844c409c..7774f8ea5f8b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/AccountsHandlers.swift @@ -1,4 +1,5 @@ import CmuxNextActions +import CmuxNextAccounts import CmuxNextCodeRouter import CmuxNextSettings import Foundation @@ -14,13 +15,7 @@ enum AccountsHandlers { registry.bind("accounts.show", run: { _ in try services.settingsWindow.show(section: .accounts) }) registry.bindEffect("accounts.githubConnect", requires: nil, effect: { _ in try services.settingsWindow.show(section: .accounts) - return .applied(result: { - let github = services.accounts.github - await github.connect() - let state = github.pageState - return .success(["status": .string(state["outcome"]?.stringValue ?? "failed"), - "detail": .string(state["detail"]?.stringValue ?? "")]) - }) + return .applied(result: { await githubConnectResult(services.accounts.github) }) }) registry.bind("accounts.refresh", run: { _ in services.accounts.model.refresh() }) registry.bind("accounts.reauthenticate", run: { invocation in @@ -57,6 +52,14 @@ enum AccountsHandlers { }) } + static func githubConnectResult(_ github: GitHubConnections) async -> Result<[String: ActionResultValue], ActionWorkFailure> { + await github.connect() + let state = github.pageState + let detail = state["detail"]?.stringValue ?? "" + if github.error { return .failure(ActionWorkFailure(detail)) } + return .success(["status": .string(state["outcome"]?.stringValue ?? "needs-action"), "detail": .string(detail)]) + } + private static func provider(_ invocation: ActionInvocation) throws -> AIProvider { let raw = invocation["provider"]?.stringValue ?? "" guard let provider = AIProvider(rawValue: raw.lowercased()) else { throw ActionFailure(message: AccountsAppStrings.unknownProvider(raw)) } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index 401e24c18db3..dc5acb8832a4 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -33,7 +33,7 @@ import Testing #expect(params == ["provider": "github"]) #expect(mutation != nil) connected = true - return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=sample"] + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] } if connected { return ["connections": [["id": "sample", "provider": "github", "status": "pending"]], "providers": [["provider": "github", "configured": true]]] } return self.listing @@ -117,7 +117,7 @@ import Testing if op == "integration.connect" { keys.append(key) if keys.count == 1 { throw GitHubConnections.GitHubFailure.refused } - return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=sample"] + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] } return self.listing }, open: { _ in }) @@ -141,6 +141,31 @@ import Testing #expect(model.pageState["outcome"] == "connected") } + @Test func anExpiredAuthorizationStartsANewAttempt() async { + var clock = Date(timeIntervalSince1970: 4102444799) + var keys: [String?] = [] + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in + if op == "integration.connect" { + keys.append(key) + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] + } + return ["connections": [["id": "sample", "provider": "github", "status": "pending"]], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in }, now: { clock }) + await model.connect() + clock = clock.addingTimeInterval(1) + await model.connect() + #expect(keys.count == 2) + #expect(keys[0] != keys[1]) + } + + @Test func theCLIReportsAConnectionFailureAsFailure() async { + let model = GitHubConnections(scope: { self.scope }, call: { _, _, _, _ in throw GitHubConnections.GitHubFailure.refused }, open: { _ in }) + let result = await AccountsHandlers.githubConnectResult(model) + guard case .failure(let error) = result else { Issue.record("the command reported success after a failed request"); return } + #expect(!error.message.isEmpty) + #expect(model.pageState["outcome"] == "failed") + } + @Test func theGuideAndCLIShareAWaitingConnectAction() throws { let action = try #require(ActionRegistry.standard().descriptor(for: "accounts.githubConnect")) #expect(action.cliName == "accounts github-connect") From 5aa1be918000b39625d43f263c378352eccbb8d5 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 08:28:38 +0000 Subject: [PATCH 04/13] Recognize active shared GitHub connections without enabling revoke Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- .../CmuxNextAccounts/GitHubConnections.swift | 2 +- .../CmuxNextAppTests/GitHubConnectionsTests.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index f0b32987e76f..1d5d003747fc 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -38,7 +38,7 @@ public final class GitHubConnections { public var scopeIsSignedOut: Bool { scope() == nil } public var busy: Bool { workingScope != nil && workingScope == scope() } public var currentConnections: [JSONValue] { loadedScope == scope() ? connections : [] } - public var connected: Bool { currentConnections.contains { $0["status"]?.stringValue == "active" && canDisconnect($0) } } + public var connected: Bool { currentConnections.contains { $0["status"]?.stringValue == "active" && (canDisconnect($0) || $0["sharing"]?.stringValue == "team") } } public var ready: Bool { scope() != nil && loadedScope == scope() && configured } public func canDisconnect(_ row: JSONValue) -> Bool { diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index dc5acb8832a4..aa7023aa8a56 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -141,6 +141,21 @@ import Testing #expect(model.pageState["outcome"] == "connected") } + @Test func anActiveSharedConnectionIsConnectedButCannotBeRevoked() async { + var mutations = 0 + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, _ in + if op != "integration.list" { mutations += 1 } + return ["connections": [["id": "shared", "provider": "github", "created_by": "another-user", "sharing": "team", "status": "active"]], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in }) + await model.connect() + await model.perform("githubDisconnect", connection: "shared") + #expect(model.connected) + #expect(model.pageState["outcome"] == "connected") + #expect(model.pageState["canConnect"] == false) + #expect(model.pageState["connections"]?.arrayValue?.first?["canDisconnect"] == false) + #expect(mutations == 0) + } + @Test func anExpiredAuthorizationStartsANewAttempt() async { var clock = Date(timeIntervalSince1970: 4102444799) var keys: [String?] = [] From dce2ee1b01937c369b26d5cd090b0cdc5112e2d8 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 08:41:34 +0000 Subject: [PATCH 05/13] Use native status output for GitHub disconnect confirmation Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- .../src/pages/settings/components/GitHubAccountsCard.tsx | 6 +----- webviews/src/pages/settings/settings.gallery.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/webviews/src/pages/settings/components/GitHubAccountsCard.tsx b/webviews/src/pages/settings/components/GitHubAccountsCard.tsx index 49ae939eaab6..7ff9fd934751 100644 --- a/webviews/src/pages/settings/components/GitHubAccountsCard.tsx +++ b/webviews/src/pages/settings/components/GitHubAccountsCard.tsx @@ -41,11 +41,7 @@ export function GitHubAccountsCard() { {connection.account}

{connection.status}

{connection.permissions.join(" · ")}

- {confirming === connection.id && ( -

- {github.confirm} -

- )} + {confirming === connection.id && {github.confirm}}
diff --git a/webviews/src/pages/settings/settings.gallery.ts b/webviews/src/pages/settings/settings.gallery.ts index e675fc187f93..600cbf4b3594 100644 --- a/webviews/src/pages/settings/settings.gallery.ts +++ b/webviews/src/pages/settings/settings.gallery.ts @@ -1192,7 +1192,7 @@ variants["github-connect-confirm"] = variant("accounts", { play: async (ctx) => { await ctx.waitFor(() => ctx.document.querySelector('[data-github-action="disconnect"]')); await ctx.click({ selector: '[data-github-action="disconnect"]' }); - await ctx.waitFor(() => ctx.document.querySelector('[data-card="github-connection"] [role="status"]')); + await ctx.waitFor(() => ctx.document.querySelector('[data-card="github-connection"] output')); }, }); From 255e094f63279f9c660c765073af51821963ce66 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 08:42:44 +0000 Subject: [PATCH 06/13] Use the Settings wire value for GitHub account state Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- Packages/macOS/CmuxNext/Package.swift | 2 +- .../Sources/CmuxNextAccounts/GitHubConnections+Page.swift | 2 +- .../CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift | 2 +- .../CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift | 2 +- .../Tests/CmuxNextAppTests/GitHubConnectionsTests.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 8ffbca846e0c..73bfc2c83ad9 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -440,7 +440,7 @@ let packageTargets: [Target] = uiLayerPreviewTargets + [ // supplies `AccountsServices`. .target( name: "CmuxNextAccounts", - dependencies: ["CmuxNextDaemon", "CmuxNextCloud", "CmuxNextCodeRouter", "CmuxNextDesign", "CmuxNextIcons", .product(name: "CmuxAgentBrands", package: "CmuxAgentBrands"), + dependencies: ["CmuxNextSettings", "CmuxNextCloud", "CmuxNextCodeRouter", "CmuxNextDesign", "CmuxNextIcons", .product(name: "CmuxAgentBrands", package: "CmuxAgentBrands"), .product(name: "CMUXAuthCore", package: "CMUXAuthCore")], resources: [ .process("Localizable.xcstrings"), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift index 119aad14bbf8..c9c727e206b4 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift @@ -1,4 +1,4 @@ -public import CmuxNextDaemon +public import CmuxNextSettings import Foundation extension GitHubConnections { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index 1d5d003747fc..7067504e3669 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -1,4 +1,4 @@ -public import CmuxNextDaemon +public import CmuxNextSettings import CryptoKit public import Foundation import Observation diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index b10c36fc4380..3ac005b23bb3 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -3,7 +3,7 @@ import AppKit public import CmuxNextAccounts import CmuxNextCloud import CmuxNextCodeRouter -import CmuxNextDaemon +import CmuxNextSettings import Foundation import os import CmuxNextCompat diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index aa7023aa8a56..0a607edcf09c 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -1,6 +1,6 @@ import CmuxNextActions import CmuxNextAccounts -import CmuxNextDaemon +import CmuxNextSettings import Foundation import Testing @testable import CmuxNextApp From e4025f36dab0f33fc52a00492e4d89bccee99141 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 09:06:24 +0000 Subject: [PATCH 07/13] Serialize GitHub operation frames with the Settings JSON writer Agent-Session: https://cmux.com/deeplink/agent?id=aid-43b0dcc8e229a5561313 --- .../Sources/CmuxNextApp/Accounts/AccountsService.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index 3ac005b23bb3..4d1757a217f1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -4,6 +4,7 @@ public import CmuxNextAccounts import CmuxNextCloud import CmuxNextCodeRouter import CmuxNextSettings +import struct CmuxNextDaemon.LoginEnvironment import Foundation import os import CmuxNextCompat @@ -195,7 +196,7 @@ extension GitHubConnections { } var headers: [String: String] = [:] if let team = backendTeam(project: auth.configuration.stackProjectID, stackTeam: bound.team) { headers["x-cmux-team"] = team } - let response = try await transport.post(mutationKey != nil ? "v1/ops" : "v1/read", json: JSONEncoder().encode(JSONValue.object(body)), bearer: access, headers: headers) + let response = try await transport.post(mutationKey != nil ? "v1/ops" : "v1/read", json: Data(JSONValue.object(body).compactText.utf8), bearer: access, headers: headers) guard current() == bound else { throw GitHubFailure.changedAccount } let reply = try JSONValue.parse(response.body) guard (200..<300).contains(response.status), (reply["error"] == nil || reply["error"] == .null), reply["ok"] != .bool(false), let value = reply["value"] else { throw GitHubFailure.refused } From b09afc6004cb715653a298dc21874cca00ef13c0 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 29 Oct 2026 18:39:28 +0000 Subject: [PATCH 08/13] Cover GitHub connection retries after refusal and relaunch Agent-Session: https://cmux.com/deeplink/agent?id=aid-6db7dd75de7de7b32e16 --- .../GitHubConnectionsTests.swift | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index 0a607edcf09c..ca3319383816 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -116,7 +116,7 @@ import Testing let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in if op == "integration.connect" { keys.append(key) - if keys.count == 1 { throw GitHubConnections.GitHubFailure.refused } + if keys.count == 1 { throw URLError(.networkConnectionLost) } return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] } return self.listing @@ -129,6 +129,39 @@ import Testing #expect(keys[0] == keys[1]) } + @Test func policyRejectionDoesNotReplayAfterPolicyChanges() async throws { + var keys: [String] = [] + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in + if op == "integration.connect" { + keys.append(try #require(key)) + if keys.count == 1 { throw GitHubConnections.GitHubFailure.refused } + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] + } + return self.listing + }, open: { _ in }) + await model.connect() + #expect(model.error) + await model.connect() + #expect(!model.error) + try #require(keys.count == 2) + #expect(keys[0] != keys[1]) + } + + @Test func restoredPendingConnectionDoesNotCreateADuplicate() async { + var connects = 0 + var opened = 0 + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, _ in + if op == "integration.connect" { connects += 1 } + return ["connections": [["id": "pending", "provider": "github", "created_by": "user", "status": "pending"]], "providers": [["provider": "github", "configured": true]]] + }, open: { _ in opened += 1 }) + await model.connect() + #expect(model.pageState["canConnect"] == false) + #expect(model.pageState["outcome"] == "pending") + await model.perform("githubConnect") + #expect(connects == 0) + #expect(opened == 0) + } + @Test func sharedConnectDoesNotCreateAnotherActiveConnection() async { var connects = 0 let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, _ in @@ -164,6 +197,7 @@ import Testing keys.append(key) return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] } + if keys.isEmpty { return self.listing } return ["connections": [["id": "sample", "provider": "github", "status": "pending"]], "providers": [["provider": "github", "configured": true]]] }, open: { _ in }, now: { clock }) await model.connect() From b497fafd2aa0df3e8f129a478eb9fd0793a14fcb Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 29 Oct 2026 18:41:20 +0000 Subject: [PATCH 09/13] Make GitHub retries respect refusals and pending authorization Agent-Session: https://cmux.com/deeplink/agent?id=aid-6db7dd75de7de7b32e16 --- .../CmuxNextAccounts/GitHubConnections+Page.swift | 2 +- .../Sources/CmuxNextAccounts/GitHubConnections.swift | 10 +++++++++- .../Sources/CmuxNextApp/Accounts/AccountsService.swift | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift index c9c727e206b4..64b7b34f989b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections+Page.swift @@ -15,7 +15,7 @@ extension GitHubConnections { "title": "GitHub", "detail": .string(note), "busy": .bool(busy), "connect": .string(Self.connect), "disconnect": .string(Self.disconnect), "confirm": .string(Self.confirm), "cancel": .string(Self.cancel), - "refresh": .string(Self.refresh), "canConnect": .bool(ready && !busy && !connected), + "refresh": .string(Self.refresh), "canConnect": .bool(canConnect), "connections": .array(currentConnections.map { row in ["id": row["id"] ?? .null, "account": row["account"]?["name"] ?? "GitHub", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index 7067504e3669..c2d3845c1bf9 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -40,6 +40,7 @@ public final class GitHubConnections { public var currentConnections: [JSONValue] { loadedScope == scope() ? connections : [] } public var connected: Bool { currentConnections.contains { $0["status"]?.stringValue == "active" && (canDisconnect($0) || $0["sharing"]?.stringValue == "team") } } public var ready: Bool { scope() != nil && loadedScope == scope() && configured } + public var canConnect: Bool { ready && !busy && !connected && (!pending || scope().map { authorizations[$0] != nil } == true) } public func canDisconnect(_ row: JSONValue) -> Bool { scope() != nil && row["created_by"]?.stringValue == scope()?.user @@ -64,7 +65,9 @@ public final class GitHubConnections { do { switch action { case "githubConnect": - guard ready else { return } + // A pending flow from another launch has no resumable URL here. + // Keep it pending until refresh or disconnect resolves that flow. + guard ready, !connected, !pending || authorizations[bound] != nil else { return } if let authorization = authorizations[bound], authorization.expires <= now() { authorizations[bound] = nil connectKeys[bound] = nil @@ -100,6 +103,11 @@ public final class GitHubConnections { connectKeys[bound] = nil } } catch { + if action == "githubConnect", let failure = error as? GitHubFailure, case .refused = failure { + // A definitive API refusal is replayed under its mutation key. + // Transport failures keep the key because the mutation may have succeeded. + connectKeys[bound] = nil + } if scope() == bound, generation == ticket { errorScope = bound } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index 4d1757a217f1..7039720588e1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -198,8 +198,12 @@ extension GitHubConnections { if let team = backendTeam(project: auth.configuration.stackProjectID, stackTeam: bound.team) { headers["x-cmux-team"] = team } let response = try await transport.post(mutationKey != nil ? "v1/ops" : "v1/read", json: Data(JSONValue.object(body).compactText.utf8), bearer: access, headers: headers) guard current() == bound else { throw GitHubFailure.changedAccount } + // A timeout, throttling response or server failure does not prove that + // the mutation was refused. Keep its key for a safe retry. + guard response.status < 500, response.status != 408, response.status != 429 else { throw URLError(.badServerResponse) } + guard (200..<300).contains(response.status) else { throw GitHubFailure.refused } let reply = try JSONValue.parse(response.body) - guard (200..<300).contains(response.status), (reply["error"] == nil || reply["error"] == .null), reply["ok"] != .bool(false), let value = reply["value"] else { throw GitHubFailure.refused } + guard (reply["error"] == nil || reply["error"] == .null), reply["ok"] != .bool(false), let value = reply["value"] else { throw GitHubFailure.refused } return value }, open: { url in LinkRouter.shared.open(url) }) } From eeb5dbf2529832362c3f353f0215f0c28abead43 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 29 Oct 2026 19:05:04 +0000 Subject: [PATCH 10/13] Guard GitHub retries against stale scopes and teams Agent-Session: https://cmux.com/deeplink/agent?id=aid-6db7dd75de7de7b32e16 --- .../CmuxNextAccounts/GitHubConnections.swift | 3 +- .../Accounts/AccountsService.swift | 3 +- .../GitHubConnectionsTests.swift | 37 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index c2d3845c1bf9..66c3e5a1c750 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -103,7 +103,8 @@ public final class GitHubConnections { connectKeys[bound] = nil } } catch { - if action == "githubConnect", let failure = error as? GitHubFailure, case .refused = failure { + if action == "githubConnect", generation == ticket, scope() == bound, + let failure = error as? GitHubFailure, case .refused = failure { // A definitive API refusal is replayed under its mutation key. // Transport failures keep the key because the mutation may have succeeded. connectKeys[bound] = nil diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index 7039720588e1..c5f4847a3b18 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -183,7 +183,8 @@ extension GitHubConnections { let auth = cloud.auth let transport = InstallHTTPTransport(baseURL: cloud.configuration.ownerAPIBaseURL()) let current: @MainActor () -> Scope? = { - guard !preview, auth.isSignedIn, let user = auth.user?.id else { return nil } + guard !preview, auth.isSignedIn, let user = auth.user?.id, + auth.managedTeamID == nil || auth.teamID != nil else { return nil } return Scope(user: backendUser(project: auth.configuration.stackProjectID, stackUser: user), team: auth.teamID) } return GitHubConnections(scope: current, call: { bound, op, params, mutationKey in diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index ca3319383816..2fd3a6f943aa 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -147,6 +147,43 @@ import Testing #expect(keys[0] != keys[1]) } + @Test func aRefusalFromAnOlderGenerationCannotClearANewKey() async { + var current: GitHubConnections.Scope? = self.scope + var firstResult: CheckedContinuation? + var secondResult: CheckedContinuation? + var started: CheckedContinuation? + var secondStarted: CheckedContinuation? + var keys: [String] = [] + let other = GitHubConnections.Scope(user: "other", team: nil) + let model = GitHubConnections(scope: { current }, call: { _, op, _, key in + if op == "integration.connect" { + keys.append(key ?? "") + if keys.count == 1 { + started?.resume() + started = nil + return try await withCheckedThrowingContinuation { firstResult = $0 } + } + secondStarted?.resume() + secondStarted = nil + return try await withCheckedThrowingContinuation { secondResult = $0 } + } + return self.listing + }, open: { _ in }) + let first = Task { await model.perform("githubConnect") } + await withCheckedContinuation { started = $0 } + current = other + await model.perform("githubRefresh") + current = self.scope + let second = Task { await model.perform("githubConnect") } + await withCheckedContinuation { secondStarted = $0 } + firstResult?.resume(throwing: GitHubConnections.GitHubFailure.refused) + await first.value + secondResult?.resume(throwing: URLError(.networkConnectionLost)) + await second.value + #expect(keys.count == 2) + #expect(keys[0] != keys[1]) + } + @Test func restoredPendingConnectionDoesNotCreateADuplicate() async { var connects = 0 var opened = 0 From e004e898c1ebc879332acf9dbf483fe2d2670e75 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 29 Oct 2026 19:24:45 +0000 Subject: [PATCH 11/13] Test GitHub retry generations and managed team isolation Agent-Session: https://cmux.com/deeplink/agent?id=aid-6db7dd75de7de7b32e16 --- .../Accounts/AccountsService.swift | 10 ++- .../GitHubConnectionsTests.swift | 76 ++++++++++++------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift index c5f4847a3b18..f5cf63bcf23a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Accounts/AccountsService.swift @@ -179,13 +179,17 @@ private struct NoKeychain: KeychainProbing { } extension GitHubConnections { + static func signedInScope(project: String, user: String, team: String?, managedTeam: String?) -> Scope? { + guard managedTeam == nil || team == managedTeam else { return nil } + return Scope(user: backendUser(project: project, stackUser: user), team: team) + } + static func live(cloud: CloudService, preview: Bool) -> GitHubConnections { let auth = cloud.auth let transport = InstallHTTPTransport(baseURL: cloud.configuration.ownerAPIBaseURL()) let current: @MainActor () -> Scope? = { - guard !preview, auth.isSignedIn, let user = auth.user?.id, - auth.managedTeamID == nil || auth.teamID != nil else { return nil } - return Scope(user: backendUser(project: auth.configuration.stackProjectID, stackUser: user), team: auth.teamID) + guard !preview, auth.isSignedIn, let user = auth.user?.id else { return nil } + return signedInScope(project: auth.configuration.stackProjectID, user: user, team: auth.teamID, managedTeam: auth.managedTeamID) } return GitHubConnections(scope: current, call: { bound, op, params, mutationKey in let access = try await auth.tokens().access diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index 2fd3a6f943aa..8e1cdb835d14 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -147,41 +147,65 @@ import Testing #expect(keys[0] != keys[1]) } - @Test func aRefusalFromAnOlderGenerationCannotClearANewKey() async { + @Test func aRefusalFromAnOlderGenerationCannotClearANewKey() async throws { var current: GitHubConnections.Scope? = self.scope - var firstResult: CheckedContinuation? - var secondResult: CheckedContinuation? - var started: CheckedContinuation? - var secondStarted: CheckedContinuation? + let requests = AsyncStream>.makeStream() + var events = requests.stream.makeAsyncIterator() var keys: [String] = [] - let other = GitHubConnections.Scope(user: "other", team: nil) let model = GitHubConnections(scope: { current }, call: { _, op, _, key in - if op == "integration.connect" { - keys.append(key ?? "") - if keys.count == 1 { - started?.resume() - started = nil - return try await withCheckedThrowingContinuation { firstResult = $0 } - } - secondStarted?.resume() - secondStarted = nil - return try await withCheckedThrowingContinuation { secondResult = $0 } + guard op == "integration.connect" else { return self.listing } + keys.append(try #require(key)) + if keys.count == 1 || keys.count == 3 { + return try await withCheckedThrowingContinuation { requests.continuation.yield($0) } } - return self.listing + throw GitHubConnections.GitHubFailure.refused }, open: { _ in }) - let first = Task { await model.perform("githubConnect") } - await withCheckedContinuation { started = $0 } - current = other + let first = Task { + await model.connect() + requests.continuation.finish() + } + let oldest = try #require(await events.next()) + current = .init(user: "other", team: nil) await model.perform("githubRefresh") current = self.scope - let second = Task { await model.perform("githubConnect") } - await withCheckedContinuation { secondStarted = $0 } - firstResult?.resume(throwing: GitHubConnections.GitHubFailure.refused) + await model.connect() // Current refusal of K1 permits a new key. + let second = Task { + await model.connect() + requests.continuation.finish() + } + let latest = try #require(await events.next()) + oldest.resume(throwing: GitHubConnections.GitHubFailure.refused) await first.value - secondResult?.resume(throwing: URLError(.networkConnectionLost)) + latest.resume(throwing: URLError(.networkConnectionLost)) await second.value - #expect(keys.count == 2) - #expect(keys[0] != keys[1]) + await model.connect() + try #require(keys.count == 4) + #expect(keys[0] == keys[1]) + #expect(keys[1] != keys[2]) + #expect(keys[2] == keys[3]) // Obsolete K1 failure must not discard K2. + } + + @Test func managedTeamCannotFallBackToPersonalIntegration() async { + var team: String? + var calls = 0 + let model = GitHubConnections(scope: { + GitHubConnections.signedInScope(project: "project", user: "user", team: team, managedTeam: "managed") + }, call: { bound, _, _, _ in + calls += 1 + #expect(bound.team == "managed") + return self.listing + }, open: { _ in }) + await model.connect() + #expect(calls == 0) + team = "another-team" + await model.connect() + #expect(calls == 0) + team = "managed" + await model.perform("githubRefresh") + #expect(calls == 1) + #expect(model.ready) + #expect(GitHubConnections.signedInScope(project: "project", user: "user", team: nil, managedTeam: nil)?.team == nil) + #expect(GitHubConnections.signedInScope(project: "project", user: "user", team: nil, managedTeam: nil) != nil) } @Test func restoredPendingConnectionDoesNotCreateADuplicate() async { From 7e618fc61c9ef14685c61c593fc396997f7a6ebc Mon Sep 17 00:00:00 2001 From: Leo Li Date: Fri, 30 Oct 2026 01:25:18 -0700 Subject: [PATCH 12/13] Let a lost GitHub connect resume its server-side pending flow Review P2 at c93b0796: a connect whose response was lost left the server's pending row and the held idempotency key, but Connect stayed disabled and perform refused, so the person was stuck until the flow expired. A pending flow is now resumable when this launch holds its key; replaying the key returns the same connection's URL. A pending row from another launch with no key still stays pending. Regression: aLostConnectResponseWithAServerPendingRowRetriesUnderItsKey. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-0cf32b32a03b27dcdf4d --- .../CmuxNextAccounts/GitHubConnections.swift | 10 +++++--- .../GitHubConnectionsTests.swift | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift index 66c3e5a1c750..70f5bd016e66 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAccounts/GitHubConnections.swift @@ -40,7 +40,11 @@ public final class GitHubConnections { public var currentConnections: [JSONValue] { loadedScope == scope() ? connections : [] } public var connected: Bool { currentConnections.contains { $0["status"]?.stringValue == "active" && (canDisconnect($0) || $0["sharing"]?.stringValue == "team") } } public var ready: Bool { scope() != nil && loadedScope == scope() && configured } - public var canConnect: Bool { ready && !busy && !connected && (!pending || scope().map { authorizations[$0] != nil } == true) } + public var canConnect: Bool { ready && !busy && !connected && (!pending || scope().map(resumable) == true) } + + /// A pending flow this launch can resume: its cached browser URL, or the idempotency key of a + /// connect whose response was lost (replaying it returns the same pending connection's URL). + private func resumable(_ bound: Scope) -> Bool { authorizations[bound] != nil || connectKeys[bound] != nil } public func canDisconnect(_ row: JSONValue) -> Bool { scope() != nil && row["created_by"]?.stringValue == scope()?.user @@ -65,9 +69,9 @@ public final class GitHubConnections { do { switch action { case "githubConnect": - // A pending flow from another launch has no resumable URL here. + // A pending flow from another launch has no resumable URL or key here. // Keep it pending until refresh or disconnect resolves that flow. - guard ready, !connected, !pending || authorizations[bound] != nil else { return } + guard ready, !connected, !pending || resumable(bound) else { return } if let authorization = authorizations[bound], authorization.expires <= now() { authorizations[bound] = nil connectKeys[bound] = nil diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift index 8e1cdb835d14..570fda62bfc8 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/GitHubConnectionsTests.swift @@ -129,6 +129,30 @@ import Testing #expect(keys[0] == keys[1]) } + @Test func aLostConnectResponseWithAServerPendingRowRetriesUnderItsKey() async { + var keys: [String?] = [] + var opened: [URL] = [] + var created = false + let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in + if op == "integration.connect" { + keys.append(key) + created = true + if keys.count == 1 { throw URLError(.networkConnectionLost) } + return ["connection": ["id": "sample"], "authorize_url": "https://github.com/apps/cmux/installations/new?state=a.eyJleHAiOjQxMDI0NDQ4MDB9.b"] + } + // The lost connect did create the server's pending row. + let rows: [JSONValue] = created ? [["id": "sample", "provider": "github", "created_by": "user", "status": "pending"]] : [] + return ["connections": .array(rows), "providers": [["provider": "github", "configured": true]]] + }, open: { opened.append($0) }) + await model.connect() + #expect(model.error) + #expect(model.canConnect, "the held key makes the pending flow resumable") + await model.connect() + #expect(keys.count == 2) + #expect(keys[0] != nil && keys[0] == keys[1]) + #expect(opened.count == 1) + } + @Test func policyRejectionDoesNotReplayAfterPolicyChanges() async throws { var keys: [String] = [] let model = GitHubConnections(scope: { self.scope }, call: { _, op, _, key in From 097e4f73799519b9074d87ff46b6282d8ae2894c Mon Sep 17 00:00:00 2001 From: Leo Li Date: Fri, 30 Oct 2026 22:23:29 -0700 Subject: [PATCH 13/13] Regenerate cmux-next exports for the GitHub connect action and GitHub Inbox setting The generated files check found them stale at 6976e112; these are the files from its cmux-next-generated-patch artifact. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-0cf32b32a03b27dcdf4d --- docs/mdm/com.manaflow.cmux.json | 4 ++-- docs/mdm/com.manaflow.cmux.plist | 4 ++-- docs/mdm/managed-preferences.md | 2 +- plans/cmux-next/action-surfaces.json | 28 +++++++++++++++++++++++++++ plans/cmux-next/actions.md | 10 +++++----- schemas/defaults/cmux-next.json | 5 +++-- schemas/settings/settings-schema.json | 8 ++++---- 7 files changed, 45 insertions(+), 16 deletions(-) diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index 3c69d3b319a1..df0e81aeff54 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -2295,8 +2295,8 @@ }, "feed.github.enabled" : { "default" : false, - "description" : "Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first.", - "title" : "Connect GitHub", + "description" : "Inbox shows your GitHub notifications", + "title" : "GitHub Inbox", "type" : "boolean" }, "feed.github.pollIntervalSeconds" : { diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 4c60eb617547..5df49c19634c 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -8012,11 +8012,11 @@ pfm_default pfm_description - Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first. + Inbox shows your GitHub notifications pfm_name feed.github.enabled pfm_title - Connect GitHub + GitHub Inbox pfm_type boolean diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index 2cd1e077647f..e4b524fb0099 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -576,7 +576,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `labs.voiceRoom` | boolean | `false` | | Voice Room. An unofficial room where you talk with people and Chief. While a room is open, cmux transcribes your mic and reads your screen's text on this Mac, then sends that text to the room. | | `labs.voiceRoomURL` | string | `""` | | Voice Room Address. The room's web address. Empty uses the room server on this Mac. | | `labs.machineJoin` | boolean | `false` | | Join Any Machine with One Line. Add Device shows a line to paste on any Mac or Linux computer; it joins your team's machines. Screen sharing and control stay off until that machine allows them. | -| `feed.github.enabled` | boolean | `false` | | Connect GitHub. Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first. | +| `feed.github.enabled` | boolean | `false` | | GitHub Inbox. Inbox shows your GitHub notifications | | `feed.github.pollIntervalSeconds` | real | `120` | 60 to 900 | Refresh Interval. Seconds between GitHub refreshes. Refresh in the Inbox runs immediately. | | `agentPane.links.outsideRoots` | string | `"open"` | `confirm`, `text`, `open` | Files Outside the Project. What a file link in a reply does when the file is outside the chat's folders. Keys and .env files never open. | | `agentPane.images.remote` | string | `"click"` | `click`, `never`, `always` | Web Images in Replies. A web image loads from its site, which then sees that you read the reply. | diff --git a/plans/cmux-next/action-surfaces.json b/plans/cmux-next/action-surfaces.json index 1d1e76ce355f..a66efdb052e8 100644 --- a/plans/cmux-next/action-surfaces.json +++ b/plans/cmux-next/action-surfaces.json @@ -26843,6 +26843,34 @@ "title_key" : "action.cloudMachineLink", "title_table" : "CloudPortActions" }, + { + "cli" : "offered", + "cli_name" : "accounts github-connect", + "context_menu" : "noObject", + "context_menus" : [ + + ], + "default_aliases" : [ + + ], + "default_chord" : null, + "default_shortcut" : null, + "id" : "accounts.githubConnect", + "main_menu" : null, + "mcp" : "credentials", + "palette" : "offered", + "palette_section" : { + "id" : "category.cloud", + "order" : 110, + "title" : "Cloud and Account", + "title_key" : "category.cloud", + "title_table" : "Localizable" + }, + "palette_suggested" : null, + "title" : "Connect GitHub", + "title_key" : "action.accounts.githubConnect", + "title_table" : "AccountsActions" + }, { "cli" : "offered", "cli_name" : "accounts show", diff --git a/plans/cmux-next/actions.md b/plans/cmux-next/actions.md index 068e0ddb29aa..262ff9f5a35a 100644 --- a/plans/cmux-next/actions.md +++ b/plans/cmux-next/actions.md @@ -45,13 +45,13 @@ Declare its placements (menu, group, rank) and whether the CLI offers it, or nam - `sidebar`: 91 - `notifications`: 27 - `agents`: 107 -- `cloud`: 65 +- `cloud`: 66 - `remote`: 21 - `settings`: 132 -## Counts (1281 actions) +## Counts (1282 actions) -Palette 1228, CLI verbs 756, right-click 596, MCP tools 674. +Palette 1229, CLI verbs 757, right-click 596, MCP tools 674. ## Menus @@ -143,7 +143,7 @@ Palette 1228, CLI verbs 756, right-click 596, MCP tools 674. **guiOnly** (45): `browser.media.show`, `browser.downloads.show`, `browser.profile.choose`, `browser.overflow.menu`, `browser.download.openBlockedSiteSettings`, `palette.newAgentChatOn`, `agentPane.toggleSessionInfo`, `agentPane.toggleFiles`, `agentPane.togglePreview`, `agentPane.toggleSideTabs`, `agentPane.stopTurn`, `agentPane.continueIn`, `agentPane.switchModel`, `agentPane.createCheckpoint`, `agentPane.searchFiles`, `agentPane.copyLastCodeBlock`, `agentPane.permission.allowOnce`, `agentPane.permission.allowChat`, `agentPane.permission.deny`, `agentPane.permission.expand`, `agentPane.permission.retry`, `agentPane.permission.revoke`, `agentPane.permission.refresh`, `agentPane.toggleDisplaySettings`, `agentPane.replayConversation`, `agentPane.editInstructions`, `agentPane.addProjectFiles`, `agentPane.markUnread`, `agentPane.moveToFloatingChat`, `agentPane.continueAfterError`, `agentPane.toggleMinimap`, `agentPane.revealCwdInFinder`, `agentPane.openCwdInTerminal`, `agentPane.previousTurn`, `agentPane.nextTurn`, `agentPane.previousBookmarkedPrompt`, `agentPane.nextBookmarkedPrompt`, `agentChat.copyName`, `agentChat.copyHarnessSessionID`, `agentChat.copyConversation`, `agentChat.copySessionDetails`, `agentChat.copyWorkingDirectory`, `agentChat.copyHost`, `agentPane.sideQuestion`, `sidebar.priorityView.toggle` -**noObject** (303): `openSettings`, `newWindow`, `newIncognitoWindow`, `closeWindow`, `window.closeOthers`, `closeAllWindows`, `zoomWindow`, `selectNextWindow`, `selectPreviousWindow`, `minimizeWindow`, `toggleFullScreen`, `quit`, `quitKeepSessions`, `quitEndSessions`, `quitEndEverything`, `globalSearch`, `palette.open`, `commandPalette`, `palette.openTaskManager`, `palette.sleepyMode`, `keepMacAwake`, `about`, `workspace.closeAllInAllWindows`, `manageLayouts`, `palette.findWork`, `workspace.importClassic`, `session.moveFromBuild`, `palette.openFilesPane`, `palette.openFindPane`, `palette.openVaultPane`, `palette.openCloudPane`, `macScreen.stopSharing`, `macScreen.revokeViewer`, `reopenClosedBrowserPanel`, `home.show`, `home.openChief`, `home.tab`, `home.channels`, `home.switchView`, `home.enableChannels`, `home.disableChannels`, `home.toggleChiefPet`, `home.messageChiefPet`, `home.messageChiefFromAnyApp`, `home.attachFiles`, `home.newMessage`, `home.invite`, `home.inviteToTeam`, `home.newChief`, `home.createGroup`, `home.stopAllAgents`, `home.saveFlightRecording`, `home.messageTo`, `palette.browserClearHistory`, `importFromBrowser`, `password.importCSV`, `passwords.open`, `palette.enableBrowser`, `palette.disableBrowser`, `browser.allowAgentWithExtensions`, `browser.importFromFiles`, `browser.importCookies`, `screenshots.open`, `fileEditor.importEditorSettings`, `file.openRecoveredDraft`, `sidebar.profileMenu`, `browser.downloads.showFolder`, `toggleRightSidebar`, `switchRightSidebarToFiles`, `switchRightSidebarToFind`, `switchRightSidebarToSessions`, `switchRightSidebarToFeed`, `switchRightSidebarToDock`, `switchRightSidebarToMachines`, `palette.toggleMatchTerminalBackground`, `feed.show`, `showNotifications`, `markAllNotificationsRead`, `clearAllNotifications`, `notifications.toggleBanners`, `notifications.dismissal.keystroke`, `notifications.dismissal.focus`, `notifications.dismissal.click`, `notifications.dismissal.explicit`, `notifications.dismissal.timeout`, `notifications.dismissal.never`, `notifications.setAgentSound`, `notifications.setEventSound`, `notifications.importSound`, `palette.quickAgentChat`, `palette.startAgentFromAnyApp`, `palette.addHarness`, `stats.show`, `agentPane.stopReadingAloud`, `palette.openTerminalChatView`, `palette.launchClaudeTeams`, `palette.launchCodexTeams`, `palette.computerUse.setup`, `palette.computerUse.restart`, `palette.computerUse.accessibility`, `palette.computerUse.screenRecording`, `agent.openSessionWorkspace`, `agent.openSessionTab`, `home.toggleChiefSettings`, `home.stopChief`, `chief.openMemoryInspector`, `chief.toggleMemoryInspector`, `chief.moveTo`, `chief.importMemories`, `chief.importMemories.undo`, `chief.openUsage`, `chief.openViews`, `agentActivity.open`, `computerUseStop`, `agentChat.view`, `agent.harness.add`, `agent.harness.addFromRegistry`, `agent.harness.remove`, `agent.harness.restore`, `agent.harness.doctor`, `agent.harness.updateAll`, `newCloudMachine`, `cloudDiagnostics`, `openTeamPicker`, `palette.auth.signIn`, `palette.auth.signOut`, `palette.mobileConnect`, `accounts.show`, `accounts.refresh`, `accounts.reauthenticate`, `accounts.connect`, `accounts.remove`, `machine.add`, `remote.openBrowserTab`, `remote.openLocalBrowserTab`, `reloadConfiguration`, `palette.openCmuxSettingsFile`, `palette.openGhosttySettings`, `ghostty.showDiagnostics`, `palette.makeDefaultBrowser`, `palette.makeDefaultTerminal`, `palette.toggleSetting`, `palette.shortcutKeymap`, `palette.searchShortcuts`, `settings.customizeMenus`, `keybindings.open`, `palette.installCLI`, `palette.uninstallCLI`, `palette.restartSocketListener`, `palette.restartServer`, `palette.checkForUpdates`, `updates.whatsNew`, `announcements.show`, `announcements.hide`, `palette.applyUpdateIfAvailable`, `palette.switchAppChannel`, `updates.rollback`, `palette.pro.upgrade`, `sendFeedback`, `help.showCrashLogs`, `help.featureFlags`, `help.documentation`, `appearance.animationSpeed.fast`, `appearance.animationSpeed.normal`, `appearance.animationSpeed.off`, `appearance.animations.newTab.toggle`, `appearance.animations.newWorkspace.toggle`, `appearance.animations.toggle`, `appearance.animations.sidebar.toggle`, `appearance.animations.tabs.toggle`, `appearance.animations.panes.toggle`, `appearance.animations.overlays.toggle`, `appearance.animations.hoverCards.toggle`, `appearance.animations.home.toggle`, `appearance.animations.cursor.toggle`, `appearance.animations.indicators.toggle`, `appearance.animations.feedback.toggle`, `appearance.animations.web.toggle`, `appearance.animations.bounce.subtle`, `appearance.animations.bounce.none`, `browser.defaultEngine.chromium`, `browser.defaultEngine.webkit`, `appearance.paneBorder.toggle`, `appearance.panePadding.toggle`, `appearance.paneCorners.toggle`, `layout.centerFocusedColumn.never`, `layout.centerFocusedColumn.always`, `layout.centerFocusedColumn.onOverflow`, `tabs.overflow.scroll`, `tabs.overflow.shrink`, `links.openInCmuxBrowser`, `links.openInDefaultBrowser`, `links.signInPagesInCmux`, `links.signInPagesByLinkSettings`, `focusRing.toggle`, `focusRing.style.ring`, `focusRing.style.glow`, `focusRing.singlePane.toggle`, `appearance.paneBorderWidth.toggle`, `appearance.paneBorderColor.reset`, `appearance.titlebar.minimal`, `appearance.titlebar.standard`, `appearance.glass.lens.toggle`, `debug.actionLatencyHUD`, `debug.blockingCalls.reset`, `appearance.customize`, `help.troubleshooting`, `help.discord`, `help.followOnX`, `help.github`, `help.revealLogs`, `help.copyLogs`, `appearance.density.ultraCompact`, `appearance.density.compact`, `appearance.density.cozy`, `appearance.density.comfortable`, `appearance.density.spacious`, `appearance.density.set`, `appearance.density.create`, `appearance.chromeStyle.default`, `appearance.chromeStyle.card`, `appearance.chromeStyle.cardOutlined`, `appearance.chromeStyle.flat`, `appearance.chromeStyle.flatSidebarLine`, `appearance.chromeStyle.bare`, `appearance.chromeStyle.panes`, `appearance.chromeStyle.twoTone`, `appearance.chromeStyle.floating`, `browser.hibernation.off`, `browser.hibernation.moderate`, `browser.hibernation.aggressive`, `browser.keepNeighborTabsAwake.0`, `browser.keepNeighborTabsAwake.1`, `browser.keepNeighborTabsAwake.2`, `browser.keepNeighborTabsAwake.3`, `debug.autoArchive.age`, `layout.toggleStripScrollbar`, `window.toggleDockLeft`, `window.toggleDockRight`, `window.toggleDockTop`, `window.toggleDockBottom`, `window.toggleDockOrientation`, `recentlyFocused`, `recentlyClosed`, `history.commands`, `history.show`, `agentHistory.show`, `browserShowHistory`, `history.search`, `history.resumeAgentSession`, `history.reopen`, `history.open`, `history.clear`, `layout.undo`, `stash.show`, `stash.importOneTab`, `music.open`, `music.stop`, `voiceRoom.open`, `voiceRoom.stop`, `appStore.showInstalled`, `app.hide`, `app.unhide`, `app.open`, `app.command.run`, `tasks.show`, `tasks.new`, `tasks.showMine`, `phone.show`, `phone.boot`, `phone.screenshot`, `phone.recordStart`, `phone.recordStop`, `phone.sendFeedback`, `phone.openURL`, `phone.paste`, `phone.list`, `phone.shutdown`, `phone.tap`, `phone.swipe`, `phone.type`, `phone.button`, `phone.ui`, `phone.appearance`, `phone.location`, `phone.locale`, `phone.install`, `phone.launch`, `phone.rotate`, `phone.logs`, `files.open`, `link.open`, `app.shareCmux`, `server.makeThisMacAServer`, `server.stopServing`, `server.addServer`, `server.showPanel`, `server.showHealth`, `import.list`, `import.card`, `import.suggest`, `import.open`, `diagnostics.viewHangReport` +**noObject** (304): `openSettings`, `newWindow`, `newIncognitoWindow`, `closeWindow`, `window.closeOthers`, `closeAllWindows`, `zoomWindow`, `selectNextWindow`, `selectPreviousWindow`, `minimizeWindow`, `toggleFullScreen`, `quit`, `quitKeepSessions`, `quitEndSessions`, `quitEndEverything`, `globalSearch`, `palette.open`, `commandPalette`, `palette.openTaskManager`, `palette.sleepyMode`, `keepMacAwake`, `about`, `workspace.closeAllInAllWindows`, `manageLayouts`, `palette.findWork`, `workspace.importClassic`, `session.moveFromBuild`, `palette.openFilesPane`, `palette.openFindPane`, `palette.openVaultPane`, `palette.openCloudPane`, `macScreen.stopSharing`, `macScreen.revokeViewer`, `reopenClosedBrowserPanel`, `home.show`, `home.openChief`, `home.tab`, `home.channels`, `home.switchView`, `home.enableChannels`, `home.disableChannels`, `home.toggleChiefPet`, `home.messageChiefPet`, `home.messageChiefFromAnyApp`, `home.attachFiles`, `home.newMessage`, `home.invite`, `home.inviteToTeam`, `home.newChief`, `home.createGroup`, `home.stopAllAgents`, `home.saveFlightRecording`, `home.messageTo`, `palette.browserClearHistory`, `importFromBrowser`, `password.importCSV`, `passwords.open`, `palette.enableBrowser`, `palette.disableBrowser`, `browser.allowAgentWithExtensions`, `browser.importFromFiles`, `browser.importCookies`, `screenshots.open`, `fileEditor.importEditorSettings`, `file.openRecoveredDraft`, `sidebar.profileMenu`, `browser.downloads.showFolder`, `toggleRightSidebar`, `switchRightSidebarToFiles`, `switchRightSidebarToFind`, `switchRightSidebarToSessions`, `switchRightSidebarToFeed`, `switchRightSidebarToDock`, `switchRightSidebarToMachines`, `palette.toggleMatchTerminalBackground`, `feed.show`, `showNotifications`, `markAllNotificationsRead`, `clearAllNotifications`, `notifications.toggleBanners`, `notifications.dismissal.keystroke`, `notifications.dismissal.focus`, `notifications.dismissal.click`, `notifications.dismissal.explicit`, `notifications.dismissal.timeout`, `notifications.dismissal.never`, `notifications.setAgentSound`, `notifications.setEventSound`, `notifications.importSound`, `palette.quickAgentChat`, `palette.startAgentFromAnyApp`, `palette.addHarness`, `stats.show`, `agentPane.stopReadingAloud`, `palette.openTerminalChatView`, `palette.launchClaudeTeams`, `palette.launchCodexTeams`, `palette.computerUse.setup`, `palette.computerUse.restart`, `palette.computerUse.accessibility`, `palette.computerUse.screenRecording`, `agent.openSessionWorkspace`, `agent.openSessionTab`, `home.toggleChiefSettings`, `home.stopChief`, `chief.openMemoryInspector`, `chief.toggleMemoryInspector`, `chief.moveTo`, `chief.importMemories`, `chief.importMemories.undo`, `chief.openUsage`, `chief.openViews`, `agentActivity.open`, `computerUseStop`, `agentChat.view`, `agent.harness.add`, `agent.harness.addFromRegistry`, `agent.harness.remove`, `agent.harness.restore`, `agent.harness.doctor`, `agent.harness.updateAll`, `newCloudMachine`, `cloudDiagnostics`, `openTeamPicker`, `palette.auth.signIn`, `palette.auth.signOut`, `palette.mobileConnect`, `accounts.githubConnect`, `accounts.show`, `accounts.refresh`, `accounts.reauthenticate`, `accounts.connect`, `accounts.remove`, `machine.add`, `remote.openBrowserTab`, `remote.openLocalBrowserTab`, `reloadConfiguration`, `palette.openCmuxSettingsFile`, `palette.openGhosttySettings`, `ghostty.showDiagnostics`, `palette.makeDefaultBrowser`, `palette.makeDefaultTerminal`, `palette.toggleSetting`, `palette.shortcutKeymap`, `palette.searchShortcuts`, `settings.customizeMenus`, `keybindings.open`, `palette.installCLI`, `palette.uninstallCLI`, `palette.restartSocketListener`, `palette.restartServer`, `palette.checkForUpdates`, `updates.whatsNew`, `announcements.show`, `announcements.hide`, `palette.applyUpdateIfAvailable`, `palette.switchAppChannel`, `updates.rollback`, `palette.pro.upgrade`, `sendFeedback`, `help.showCrashLogs`, `help.featureFlags`, `help.documentation`, `appearance.animationSpeed.fast`, `appearance.animationSpeed.normal`, `appearance.animationSpeed.off`, `appearance.animations.newTab.toggle`, `appearance.animations.newWorkspace.toggle`, `appearance.animations.toggle`, `appearance.animations.sidebar.toggle`, `appearance.animations.tabs.toggle`, `appearance.animations.panes.toggle`, `appearance.animations.overlays.toggle`, `appearance.animations.hoverCards.toggle`, `appearance.animations.home.toggle`, `appearance.animations.cursor.toggle`, `appearance.animations.indicators.toggle`, `appearance.animations.feedback.toggle`, `appearance.animations.web.toggle`, `appearance.animations.bounce.subtle`, `appearance.animations.bounce.none`, `browser.defaultEngine.chromium`, `browser.defaultEngine.webkit`, `appearance.paneBorder.toggle`, `appearance.panePadding.toggle`, `appearance.paneCorners.toggle`, `layout.centerFocusedColumn.never`, `layout.centerFocusedColumn.always`, `layout.centerFocusedColumn.onOverflow`, `tabs.overflow.scroll`, `tabs.overflow.shrink`, `links.openInCmuxBrowser`, `links.openInDefaultBrowser`, `links.signInPagesInCmux`, `links.signInPagesByLinkSettings`, `focusRing.toggle`, `focusRing.style.ring`, `focusRing.style.glow`, `focusRing.singlePane.toggle`, `appearance.paneBorderWidth.toggle`, `appearance.paneBorderColor.reset`, `appearance.titlebar.minimal`, `appearance.titlebar.standard`, `appearance.glass.lens.toggle`, `debug.actionLatencyHUD`, `debug.blockingCalls.reset`, `appearance.customize`, `help.troubleshooting`, `help.discord`, `help.followOnX`, `help.github`, `help.revealLogs`, `help.copyLogs`, `appearance.density.ultraCompact`, `appearance.density.compact`, `appearance.density.cozy`, `appearance.density.comfortable`, `appearance.density.spacious`, `appearance.density.set`, `appearance.density.create`, `appearance.chromeStyle.default`, `appearance.chromeStyle.card`, `appearance.chromeStyle.cardOutlined`, `appearance.chromeStyle.flat`, `appearance.chromeStyle.flatSidebarLine`, `appearance.chromeStyle.bare`, `appearance.chromeStyle.panes`, `appearance.chromeStyle.twoTone`, `appearance.chromeStyle.floating`, `browser.hibernation.off`, `browser.hibernation.moderate`, `browser.hibernation.aggressive`, `browser.keepNeighborTabsAwake.0`, `browser.keepNeighborTabsAwake.1`, `browser.keepNeighborTabsAwake.2`, `browser.keepNeighborTabsAwake.3`, `debug.autoArchive.age`, `layout.toggleStripScrollbar`, `window.toggleDockLeft`, `window.toggleDockRight`, `window.toggleDockTop`, `window.toggleDockBottom`, `window.toggleDockOrientation`, `recentlyFocused`, `recentlyClosed`, `history.commands`, `history.show`, `agentHistory.show`, `browserShowHistory`, `history.search`, `history.resumeAgentSession`, `history.reopen`, `history.open`, `history.clear`, `layout.undo`, `stash.show`, `stash.importOneTab`, `music.open`, `music.stop`, `voiceRoom.open`, `voiceRoom.stop`, `appStore.showInstalled`, `app.hide`, `app.unhide`, `app.open`, `app.command.run`, `tasks.show`, `tasks.new`, `tasks.showMine`, `phone.show`, `phone.boot`, `phone.screenshot`, `phone.recordStart`, `phone.recordStop`, `phone.sendFeedback`, `phone.openURL`, `phone.paste`, `phone.list`, `phone.shutdown`, `phone.tap`, `phone.swipe`, `phone.type`, `phone.button`, `phone.ui`, `phone.appearance`, `phone.location`, `phone.locale`, `phone.install`, `phone.launch`, `phone.rotate`, `phone.logs`, `files.open`, `link.open`, `app.shareCmux`, `server.makeThisMacAServer`, `server.stopServing`, `server.addServer`, `server.showPanel`, `server.showHealth`, `import.list`, `import.card`, `import.suggest`, `import.open`, `diagnostics.viewHangReport` **noTargetSurface** (97): `window.screenshot`, `taskManager.killProcess`, `toggleChecklistItemComplete`, `canvasOverview`, `canvasTidy`, `canvasAlignLeft`, `canvasAlignRight`, `canvasAlignTop`, `canvasAlignBottom`, `canvasEqualizeWidths`, `canvasEqualizeHeights`, `canvasDistributeHorizontally`, `canvasDistributeVertically`, `simulatorHome`, `simulatorRotateLeft`, `simulatorRotateRight`, `simulatorToggleAppearance`, `simulatorToggleSoftwareKeyboard`, `file.open`, `home.archiveChief`, `home.deleteChief`, `home.conversationInfo`, `home.renameConversation`, `home.addMembers`, `home.removeMember`, `home.leaveConversation`, `home.muteConversation`, `home.unmuteConversation`, `home.setChiefReplies`, `home.togglePin`, `home.chatColors`, `home.sendMessage`, `tabGroup.deleteSaved`, `tabGroup.reopenSaved`, `screenGroup.reopenSaved`, `screenGroup.deleteSaved`, `resumeCommandSet`, `resumeCommandEdit`, `resumeCommandClear`, `saveFilePreview`, `toggleFileEditorWordWrap`, `filePreviewOpenWith`, `filePreviewOpenExternally`, `filePreviewRevealInFinder`, `filePreviewRevealInFiles`, `diffViewerNextLine`, `diffViewerPreviousLine`, `diffViewerHalfPageDown`, `diffViewerHalfPageUp`, `diffViewerNextHunk`, `diffViewerPreviousHunk`, `diffViewerGoToBottom`, `diffViewerGoToTop`, `diffViewerSearch`, `diffViewerNextFile`, `diffViewerPreviousFile`, `palette.vscodeServeWebStop`, `palette.vscodeServeWebRestart`, `browser.openOnMachine`, `openDiffViewer`, `palette.openDirectoryDiffViewer`, `openMarkdownFile`, `fileEditorGotoLine`, `fileEditorReplace`, `fileEditorAction`, `browser.certificateWarning.proceed`, `browser.certificateWarning.goBack`, `browserProfile.openLink`, `fileExplorerOpenSelection`, `fileExplorerOpenSelectionFinderAlias`, `vaultOpenSession`, `vaultResumeInNewWorkspace`, `vaultCopyResumeCommand`, `vaultOpenPullRequest`, `checklistEditItem`, `checklistMarkInProgress`, `checklistCompleteItem`, `checklistRemoveItem`, `checklistOpenAsPane`, `checklistAttachImages`, `agentPane.git.commit`, `agentPane.git.push`, `agentPane.searchChats`, `agentPane.setModel`, `agentPane.setEffort`, `agentPane.setMode`, `cloudExec`, `cloudDomainList`, `cloudPublicationList`, `cloudListMachines`, `markdownSave`, `markdownLink`, `markdownBack`, `markdownForward`, `markdownReload`, `markdownResetTableWidths`, `markdownGoToTop` @@ -163,7 +163,7 @@ Palette 1228, CLI verbs 756, right-click 596, MCP tools 674. ## Exemptions: mcp -**credentials** (6): `home.invite`, `palette.auth.signIn`, `palette.auth.signOut`, `accounts.reauthenticate`, `accounts.connect`, `accounts.remove` +**credentials** (7): `home.invite`, `palette.auth.signIn`, `palette.auth.signOut`, `accounts.githubConnect`, `accounts.reauthenticate`, `accounts.connect`, `accounts.remove` **endsApp** (4): `quit`, `quitKeepSessions`, `quitEndSessions`, `quitEndEverything` diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index 1ef6f97cda8b..e319709abdf3 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -1974,7 +1974,8 @@ "tunable-source.CmuxNextApp/Groups/GroupOwnership.swift:pane#": "ea9fb3f1620a3181925ccb65b0db03ee8d1390a944f74b70bbdfe5d3ce1069fa", "tunable-source.CmuxNextApp/Groups/GroupOwnership.swift:screenGroup": "9bf30d8126208410f0cb5d8232a290c632a64da299a8f534a483b5ea789fa253", "tunable-source.CmuxNextApp/Groups/GroupOwnership.swift:workspace": "6ef65d6041005cf1d213febee060323f7431261cd74e70e732b2dcb399ce71ca", - "tunable-source.CmuxNextApp/Handlers/AccountsHandlers.swift:bind": "fe6a0963d8b71cef6220e24bd0fb0e7a57c869317b9fb3dcc3bf92ff37600808", + "tunable-source.CmuxNextApp/Handlers/AccountsHandlers.swift:bind": "c67b283d50f10172fc7a58cf095b5f4bc22c284f670d0e54208622bb0547bfe3", + "tunable-source.CmuxNextApp/Handlers/AccountsHandlers.swift:githubConnectResult": "eae0623357f1aeda6d1253abe95ef06f3663671088418ae8138d82cccb54c9d8", "tunable-source.CmuxNextApp/Handlers/AccountsHandlers.swift:provider": "58b2a039bf81bf2ef228f4f1901eb2497aca445c97ed92558bcaa215913f3bfd", "tunable-source.CmuxNextApp/Handlers/AddHarnessHandler.swift:bind": "e5474f6c8c19ddbff57017cfa68cd9032d3e30055ee3f911bec2feb8c19afb9e", "tunable-source.CmuxNextApp/Handlers/AddHarnessHandler.swift:needsFocus": "d15bd721667ea60bd8cfceec2104c0029f85d0a45fa15955e3ba887116ef284b", @@ -5837,7 +5838,7 @@ "tunable-source.CmuxNextSettings/Schema/SettingsSchema+ColorTransition.generated.swift:springResponsePath": "2d5b0fe1cd68ec7d80df9a0b551a73c468a83919e45d45134a5c0448568191ca", "tunable-source.CmuxNextSettings/Schema/SettingsSchema+ColorTransition.generated.swift:springResponseRange": "8f6fd93110cfcfd50fb4c1554c485117d75bbbf15a2985d196fb04bfa3562b07", "tunable-source.CmuxNextSettings/Schema/SettingsSchema+ColumnLayout.swift:descriptors": "f3b34f26347f92c01a27a5d73245ee3c10b44f5dfeb56bb268c74789531bf1c0", - "tunable-source.CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift:descriptors": "70aaf7541ea894edf03c43e350d86e18c13434cc1922d4cb9256915bb354988f", + "tunable-source.CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift:descriptors": "c1434b258a2d40e4bcfb95b56872c7bb8573782098103fcf548b8909fab10626", "tunable-source.CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift:enabledDefault": "24f040d60e699a35706ca65ce11bdd7c3e9bc4699005bfefda39c1a9ec638fea", "tunable-source.CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift:enabledPath": "67991ca9f4bd4c6f612c89287ef82d567b0a7ab2308ae7058b6039b15e2ced57", "tunable-source.CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift:pollIntervalDefault": "81e8cec5b81d19bd27b35f433e1bf14e8af20699515a38f8f0694bb2f2b06e7b", diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index d7b9a6367bde..fbad55ebae34 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -85395,8 +85395,8 @@ "text" : "GitHub Inbox" }, "help" : { - "key" : "settings.feed.github.enabled.help", - "text" : "Uses your gh login to read notifications and review requests on this Mac. Sign in with gh auth login first." + "key" : "settings.feed.github.enabled.summary", + "text" : "Inbox shows your GitHub notifications" }, "kept_on_reset_all" : false, "key" : "feed.github.enabled", @@ -85509,8 +85509,8 @@ } }, "title" : { - "key" : "settings.feed.github.enabled", - "text" : "Connect GitHub" + "key" : "settings.group.githubInbox", + "text" : "GitHub Inbox" }, "validation" : "portable" },