Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Sources/Cloud/CloudWorkspaceSidebarPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ struct CloudWorkspaceSidebarPresentation {
init?(workspace: Workspace, orderedPanelIDs: [UUID], usesLastSegmentPath: Bool) {
let state = workspace.cloudBindingState

func machineMetadata(for id: String) -> String? {
if let name = state.machineNames[id] { return name }
if let name = state.machineNames[SurfaceMachineID.cloud(id).rawValue] { return name }
return SurfaceCatalog.shared.machineInfo(for: .cloud(id))?.name
}

var cloudMachineIDs = Set(state.projectedResources.values.compactMap { $0.machine.cloudMachineID })
if let id = workspace.cloudVMID { cloudMachineIDs.insert(id) }
let deviceMachines = Self.deviceMachines(for: workspace)
Expand All @@ -57,9 +63,21 @@ struct CloudWorkspaceSidebarPresentation {
guard !machineIDs.isEmpty else { return nil }
deviceLabel = Self.deviceLabel(workspace: workspace, machines: deviceMachines)
let names = Dictionary(uniqueKeysWithValues: machineIDs.map { id in
let name = state.machineNames[id]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? id
let name = machineMetadata(for: id)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? id
return (id, name.isEmpty ? id : name)
})
// A restored Cloud terminal can publish its projection before the
// catalog has finished loading machine metadata. Do not turn that
// transient state into user-visible identity or directory copy: the
// raw VM id and "Directory unavailable" are loading placeholders, not
// the values the sidebar is meant to present.
let projectedCloudMachineIDs = Set(state.projectedResources.values.compactMap { resource in
resource.machine.cloudMachineID
})
guard projectedCloudMachineIDs.allSatisfy({ id in
guard machineMetadata(for: id) != nil else { return false }
return true
}) else { return nil }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Keep stable IDs in badge help/accessibility; width-dependent rows use
// them only when friendly names collide across machines.
let identities = machineIDs.sorted().map { id -> String in
Expand Down
7 changes: 5 additions & 2 deletions Sources/SidebarWorkspaceSnapshotFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct SidebarWorkspaceSnapshotFactory {
let showsPullRequestRows = detailVisibility.showsPullRequests && !settings.compactsAgentStatus
let orderedPanelIds = workspace.sidebarOrderedPanelIds()
let cloud = CloudWorkspaceSidebarPresentation(workspace: workspace, orderedPanelIDs: orderedPanelIds, usesLastSegmentPath: settings.usesLastSegmentPath)
let hasCloudProjection = workspace.cloudVMID != nil
|| workspace.cloudBindingState.projectedResources.values.contains { $0.machine.cloudMachineID != nil }
let taskStatusInput = SidebarWorkspaceTaskStatusSnapshot.capture(workspace: workspace, orderedPanelIds: orderedPanelIds)
let compactGitBranchSummaryText: String? = {
guard showsBranchDirectoryRows,
Expand All @@ -41,7 +43,7 @@ struct SidebarWorkspaceSnapshotFactory {
settings.branchDirectory.branchLayout == .inline else {
return []
}
return cloud?.directoryCandidates ?? compactDirectoryCandidatesList(orderedPanelIds: orderedPanelIds)
return cloud?.directoryCandidates ?? (hasCloudProjection ? [] : compactDirectoryCandidatesList(orderedPanelIds: orderedPanelIds))
}()
let compactBranchDirectoryCandidates = compactBranchDirectoryCandidatesList(
gitSummary: compactGitBranchSummaryText,
Expand All @@ -53,6 +55,7 @@ struct SidebarWorkspaceSnapshotFactory {
return []
}
if let cloud { return [.init(branch: nil, directoryCandidates: cloud.directoryCandidates)] }
if hasCloudProjection { return [] }
return verticalBranchDirectoryLines(orderedPanelIds: orderedPanelIds)
}()
let pullRequestRows: [SidebarWorkspaceSnapshotBuilder.PullRequestDisplay] = {
Expand Down Expand Up @@ -95,7 +98,7 @@ struct SidebarWorkspaceSnapshotFactory {
// The directory toggle itself, like the branch and PR ones, so
// the tooltip keeps it under Hide All Details.
directory: settings.details.showBranchDirectory
? (cloud?.directoryCandidates ?? compactDirectoryCandidatesList(orderedPanelIds: orderedPanelIds)).first
? (cloud?.directoryCandidates ?? (hasCloudProjection ? [] : compactDirectoryCandidatesList(orderedPanelIds: orderedPanelIds))).first
: nil,
orderedPanelIds: orderedPanelIds
))
Expand Down
10 changes: 3 additions & 7 deletions cmuxTests/CLICoderouterCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import Testing
@testable import cmux
#endif

// The CLI executable's CMUXCLI type is not part of the app test target. The
// provider-first alias is a pure routing helper shared with the app instead.
typealias CMUXCLI = CmuxTuiRemoteRouting

@Suite struct AgentAliasArgumentTests {
final class BundleProbe {}

Expand Down Expand Up @@ -670,16 +666,16 @@ extension CLINotifyProcessIntegrationRegressionTests {

func testProviderFirstAgentAliasAddsTheCanonicalSeparator() {
XCTAssertEqual(
CMUXCLI.vmAgentAliasArgs(["claude", "--machine", "vm-agent-test", "reply exactly pong"]),
CmuxTuiRemoteRouting.vmAgentAliasArgs(["claude", "--machine", "vm-agent-test", "reply exactly pong"]),
["--agent", "claude", "--machine", "vm-agent-test", "--", "reply exactly pong"]
)
XCTAssertEqual(
CMUXCLI.vmAgentAliasArgs(["codex", "--", "exec", "summarize"]),
CmuxTuiRemoteRouting.vmAgentAliasArgs(["codex", "--", "exec", "summarize"]),
["--agent", "codex", "--", "exec", "summarize"]
)
// Focus flags belong to `vm agent`, not to the agent's prompt.
XCTAssertEqual(
CMUXCLI.vmAgentAliasArgs(["claude", "--no-focus", "reply exactly pong"]),
CmuxTuiRemoteRouting.vmAgentAliasArgs(["claude", "--no-focus", "reply exactly pong"]),
["--agent", "claude", "--no-focus", "--", "reply exactly pong"]
)
}
Expand Down
4 changes: 2 additions & 2 deletions cmuxTests/CodexForkMonitorArgumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Testing
struct CodexForkMonitorArgumentTests {
@Test
func forwardsForkParentClaimToDetachedMonitor() {
let arguments = CMUXCLI.codexForkMonitorArguments(environment: [
let arguments = CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: [
"CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session",
"CMUX_AGENT_FORK_LAUNCH_ID": "launch-id",
"CMUX_CODEX_PID": "1234",
Expand All @@ -25,6 +25,6 @@ struct CodexForkMonitorArgumentTests {

@Test
func omitsForkArgumentsForNormalCodexMonitor() {
#expect(CMUXCLI.codexForkMonitorArguments(environment: [:]).isEmpty)
#expect(CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: [:]).isEmpty)
}
}
30 changes: 30 additions & 0 deletions cmuxTests/SidebarCloudWorkspaceBadgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ import Testing
@Suite(.serialized)
@MainActor
struct SidebarCloudWorkspaceBadgeTests {
@Test("Cloud sidebar waits for machine metadata before showing identity or directory")
func cloudProjectionLoadingDoesNotShowPlaceholders() throws {
let workspace = Workspace(title: "Project", initialSurface: .cloudVMLoading)
defer { workspace.teardownAllPanels() }
let panelID = try #require(workspace.focusedPanelId)
let machine = SurfaceMachineID.cloud("vm-loading")
workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: machine.rawValue, isBase: true)
workspace.cloudBindingState.updateCatalogMetadata(
resources: [panelID: SurfaceResourceID(machine: machine, kind: .terminal, key: "terminal")],
machineNames: [:]
)
workspace.updateCloudPanelDirectory(panelId: panelID, directory: nil)

let settings = SidebarTabItemSettingsSnapshot(defaults: Self.makeDefaults())
let factory = SidebarWorkspaceSnapshotFactory(workspace: workspace, settings: settings, showsAgentActivity: false)
let loading = factory.makeSnapshot()
#expect(loading.cloudWorkspaceLabel == nil)
#expect(loading.compactDirectoryCandidates.isEmpty)
#expect(loading.remoteWorkspaceBadgeLabel?.contains("Directory unavailable") != true)
#expect(loading.remoteWorkspaceBadgeLabel?.contains(machine.rawValue) != true)

workspace.cloudBindingState.updateCatalogMetadata(
resources: [panelID: SurfaceResourceID(machine: machine, kind: .terminal, key: "terminal")],
machineNames: [machine.rawValue: "Friendly machine"]
)
workspace.updateRemotePanelDirectory(panelId: panelID, directory: "/home/cmux/project")
let loaded = factory.makeSnapshot()
#expect(loaded.cloudWorkspaceLabel?.contains("Friendly machine") == true)
}

@Test(arguments: [false, true])
func deviceNameIsVisibleBesideItsDirectory(vertical: Bool) throws {
let defaults = Self.makeDefaults()
Expand Down
4 changes: 2 additions & 2 deletions cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ struct SurfaceMachineIDDeviceEncodingTests {
#expect(payload["link_error"] as? String == "cloud_api_unavailable")
#expect(payload["link_error_message"] as? String == "cmux cannot reach the Cloud service for this machine right now.")

let lines = CMUXCLI.vmTreeLines(
let lines = CmuxTuiRemoteRouting.vmTreeLines(
machine: [
"id": "brave-otter",
"status": "running",
Expand All @@ -141,7 +141,7 @@ struct SurfaceMachineIDDeviceEncodingTests {
#expect(lines.contains { $0.contains("cmux cannot reach the Cloud service") })
#expect(!lines.contains { $0.contains("cloud_api_unavailable") })

let fallbackLines = CMUXCLI.vmTreeLines(
let fallbackLines = CmuxTuiRemoteRouting.vmTreeLines(
machine: [
"id": "brave-otter",
"status": "running",
Expand Down
Loading