Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a7642b0
fix: gate Cloud command palette actions by capability
austinywang Sep 30, 2026
c113282
Merge main (02b80fecc984) into issue-16266-cloud-command-palette-parity
austinywang Sep 30, 2026
9408824
refactor: centralize Cloud palette capability policy
austinywang Sep 30, 2026
4ef7b1c
fix: instantiate Cloud palette capability policy
austinywang Sep 30, 2026
4f625f2
test: pin audited Cloud palette capability sets
austinywang Sep 30, 2026
ba7689a
fix: complete Cloud palette localization and docs
austinywang Sep 30, 2026
5e7852b
fix: defer cloud palette availability alert
austinywang Oct 1, 2026
6191d48
fix: tighten Cloud palette capability guidance
austinywang Oct 1, 2026
eea0219
feat: gate Cloud palette actions by VM capabilities
austinywang Oct 1, 2026
516546b
Merge main (8a5b39c4ab15) into issue-16266-cloud-command-palette-parity
austinywang Oct 1, 2026
ff1ae6a
test: serialize Cloud palette feature flag coverage
austinywang Oct 1, 2026
bf5ae8f
Merge main (8395a5b4301c) into issue-16266-cloud-command-palette-parity
austinywang Oct 1, 2026
dffe328
test: reproduce Cloud restore and terminal chat palette gaps
austinywang Oct 1, 2026
ddd8a57
fix: align Cloud palette capability gates with CLI routes
austinywang Oct 1, 2026
f70220c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Oct 1, 2026
85ab349
fix: remove duplicate titlebar accent environment
austinywang Oct 1, 2026
ea89f4a
fix: use accent color value in notification row
austinywang Oct 1, 2026
4354ea5
fix: unwrap browser restoration document URL
austinywang Oct 1, 2026
9c526cd
fix: restore test window geometry reset helper
austinywang Oct 1, 2026
6780b63
fix: restore visible notification anchor lookup
austinywang Oct 1, 2026
40d7f75
fix: keep test geometry reset private
austinywang Oct 1, 2026
60c1851
fix: keep notification anchor test on shared lookup
austinywang Oct 1, 2026
ded840b
fix: repair inherited app-host test compilation
austinywang Oct 1, 2026
a22dbeb
fix: include CLI error in CLI test target
austinywang Oct 1, 2026
72e28b6
fix: stabilize inherited agent notification fixtures
austinywang Oct 1, 2026
745b486
fix: route cloud palette commands through invoking window
austinywang Oct 1, 2026
5172b31
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Oct 1, 2026
43269cd
fix: expose palette window context to auth commands
austinywang Oct 1, 2026
f86ca7a
fix: complete Cloud command palette parity
austinywang Oct 1, 2026
2b2efde
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Oct 1, 2026
ceae107
chore: drop unrelated command palette PR diffs
austinywang Oct 1, 2026
34b2f0e
fix: isolate command palette identifiers on main actor
austinywang Oct 1, 2026
b8f9819
Merge origin/main into issue-16266-cloud-command-palette-parity
austinywang Oct 1, 2026
3378f5d
fix: expose Foundation types in paste transfer policy
austinywang Oct 1, 2026
05b2e4b
fix: remove redundant usage owner optional chain
austinywang Oct 1, 2026
ec9bd34
fix: use local shell quoting for SSH paste commands
austinywang Oct 1, 2026
f41ba17
fix: pass SSH paste policy to custom uploads
austinywang Oct 1, 2026
b614104
Merge origin/main into issue-16266-cloud-command-palette-parity
austinywang Oct 1, 2026
efb7796
chore: preserve main localization catalog layout
austinywang Oct 1, 2026
233e9e2
Merge main (51b60f3b3025) into issue-16266-cloud-command-palette-parity
austinywang Oct 2, 2026
6ceef0b
chore: preserve main localization catalog layout
austinywang Oct 2, 2026
b7d854f
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Oct 2, 2026
01b8454
Merge main (134c9d9473b6) into issue-16266-cloud-command-palette-parity
austinywang Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ final class FakeConnector: CloudTerminalConnecting, @unchecked Sendable {
return session
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ public struct CommandPaletteContextKeys: Hashable, Sendable {

/// Whether a workspace is selected.
public static let hasWorkspace = CommandPaletteContextKeys(rawValue: "workspace.hasSelection")
/// Whether the selected workspace is owned by a managed Cloud machine.
///
/// The app sets this key from the workspace's explicit managed Cloud VM
/// identity. Generic SSH and other remote workspaces intentionally do not
/// set it; legacy managed Cloud SSH workspaces do.
public static let workspaceIsCloud = CommandPaletteContextKeys(rawValue: "workspace.isCloud")
/// Whether the selected Cloud VM has a server-provided capability snapshot.
public static let cloudVMCapabilitiesKnown = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.capabilitiesKnown")
/// Whether the selected Cloud VM supports fork.
public static let cloudVMSupportsFork = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.supportsFork")
/// Whether the selected Cloud VM supports checkpoint or template promotion.
public static let cloudVMSupportsSnapshot = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.supportsSnapshot")
/// Whether the selected Cloud VM supports restoring a checkpoint.
public static let cloudVMSupportsRestore = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.supportsRestore")
/// Whether the selected Cloud VM can provide port previews.
public static let cloudVMSupportsPorts = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.supportsPorts")
/// Whether the selected Cloud VM permits control-plane command execution.
public static let cloudVMSupportsExec = CommandPaletteContextKeys(rawValue: "workspace.cloudVM.supportsExec")
/// Selected workspace display name.
public static let workspaceName = CommandPaletteContextKeys(rawValue: "workspace.name")
/// Whether the workspace has a custom name.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Foundation

/// Describes where a command-palette action can run relative to a managed
/// Cloud workspace.
public enum CommandPaletteCloudCapability: Equatable, Sendable {
/// The action is valid for both local and Cloud workspaces.
case shared
/// The action needs a selected Cloud workspace and its current VM.
case cloudOnly
/// The action creates or reads a resource on this Mac's local filesystem,
/// browser stack, or simulator and cannot target a Cloud workspace.
case localOnly
}

/// Classifies built-in command-palette actions before they are materialized.
/// The app owns command contributions and handlers; this package owns the
/// pure capability decision so it can be tested without app singletons.
public struct CommandPaletteCloudCapabilityPolicy: Sendable {
/// Creates the stateless capability policy.
public init() {}

/// Returns the Cloud capability for a built-in command ID. Unknown and
/// user-configured actions remain shared unless they add their own gate.
public func capability(for commandId: String) -> CommandPaletteCloudCapability {
if commandId.hasPrefix("palette.terminalOpenDirectory.") {
return .localOnly
}

switch commandId {
case "palette.cloud.fork",
"palette.cloud.snapshot",
"palette.cloud.promoteTemplate",
"palette.cloud.status",
"palette.cloud.ports",
"palette.cloud.tools",
"palette.cloud.handoff":
return .cloudOnly
case "palette.newBrowserWorkspace",
"palette.newAgentChat",
"palette.newSimulatorPane",
"palette.openFolder",
"palette.openFolderInVSCodeInline",
"palette.openWorkspacePullRequests",
"palette.openDiffViewer",
"palette.openDirectoryDiffViewer",
"palette.findInDirectory",
"palette.vscodeServeWebStop",
"palette.vscodeServeWebRestart",
"palette.terminalAttachTextBoxFile",
"palette.openTerminalChatView":
return .localOnly
default:
return .shared
}
}

/// Returns whether a command can be materialized for the supplied context.
/// Cloud-only commands require the selected Cloud workspace; local-only
/// commands are omitted while a Cloud workspace is selected.
public func allows(
commandId: String,
context: CommandPaletteContextSnapshot
) -> Bool {
switch capability(for: commandId) {
case .shared:
// Restore creates a new VM from a supplied snapshot and remains
// available from a local workspace, but a selected Cloud VM can
// advertise that the provider does not support restore.
guard commandId == "palette.cloud.restore",
context.bool(CommandPaletteContextKeys.workspaceIsCloud),
context.bool(CommandPaletteContextKeys.cloudVMCapabilitiesKnown) else {
return true
}
return context.bool(CommandPaletteContextKeys.cloudVMSupportsRestore)
case .cloudOnly:
guard context.bool(CommandPaletteContextKeys.workspaceIsCloud) else {
return false
}
guard context.bool(CommandPaletteContextKeys.cloudVMCapabilitiesKnown) else {
return true
}
switch commandId {
case "palette.cloud.fork":
return context.bool(CommandPaletteContextKeys.cloudVMSupportsFork)
case "palette.cloud.snapshot", "palette.cloud.promoteTemplate":
return context.bool(CommandPaletteContextKeys.cloudVMSupportsSnapshot)
case "palette.cloud.restore":
return context.bool(CommandPaletteContextKeys.cloudVMSupportsRestore)
case "palette.cloud.ports":
return context.bool(CommandPaletteContextKeys.cloudVMSupportsPorts)
case "palette.cloud.tools":
return context.bool(CommandPaletteContextKeys.cloudVMSupportsExec)
default:
return true
}
case .localOnly:
return !context.bool(CommandPaletteContextKeys.workspaceIsCloud)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import CmuxCommandPalette
import Testing

@Suite("Command palette Cloud capability policy")
struct CommandPaletteCloudCapabilityPolicyTests {
private let policy = CommandPaletteCloudCapabilityPolicy()

/// Representative command IDs cover each capability classification.
@Test("classifies representative shared, Cloud-only, and local-only commands")
func representativeCommandsHaveExpectedCapabilities() {
#expect(
policy.capability(for: "palette.newTerminalTab") == .shared
)
#expect(
policy.capability(for: "palette.terminalSplitRight") == .shared
)
#expect(
policy.capability(for: "palette.browserBack") == .shared
)
#expect(
policy.capability(for: "palette.cloud.status") == .cloudOnly
)
#expect(
policy.capability(for: "palette.cloud.fork") == .cloudOnly
)
#expect(
policy.capability(for: "palette.cloud.restore") == .shared
)
#expect(
policy.capability(for: "palette.cloud.newMachine") == .shared
)
#expect(
policy.capability(for: "palette.browserSplitRight") == .shared
)
#expect(
policy.capability(for: "palette.openDirectoryDiffViewer") == .localOnly
)
#expect(
policy.capability(for: "palette.terminalOpenDirectory.finder") == .localOnly
)
#expect(
policy.capability(for: "palette.openTerminalChatView") == .localOnly
)
}

