native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Label code host actions with the detected host name

Replace the generic "Open on Code Host" wording in menus, command palette
and toolbar with the concrete host ("Open on GitHub", "Open on GitLab",
etc.) when the repository's remote identifies the host. Falls back to
"Code Host" when the remote is unknown so the binding title in Settings
stays repo-agnostic.

onevcat afdda3e0 66633fc0

+124 -11
+3 -2
supacode/Commands/WorktreeCommands.swift
··· 21 21 let hasActiveWorktree = repositories.worktree(for: repositories.selectedWorktreeID) != nil 22 22 let orderedRows = visibleHotkeyWorktreeRows ?? repositories.orderedWorktreeRows() 23 23 let codeHostWorktreeID = selectedCodeHostWorktreeID 24 + let codeHostLabel = "Open on \(repositories.codeHost(forWorktreeID: codeHostWorktreeID).displayName)" 24 25 let deleteShortcut = KeyboardShortcut(.delete, modifiers: [.command, .shift]).display 25 26 let customCommands = store.selectedCustomCommands 26 27 CommandMenu("Worktrees") { ··· 73 74 .modifier(KeyboardShortcutModifier(shortcut: keyboardShortcut(for: AppShortcuts.CommandID.openWorktree))) 74 75 .help(helpText(title: "Open Worktree", commandID: AppShortcuts.CommandID.openWorktree)) 75 76 .disabled(openSelectedWorktreeAction == nil) 76 - Button("Open on Code Host") { 77 + Button(codeHostLabel) { 77 78 if let codeHostWorktreeID { 78 79 store.send(.repositories(.githubIntegration(.pullRequestAction(codeHostWorktreeID, .openOnCodeHost)))) 79 80 } 80 81 } 81 82 .modifier(KeyboardShortcutModifier(shortcut: keyboardShortcut(for: AppShortcuts.CommandID.openPullRequest))) 82 - .help(helpText(title: "Open on Code Host", commandID: AppShortcuts.CommandID.openPullRequest)) 83 + .help(helpText(title: codeHostLabel, commandID: AppShortcuts.CommandID.openPullRequest)) 83 84 .disabled(codeHostWorktreeID == nil) 84 85 Button("New Worktree", systemImage: "plus") { 85 86 store.send(.repositories(.worktreeCreation(.createRandomWorktree)))
+35
supacode/Domain/CodeHost.swift
··· 1 + import Foundation 2 + 3 + nonisolated enum CodeHost: Equatable, Hashable, Sendable { 4 + case github 5 + case gitlab 6 + case bitbucket 7 + case codeberg 8 + case sourcehut 9 + case gitea 10 + case unknown 11 + 12 + var displayName: String { 13 + switch self { 14 + case .github: "GitHub" 15 + case .gitlab: "GitLab" 16 + case .bitbucket: "Bitbucket" 17 + case .codeberg: "Codeberg" 18 + case .sourcehut: "SourceHut" 19 + case .gitea: "Gitea" 20 + case .unknown: "Code Host" 21 + } 22 + } 23 + 24 + static func from(host: String?) -> CodeHost { 25 + guard let host, !host.isEmpty else { return .unknown } 26 + let lowered = host.lowercased() 27 + if lowered.contains("github") { return .github } 28 + if lowered.contains("gitlab") { return .gitlab } 29 + if lowered.contains("bitbucket") { return .bitbucket } 30 + if lowered.contains("codeberg") { return .codeberg } 31 + if lowered == "sr.ht" || lowered.hasSuffix(".sr.ht") || lowered.contains("sourcehut") { return .sourcehut } 32 + if lowered.contains("gitea") { return .gitea } 33 + return .unknown 34 + } 35 + }
+7 -4
supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift
··· 282 282 return [] 283 283 } 284 284 285 + let codeHost = repositories.codeHost(for: repositoryID) 285 286 let pullRequest = repositories.worktreeInfo(for: selectedWorktreeID)?.pullRequest 286 287 if repository.capabilities.supportsPullRequests, 287 288 let pullRequest, ··· 291 292 return pullRequestItems( 292 293 pullRequest: pullRequest, 293 294 worktreeID: selectedWorktreeID, 294 - repositoryID: repositoryID 295 + repositoryID: repositoryID, 296 + codeHost: codeHost 295 297 ) 296 298 } 297 299 ··· 302 304 return [ 303 305 CommandPaletteItem( 304 306 id: CommandPaletteItemID.pullRequestOpen(repositoryID), 305 - title: "Open Repository on Code Host", 307 + title: "Open Repository on \(codeHost.displayName)", 306 308 subtitle: repository.name, 307 309 kind: .openRepositoryOnCodeHost(selectedWorktreeID), 308 310 priorityTier: 2 ··· 313 315 private func pullRequestItems( 314 316 pullRequest: GithubPullRequest, 315 317 worktreeID: Worktree.ID, 316 - repositoryID: Repository.ID 318 + repositoryID: Repository.ID, 319 + codeHost: CodeHost 317 320 ) -> [CommandPaletteItem] { 318 321 let state = pullRequest.state.uppercased() 319 322 let isOpen = state == "OPEN" ··· 387 390 var items: [CommandPaletteItem] = [ 388 391 CommandPaletteItem( 389 392 id: CommandPaletteItemID.pullRequestOpen(repositoryID), 390 - title: "Open Pull Request on GitHub", 393 + title: "Open Pull Request on \(codeHost.displayName)", 391 394 subtitle: pullRequest.title, 392 395 kind: .openPullRequest(worktreeID), 393 396 priorityTier: 2
+1 -1
supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift
··· 522 522 case .archiveWorktree: 523 523 base = "Archive \(row.title)" 524 524 case .openPullRequest, .openRepositoryOnCodeHost: 525 - base = "Open on Code Host" 525 + base = row.title 526 526 case .markPullRequestReady: 527 527 base = "Mark pull request ready for review" 528 528 case .mergePullRequest:
+3
supacode/Features/Repositories/Reducer/RepositoriesFeature+RepositoryManagement.swift
··· 147 147 } 148 148 ) 149 149 } 150 + if let effect = detectCodeHostsEffect(for: state.repositories) { 151 + allEffects.append(effect) 152 + } 150 153 return .merge(allEffects) 151 154 152 155 case .requestRemoveRepository(let repositoryID):
+55
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 216 216 var pendingPullRequestRefreshByRepositoryID: [Repository.ID: PendingPullRequestRefresh] = [:] 217 217 var inFlightPullRequestRefreshRepositoryIDs: Set<Repository.ID> = [] 218 218 var queuedPullRequestRefreshByRepositoryID: [Repository.ID: PendingPullRequestRefresh] = [:] 219 + var codeHostByRepositoryID: [Repository.ID: CodeHost] = [:] 219 220 var sidebarSelectedWorktreeIDs: Set<Worktree.ID> = [] 220 221 var nextPendingSidebarRevealID = 0 221 222 var pendingSidebarReveal: PendingSidebarReveal? ··· 267 268 case refreshWorktrees 268 269 case reloadRepositories(animated: Bool) 269 270 case repositoriesLoaded([Repository], failures: [LoadFailure], roots: [URL], animated: Bool) 271 + case codeHostsDetected([Repository.ID: CodeHost]) 270 272 case selectArchivedWorktrees 271 273 case selectCanvas 272 274 case toggleCanvas ··· 592 594 if state.archivedAutoDeletePeriod != nil { 593 595 allEffects.append(.send(.autoDeleteExpiredArchivedWorktrees)) 594 596 } 597 + if repositoriesChanged, 598 + let effect = detectCodeHostsEffect(for: state.repositories) 599 + { 600 + allEffects.append(effect) 601 + } 595 602 return .merge(allEffects) 596 603 604 + case .codeHostsDetected(let codeHostByRepositoryID): 605 + let knownIDs = Set(state.repositories.ids) 606 + var updated = state.codeHostByRepositoryID.filter { knownIDs.contains($0.key) } 607 + for (id, host) in codeHostByRepositoryID where knownIDs.contains(id) { 608 + updated[id] = host 609 + } 610 + state.codeHostByRepositoryID = updated 611 + return .none 612 + 597 613 case .selectArchivedWorktrees: 598 614 state.selection = .archivedWorktrees 599 615 state.sidebarSelectedWorktreeIDs = [] ··· 875 891 } 876 892 } 877 893 894 + func detectCodeHostsEffect(for repositories: IdentifiedArrayOf<Repository>) -> Effect<Action>? { 895 + let targets = 896 + repositories 897 + .filter { $0.capabilities.supportsCodeHost } 898 + .map { (id: $0.id, rootURL: $0.rootURL) } 899 + guard !targets.isEmpty else { return nil } 900 + let gitClient = gitClient 901 + return .run { send in 902 + var detected: [Repository.ID: CodeHost] = [:] 903 + await withTaskGroup(of: (Repository.ID, CodeHost).self) { group in 904 + for target in targets { 905 + group.addTask { 906 + let host = await gitClient.repositoryWebURL(target.rootURL)?.host 907 + return (target.id, CodeHost.from(host: host)) 908 + } 909 + } 910 + for await (id, host) in group { 911 + detected[id] = host 912 + } 913 + } 914 + // `codeHost(for:)` defaults to `.unknown`, so storing `.unknown` 915 + // explicitly is a no-op. Skip the round trip when nothing is known. 916 + let meaningful = detected.filter { $0.value != .unknown } 917 + guard !meaningful.isEmpty else { return } 918 + await send(.codeHostsDetected(meaningful)) 919 + } 920 + } 921 + 878 922 func refreshRepositoryPullRequests( 879 923 repositoryID: Repository.ID, 880 924 repositoryRootURL: URL, ··· 1353 1397 1354 1398 func worktreeInfo(for worktreeID: Worktree.ID) -> WorktreeInfoEntry? { 1355 1399 worktreeInfoByID[worktreeID] 1400 + } 1401 + 1402 + func codeHost(for repositoryID: Repository.ID) -> CodeHost { 1403 + codeHostByRepositoryID[repositoryID] ?? .unknown 1404 + } 1405 + 1406 + func codeHost(forWorktreeID worktreeID: Worktree.ID?) -> CodeHost { 1407 + guard let worktreeID, let repositoryID = repositoryID(containing: worktreeID) else { 1408 + return .unknown 1409 + } 1410 + return codeHost(for: repositoryID) 1356 1411 } 1357 1412 1358 1413 func worktreesForInfoWatcher() -> [Worktree] {
+4 -2
supacode/Features/Repositories/Views/PullRequestStatusButton.swift
··· 2 2 3 3 struct PullRequestStatusButton: View { 4 4 let model: PullRequestStatusModel 5 + let codeHost: CodeHost 5 6 @Environment(CommandKeyObserver.self) private var commandKeyObserver 6 7 @Environment(\.resolvedKeybindings) private var resolvedKeybindings 7 8 ··· 38 39 } 39 40 40 41 private var openPullRequestLabel: String { 42 + let base = "Open on \(codeHost.displayName)" 41 43 let shortcut = AppShortcuts.resolvedShortcut( 42 44 for: AppShortcuts.CommandID.openPullRequest, 43 45 in: resolvedKeybindings 44 46 )?.display 45 47 if let shortcut { 46 - return "Open on GitHub \(shortcut)" 48 + return "\(base) \(shortcut)" 47 49 } 48 - return "Open on GitHub" 50 + return base 49 51 } 50 52 } 51 53
+2 -1
supacode/Features/Repositories/Views/ToolbarStatusView.swift
··· 3 3 struct ToolbarStatusView: View { 4 4 let toast: RepositoriesFeature.StatusToast? 5 5 let pullRequest: GithubPullRequest? 6 + let codeHost: CodeHost 6 7 7 8 var body: some View { 8 9 Group { ··· 38 39 .transition(.opacity) 39 40 case nil: 40 41 if let model = PullRequestStatusModel(pullRequest: pullRequest) { 41 - PullRequestStatusButton(model: model) 42 + PullRequestStatusButton(model: model, codeHost: codeHost) 42 43 .transition(.opacity) 43 44 } else { 44 45 MotivationalStatusView()
+5 -1
supacode/Features/Repositories/Views/WorktreeDetailView.swift
··· 165 165 title: title, 166 166 statusToast: input.repositories.statusToast, 167 167 pullRequest: matchesBranch ? pullRequest : nil, 168 + codeHost: input.repositories.codeHost(forWorktreeID: input.selectedWorktree?.id), 168 169 notificationGroups: input.notificationGroups, 169 170 unseenNotificationWorktreeCount: input.unseenNotificationWorktreeCount, 170 171 openActionSelection: input.openActionSelection, ··· 411 412 let title: DetailToolbarTitle 412 413 let statusToast: RepositoriesFeature.StatusToast? 413 414 let pullRequest: GithubPullRequest? 415 + let codeHost: CodeHost 414 416 let notificationGroups: [ToolbarNotificationRepositoryGroup] 415 417 let unseenNotificationWorktreeCount: Int 416 418 let openActionSelection: OpenWorktreeAction ··· 449 451 ToolbarItemGroup { 450 452 ToolbarStatusView( 451 453 toast: toolbarState.statusToast, 452 - pullRequest: toolbarState.pullRequest 454 + pullRequest: toolbarState.pullRequest, 455 + codeHost: toolbarState.codeHost 453 456 ) 454 457 .padding(.horizontal) 455 458 } ··· 884 887 title: DetailToolbarTitle(kind: .branch(name: "feature/toolbar-preview")), 885 888 statusToast: nil, 886 889 pullRequest: nil, 890 + codeHost: .github, 887 891 notificationGroups: [], 888 892 unseenNotificationWorktreeCount: 0, 889 893 openActionSelection: .finder,
+9
supacodeTests/CommandPaletteFeatureTests.swift
··· 202 202 203 203 let searchedItems = CommandPaletteFeature.filterItems(items: items, query: "code host") 204 204 #expect(searchedItems.contains(where: { $0.id == openItem?.id })) 205 + 206 + var githubState = state 207 + githubState.codeHostByRepositoryID[repository.id] = .github 208 + let githubItems = CommandPaletteFeature.commandPaletteItems(from: githubState) 209 + let githubOpenItem = githubItems.first { 210 + if case .openRepositoryOnCodeHost = $0.kind { return true } 211 + return false 212 + } 213 + #expect(githubOpenItem?.title == "Open Repository on GitHub") 205 214 } 206 215 207 216 @Test func emptyQueryHidesChangeFocusedTabIcon() {