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
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ public struct SidebarSection: View {
SettingsCardRow(
configurationReview: .json("sidebar.showBranchDirectory"),
String(localized: "settings.app.showBranchDirectory", defaultValue: "Show Branch + Directory in Sidebar"),
subtitle: String(localized: "settings.app.showBranchDirectory.subtitle", defaultValue: "Display the built-in git branch and working-directory row.")
subtitle: String(localized: "settings.app.showBranchDirectory.subtitle", defaultValue: "Display git branches, Cloud machine info, and working directories.")
) {
Toggle("", isOn: Binding(get: { showBranchDir.current }, set: { showBranchDir.set($0) }))
.labelsHidden()
Expand Down
38 changes: 19 additions & 19 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -302888,115 +302888,115 @@
"ar": {
"stringUnit": {
"state": "translated",
"value": "عرض صف فرع git المدمج ودليل العمل."
"value": "عرض فروع git ومعلومات أجهزة Cloud وأدلة العمل."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Prikaži ugrađeni red za git granu i radni direktorij."
"value": "Prikaži git grane, informacije o Cloud mašinama i radne direktorije."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Vis den indbyggede git-gren og arbejdsmapperækken."
"value": "Vis git-grene, oplysninger om Cloud-maskiner og arbejdsmapper."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Die integrierte Git-Branch- und Arbeitsverzeichniszeile anzeigen."
"value": "Git-Branches, Informationen zu Cloud-Maschinen und Arbeitsverzeichnisse anzeigen."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Display the built-in git branch and working-directory row."
"value": "Display git branches, Cloud machine info, and working directories."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Mostrar la fila integrada de rama git y directorio de trabajo."
"value": "Mostrar ramas de git, información de máquinas Cloud y directorios de trabajo."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Afficher la ligne de branche git et de répertoire de travail intégrée."
"value": "Afficher les branches git, les informations des machines Cloud et les répertoires de travail."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Visualizza la riga integrata con il branch git e la directory di lavoro."
"value": "Mostra i branch git, le informazioni sulle macchine Cloud e le directory di lavoro."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "組み込みのgitブランチと作業ディレクトリの行を表示します。"
"value": "gitブランチ、Cloudマシンの情報、作業ディレクトリを表示します。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "기본 제공 git 브랜치 및 작업 디렉토리 행을 표시합니다."
"value": "git 브랜치, Cloud 머신 정보 및 작업 디렉토리를 표시합니다."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Vis den innebygde git-grenen og arbeidskatalograden."
"value": "Vis git-grener, informasjon om Cloud-maskiner og arbeidskataloger."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Wyświetlaj wbudowany wiersz gałęzi git i katalogu roboczego."
"value": "Wyświetlaj gałęzie git, informacje o maszynach Cloud i katalogi robocze."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Exibir a linha integrada de branch git e diretório de trabalho."
"value": "Exibir branches git, informações das máquinas Cloud e diretórios de trabalho."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Отображать встроенную строку ветки git и рабочего каталога."
"value": "Отображать ветки git, информацию о машинах Cloud и рабочие каталоги."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "แสดงแถวสาขา git และไดเรกทอรีทำงานในตัว"
"value": "แสดงสาขา git ข้อมูลเครื่อง Cloud และไดเรกทอรีทำงาน"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Yerleşik git dalı ve çalışma dizini satırını göster."
"value": "Git dallarını, Cloud makine bilgilerini ve çalışma dizinlerini göster."
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Відображати вбудований рядок з гілкою git та робочою текою."
"value": "Відображати гілки git, інформацію про машини Cloud та робочі каталоги."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "显示内置的 Git 分支和工作目录行。"
"value": "显示 Git 分支、Cloud 机器信息和工作目录。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "顯示內建的 git 分支及工作目錄列。"
"value": "顯示 Git 分支、Cloud 機器資訊和工作目錄。"
}
}
}
Expand Down
39 changes: 32 additions & 7 deletions Sources/Cloud/CloudWorkspaceSidebarPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ struct CloudWorkspaceSidebarPresentation {
var machineIDs = Set(state.projectedResources.values.compactMap { $0.machine.cloudMachineID })
if let id = workspace.cloudVMID { machineIDs.insert(id) }
guard !machineIDs.isEmpty else { return nil }
let identities = machineIDs.sorted().map { id -> String in
let names = Dictionary(uniqueKeysWithValues: machineIDs.map { id in
let name = state.machineNames[id]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? id
return name.isEmpty || name == id ? id : "\(name) (\(id))"
return (id, name.isEmpty ? id : name)
})
// 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
let name = names[id] ?? id
return name == id ? id : "\(name) (\(id))"
}
machineLabel = String.localizedStringWithFormat(
String(localized: "sidebar.cloudWorkspace.label", defaultValue: "Cloud workspace on %@"),
Expand All @@ -43,12 +49,31 @@ struct CloudWorkspaceSidebarPresentation {
? SidebarPathFormatter.pathCandidates(directory, homeDirectoryPath: "")
: [directory]
}
let full = zip(entries, paths).map { entry, paths -> String in
let name = state.machineNames[entry.identity]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? entry.identity
let identity = name.isEmpty || name == entry.identity ? entry.identity : "\(name) (\(entry.identity))"
return "\(identity) · \(paths.first ?? Self.unavailableDirectory)"
var grouped: [(identity: String, paths: [[String]])] = []
var groupIndexes: [String: Int] = [:]
for (entry, pathCandidates) in zip(entries, paths) {
if let index = groupIndexes[entry.identity] {
grouped[index].paths.append(pathCandidates)
} else {
groupIndexes[entry.identity] = grouped.count
grouped.append((entry.identity, [pathCandidates]))
}
}
var nameCounts: [String: Int] = [:]
for group in grouped {
nameCounts[names[group.identity, default: group.identity], default: 0] += 1
}
let visibleName: (String) -> String = { id in
let name = names[id] ?? id
guard name != id, nameCounts[name, default: 0] > 1 else { return name }
return "\(name) (\(id))"
}
let full = grouped.map { group in
"\(visibleName(group.identity)) · " + group.paths.map { $0.first ?? Self.unavailableDirectory }.joined(separator: ", ")
}.joined(separator: " | ")
let compact = grouped.map { group in
"\(visibleName(group.identity)) · " + group.paths.map { $0.last ?? Self.unavailableDirectory }.joined(separator: ", ")
}.joined(separator: " | ")
let compact = zip(entries, paths).map { "\($0.identity) · \($1.last ?? Self.unavailableDirectory)" }.joined(separator: " | ")
directoryCandidates = full == compact ? [full] : [full, compact]
}
}
2 changes: 1 addition & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16029,7 +16029,7 @@ struct TabItemView: View, Equatable {
.transition(.opacity)
}

SidebarCloudWorkspaceBadgeView(label: workspaceSnapshot.cloudWorkspaceLabel, pointSize: scaledFontSize(10), tint: activeSecondaryColor(0.7))
SidebarCloudWorkspaceBadgeView(label: detailVisibility.showsBranchDirectory ? workspaceSnapshot.cloudWorkspaceLabel : nil, pointSize: scaledFontSize(10), tint: activeSecondaryColor(0.7))

if isEditing {
SidebarInlineRenameField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {

// Title line
cloudImageView.configureSidebarWorkspaceAccessory(
symbol: "cloud", label: snapshot.cloudWorkspaceLabel,
symbol: "cloud", label: model.settings.visibleAuxiliaryDetails.showsBranchDirectory ? snapshot.cloudWorkspaceLabel : nil,
pointSize: model.scaled(10), tint: palette.secondary(0.7), weight: .regular
)
pinImageView.configureSidebarWorkspaceAccessory(
Expand Down
1 change: 1 addition & 0 deletions Sources/Sidebar/SidebarPathFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum SidebarPathFormatter {
) -> String {
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return path }
guard !homeDirectoryPath.isEmpty else { return trimmed }
if trimmed == homeDirectoryPath {
return "~"
}
Expand Down
123 changes: 121 additions & 2 deletions cmuxTests/CloudDirectoryLifecycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ struct CloudDirectoryLifecycleTests {
let sidebar = try fixture.sidebar()
#expect(sidebar.cloudWorkspaceLabel?.contains("Build server") == true)
#expect(sidebar.cloudWorkspaceLabel?.contains("cwd-machine") == true)
#expect(try fixture.sidebarText().contains("cwd-machine"))
#expect(try fixture.sidebarText().contains("Build server ·"))
#expect(try !fixture.sidebarText().contains("cwd-machine"))
#expect(fixture.workspace.cloudBindingState.revision > revision)
#expect(sidebar.accessibilityLabel(index: 0, workspaceCount: 1).contains("Build server"))
info.name = "Renamed server"
Expand All @@ -168,6 +169,73 @@ struct CloudDirectoryLifecycleTests {
#expect(fixture.workspace.title == "My explicit task title")
}

@Test("Every sidebar width uses the Cloud tree name, with IDs reserved for help", arguments: [
(nil as String?, "early-plum-alpaca" as String?, "early-plum-alpaca"),
("Build server", "early-plum-alpaca", "Build server"),
("", "early-plum-alpaca", "early-plum-alpaca"),
(nil, nil, "cwd-machine"),
("", "", "cwd-machine")
])
func machineNamePresentation(label: String?, slug: String?, expected: String) throws {
let fixture = try CloudDirectoryTestFixture()
defer { fixture.close() }
let binding = fixture.workspace.cloudVMBinding
let projections = fixture.catalog.projections
var summary = fixture.provider.summary
summary.displayName = label
summary.slug = slug
fixture.provider.update(summary: summary)
try fixture.changeDirectory("/home/cmux/a", terminal: 0)
try fixture.changeDirectory("/home/cmux/b", terminal: 1)

#expect(MachineSnapshotBuilder.snapshot(from: summary).displayName == expected)
#expect(fixture.provider.info.name == expected)
for usesLastSegmentPath in [false, true] {
let presentation = try #require(CloudWorkspaceSidebarPresentation(
workspace: fixture.workspace, orderedPanelIDs: fixture.panels,
usesLastSegmentPath: usesLastSegmentPath
))
let full = "\(expected) · /home/cmux/a, /home/cmux/b"
if usesLastSegmentPath {
#expect(presentation.directoryCandidates.count == 2)
#expect(presentation.directoryCandidates.first == full)
#expect(presentation.directoryCandidates.last == "\(expected) · …/a, …/b")
} else {
#expect(presentation.directoryCandidates == [full])
}
#expect(presentation.directoryCandidates.allSatisfy { $0.hasPrefix("\(expected) · ") })
#expect(presentation.machineLabel.contains(expected))
#expect(presentation.machineLabel.contains(fixture.machine.rawValue))
}
let snapshot = try fixture.sidebar()
let candidates = snapshot.compactDirectoryCandidates + snapshot.branchDirectoryLines.flatMap(\.directoryCandidates)
#expect(!candidates.isEmpty)
#expect(candidates.allSatisfy { $0.hasPrefix("\(expected) · ") })
#expect(snapshot.accessibilityLabel(index: 0, workspaceCount: 1).contains(fixture.machine.rawValue))
#expect(fixture.workspace.cloudVMBinding == binding)
#expect(fixture.catalog.projections == projections)
}

@Test("A renamed machine keeps its name while cwd is unavailable and safely falls back when cleared")
func unavailableDirectoryMachineName() throws {
let fixture = try CloudDirectoryTestFixture()
defer { fixture.close() }
try fixture.changeDirectory(nil, terminal: 0)
try fixture.changeDirectory(nil, terminal: 1)
for name in ["Build server", "Renamed server", "", " \n "] {
var info = fixture.provider.info
info.name = name
fixture.catalog.updateMachine(info, from: fixture.provider)
let expected = name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? fixture.machine.rawValue : name
let presentation = try #require(CloudWorkspaceSidebarPresentation(
workspace: fixture.workspace, orderedPanelIDs: fixture.panels, usesLastSegmentPath: true
))
#expect(presentation.directoryCandidates == ["\(expected) · \(CloudWorkspaceSidebarPresentation.unavailableDirectory)"])
#expect(fixture.workspace.cloudVMID == fixture.machine.rawValue)
}
}

@Test("Saved Cloud paths require fresh remote confirmation after restore")
func sessionRestore() throws {
let fixture = try CloudDirectoryTestFixture()
Expand Down Expand Up @@ -206,10 +274,61 @@ struct CloudDirectoryLifecycleTests {
))
#expect(workspace.reportedPanelDirectory(panelId: fixture.panels[0]) == "/srv/other")
#expect(try fixture.sidebar().cloudWorkspaceLabel?.contains("other-machine") == true)
#expect(try fixture.sidebarText().contains("other-machine ·"))
#expect(try fixture.sidebarText().contains("Other machine ·"))
#expect(workspace.title == "My explicit task title")
}

@Test("A machine name is shown once for multiple directories and collisions stay distinct")
func groupedMachineDirectories() throws {
let fixture = try CloudDirectoryTestFixture()
defer { fixture.close() }
try fixture.changeDirectory("/home/cmux/a", terminal: 0)
try fixture.changeDirectory("/home/cmux/b", terminal: 1)
var info = fixture.provider.info
info.name = "Build server"
fixture.catalog.updateMachine(info, from: fixture.provider)
let singleMachine = try #require(CloudWorkspaceSidebarPresentation(
workspace: fixture.workspace, orderedPanelIDs: fixture.panels, usesLastSegmentPath: false
))
#expect(singleMachine.directoryCandidates == ["Build server · /home/cmux/a, /home/cmux/b"])