/// The audited sets stay explicit so a new palette action cannot silently drift.
@Test("keeps the audited Cloud and local-only command sets explicit")
func auditedCapabilitySetsRemainStable() {
let cloudOnly = [
"palette.cloud.fork",
"palette.cloud.snapshot",
"palette.cloud.promoteTemplate",
"palette.cloud.status",
"palette.cloud.ports",
"palette.cloud.tools",
"palette.cloud.handoff",
]
let localOnly = [
"palette.newBrowserWorkspace",
"palette.newAgentChat",
"palette.newSimulatorPane",
"palette.openFolder",
"palette.openFolderInVSCodeInline",
"palette.openWorkspacePullRequests",
"palette.openDiffViewer",
"palette.openDirectoryDiffViewer",
"palette.findInDirectory",
"palette.vscodeServeWebStop",
"palette.vscodeServeWebRestart",
"palette.terminalAttachTextBoxFile",
"palette.openTerminalChatView",
"palette.terminalOpenDirectory.finder",
]

#expect(cloudOnly.allSatisfy { policy.capability(for: $0) == .cloudOnly })
#expect(localOnly.allSatisfy { policy.capability(for: $0) == .localOnly })
#expect(policy.capability(for: "palette.newTerminalTab") == .shared)
#expect(policy.capability(for: "palette.newBrowserTab") == .shared)
#expect(policy.capability(for: "palette.browserSplitRight") == .shared)
#expect(policy.capability(for: "palette.browserSplitDown") == .shared)
#expect(policy.capability(for: "palette.terminalSplitBrowserRight") == .shared)
#expect(policy.capability(for: "palette.terminalSplitBrowserDown") == .shared)
#expect(policy.capability(for: "palette.browserBack") == .shared)
#expect(policy.capability(for: "palette.cloud.restore") == .shared)
}