let other = SurfaceMachineID.cloud("other-machine")
let otherPanel = fixture.panels[1]
let state = try #require(CmuxTuiSnapshotParser.state(fromSnapshot: [
"cursor": ["generation": "other", "revision": "1"],
"workspaces": [], "screens": [], "panes": [], "tabs": [],
"terminals": [["id": "other-terminal", "title": "bash", "cwd": "/srv/other", "lifecycle": "running"]],
"browsers": [], "agents": []
], machine: other))
let otherProvider = CmuxTuiSurfaceProvider(
summary: VMSummary(id: other.rawValue, provider: "freestyle", status: "running", image: "cmux-devbox", createdAt: 0, base: nil),
links: CloudMachineLinkManager(clientURL: nil, hostThemeColors: { nil }), catalog: fixture.catalog
)
fixture.catalog.register(otherProvider)
defer {
fixture.catalog.endProjections(panelID: otherPanel)
fixture.catalog.unregister(machine: other)
}
var otherInfo = otherProvider.info
otherInfo.name = "Build server"
fixture.catalog.replaceCloudState(state, resources: CmuxTuiSnapshotParser.resources(from: state), info: otherInfo)
fixture.catalog.endProjections(panelID: otherPanel, reason: .replaced)
fixture.catalog.record(SurfaceProjection(
resource: SurfaceResourceID(machine: other, kind: .terminal, key: "other-terminal"),
workspaceID: fixture.workspace.id, panelID: otherPanel
))
let collision = try #require(CloudWorkspaceSidebarPresentation(
workspace: fixture.workspace, orderedPanelIDs: fixture.panels, usesLastSegmentPath: false
))
#expect(collision.directoryCandidates == [
"Build server (cwd-machine) · /home/cmux/a | Build server (other-machine) · /srv/other"
])
let hiddenOtherMachine = try #require(CloudWorkspaceSidebarPresentation(
workspace: fixture.workspace, orderedPanelIDs: [fixture.panels[0]], usesLastSegmentPath: false
))
#expect(hiddenOtherMachine.directoryCandidates == ["Build server · /home/cmux/a"])
}

@Test("Local renderer OSC reports cannot overwrite the accepted Cloud graph")
func localRendererCannotClaimRemoteProvenance() throws {
let manager = TabManager(autoWelcomeIfNeeded: false)
Expand Down
Loading
Loading