/// Context gating allows only capabilities valid for the selected workspace.
@Test("scopes Cloud-only and local-only commands to the workspace context")
func contextAllowsOnlyValidCapabilities() {
let localContext = CommandPaletteContextSnapshot()
var cloudContext = CommandPaletteContextSnapshot()
cloudContext.setBool(CommandPaletteContextKeys.workspaceIsCloud, true)

#expect(
policy.allows(
commandId: "palette.terminalSplitRight",
context: localContext
)
)
#expect(
policy.allows(
commandId: "palette.terminalSplitRight",
context: cloudContext
)
)
#expect(
policy.allows(
commandId: "palette.cloud.status",
context: cloudContext
)
)
#expect(
!policy.allows(
commandId: "palette.cloud.status",
context: localContext
)
)
#expect(
policy.allows(
commandId: "palette.browserSplitRight",
context: localContext
)
)
#expect(
policy.allows(
commandId: "palette.browserSplitRight",
context: cloudContext
)
)
#expect(policy.allows(commandId: "palette.cloud.restore", context: localContext))
#expect(policy.allows(commandId: "palette.cloud.restore", context: cloudContext))
#expect(
!policy.allows(
commandId: "palette.openTerminalChatView",
context: cloudContext
)
)
#expect(
policy.allows(
commandId: "palette.openTerminalChatView",
context: localContext
)
)

cloudContext.setBool(CommandPaletteContextKeys.cloudVMCapabilitiesKnown, true)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsFork, false)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsSnapshot, false)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsRestore, false)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsPorts, false)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsExec, false)
#expect(!policy.allows(commandId: "palette.cloud.fork", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.snapshot", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.promoteTemplate", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.restore", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.ports", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.tools", context: cloudContext))
#expect(policy.allows(commandId: "palette.cloud.status", context: cloudContext))

cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsRestore, true)
cloudContext.setBool(CommandPaletteContextKeys.cloudVMSupportsPorts, true)
#expect(policy.allows(commandId: "palette.cloud.restore", context: cloudContext))
#expect(policy.allows(commandId: "palette.cloud.ports", context: cloudContext))
#expect(!policy.allows(commandId: "palette.cloud.tools", context: cloudContext))

// Restore remains shared for local workspaces because it creates a
// new VM from an explicitly supplied snapshot ID.
#expect(policy.allows(commandId: "palette.cloud.restore", context: localContext))
}
}
Loading
Loading