native macOS codings agent orchestrator
5
fork

Configure Feed

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

feat(settings): add custom repo title for sidebar

Adds an optional `customTitle` field to `RepositorySettings`, edited
from a new "Display Name" section in Repo Settings. The sidebar
header prefers this title over the folder-derived name; whitespace-
only values normalize to nil so the placeholder folder name remains
the fallback.

+152 -7
+48 -3
supacode/Features/Repositories/Views/RepoHeaderRow.swift
··· 1 + import Sharing 1 2 import SwiftUI 2 3 3 4 struct RepoHeaderRow: View { ··· 25 26 size: 14 26 27 ) 27 28 } 28 - Text(name) 29 - .foregroundStyle(.secondary) 30 - .help(nameTooltip ?? "") 29 + RepoHeaderTitleText( 30 + fallbackName: name, 31 + repositoryRootURL: repositoryRootURL, 32 + nameTooltip: nameTooltip 33 + ) 31 34 if isRemoving { 32 35 Text("Removing...") 33 36 .font(.caption) ··· 44 47 } 45 48 } 46 49 } 50 + } 51 + } 52 + 53 + /// Resolves the repo header label, preferring the user's custom 54 + /// title from `RepositorySettings` over the folder-derived fallback. 55 + /// Subscription is isolated to this leaf so the parent header view 56 + /// doesn't re-evaluate when unrelated settings churn. 57 + private struct RepoHeaderTitleText: View { 58 + let fallbackName: String 59 + let repositoryRootURL: URL? 60 + let nameTooltip: String? 61 + 62 + var body: some View { 63 + if let repositoryRootURL { 64 + RepoHeaderTitleTextResolved( 65 + rootURL: repositoryRootURL, 66 + fallbackName: fallbackName, 67 + nameTooltip: nameTooltip 68 + ) 69 + } else { 70 + Text(fallbackName) 71 + .foregroundStyle(.secondary) 72 + .help(nameTooltip ?? "") 73 + } 74 + } 75 + } 76 + 77 + private struct RepoHeaderTitleTextResolved: View { 78 + let fallbackName: String 79 + let nameTooltip: String? 80 + @Shared private var settings: RepositorySettings 81 + 82 + init(rootURL: URL, fallbackName: String, nameTooltip: String?) { 83 + self.fallbackName = fallbackName 84 + self.nameTooltip = nameTooltip 85 + _settings = Shared(wrappedValue: .default, .repositorySettings(rootURL)) 86 + } 87 + 88 + var body: some View { 89 + Text(settings.customTitle ?? fallbackName) 90 + .foregroundStyle(.secondary) 91 + .help(nameTooltip ?? "") 47 92 } 48 93 } 49 94
+5
supacode/Features/RepositorySettings/Reducer/RepositorySettingsFeature.swift
··· 275 275 normalizedSettings.worktreeBaseDirectoryPath, 276 276 repositoryRootURL: rootURL 277 277 ) 278 + let trimmedCustomTitle = 279 + normalizedSettings.customTitle? 280 + .trimmingCharacters(in: .whitespacesAndNewlines) 281 + normalizedSettings.customTitle = 282 + (trimmedCustomTitle?.isEmpty ?? true) ? nil : trimmedCustomTitle 278 283 @Shared(.repositorySettings(rootURL)) var repositorySettings 279 284 @Shared(.userRepositorySettings(rootURL)) var userRepositorySettings 280 285 $repositorySettings.withLock { $0 = normalizedSettings }
+9 -2
supacode/Features/Settings/Models/RepositorySettings.swift
··· 15 15 var copyIgnoredOnWorktreeCreate: Bool? 16 16 var copyUntrackedOnWorktreeCreate: Bool? 17 17 var pullRequestMergeStrategy: PullRequestMergeStrategy? 18 + var customTitle: String? 18 19 private var schemaVersion: Int 19 20 20 21 private enum CodingKeys: String, CodingKey { ··· 28 29 case copyIgnoredOnWorktreeCreate 29 30 case copyUntrackedOnWorktreeCreate 30 31 case pullRequestMergeStrategy 32 + case customTitle 31 33 } 32 34 33 35 static let `default` = RepositorySettings( ··· 39 41 worktreeBaseDirectoryPath: nil, 40 42 copyIgnoredOnWorktreeCreate: nil, 41 43 copyUntrackedOnWorktreeCreate: nil, 42 - pullRequestMergeStrategy: nil 44 + pullRequestMergeStrategy: nil, 45 + customTitle: nil 43 46 ) 44 47 45 48 init( ··· 51 54 worktreeBaseDirectoryPath: String? = nil, 52 55 copyIgnoredOnWorktreeCreate: Bool? = nil, 53 56 copyUntrackedOnWorktreeCreate: Bool? = nil, 54 - pullRequestMergeStrategy: PullRequestMergeStrategy? = nil 57 + pullRequestMergeStrategy: PullRequestMergeStrategy? = nil, 58 + customTitle: String? = nil 55 59 ) { 56 60 self.setupScript = setupScript 57 61 self.archiveScript = archiveScript ··· 62 66 self.copyIgnoredOnWorktreeCreate = copyIgnoredOnWorktreeCreate 63 67 self.copyUntrackedOnWorktreeCreate = copyUntrackedOnWorktreeCreate 64 68 self.pullRequestMergeStrategy = pullRequestMergeStrategy 69 + self.customTitle = customTitle 65 70 schemaVersion = Self.currentSchemaVersion 66 71 } 67 72 ··· 86 91 try container.decodeIfPresent(String.self, forKey: .worktreeBaseRef) 87 92 worktreeBaseDirectoryPath = 88 93 try container.decodeIfPresent(String.self, forKey: .worktreeBaseDirectoryPath) 94 + customTitle = 95 + try container.decodeIfPresent(String.self, forKey: .customTitle) 89 96 if decodedSchemaVersion >= Self.currentSchemaVersion { 90 97 copyIgnoredOnWorktreeCreate = 91 98 try container.decodeIfPresent(
+16
supacode/Features/Settings/Views/RepositorySettingsView.swift
··· 63 63 get: { settings.worktreeBaseDirectoryPath.wrappedValue ?? "" }, 64 64 set: { settings.worktreeBaseDirectoryPath.wrappedValue = $0 }, 65 65 ) 66 + let customTitle = Binding( 67 + get: { settings.customTitle.wrappedValue ?? "" }, 68 + set: { settings.customTitle.wrappedValue = $0 }, 69 + ) 66 70 let exampleWorktreePath = store.exampleWorktreePath 71 + let folderName = Repository.name(for: store.rootURL) 67 72 68 73 Form { 74 + Section { 75 + TextField(folderName, text: customTitle) 76 + .textFieldStyle(.roundedBorder) 77 + } header: { 78 + VStack(alignment: .leading, spacing: 4) { 79 + Text("Display Name") 80 + Text("Custom name shown in the sidebar. Leave empty to use the folder name.") 81 + .foregroundStyle(.secondary) 82 + } 83 + } 84 + 69 85 Section { 70 86 RepositoryAppearancePickerView(store: store) 71 87 } header: {
+74 -2
supacodeTests/RepositorySettingsFeatureTests.swift
··· 135 135 key: "b", 136 136 modifiers: UserCustomShortcutModifiers(command: true) 137 137 ) 138 - ) 138 + ), 139 139 ] 140 140 ) 141 141 ··· 149 149 #expect(decoded.customCommands.first?.shortcut == conflicted.customCommands.first?.shortcut) 150 150 } 151 151 152 + @Test(.dependencies) func customTitleBindingPersistsToRepositoryFile() async throws { 153 + let rootURL = URL(fileURLWithPath: "/tmp/repo-\(UUID().uuidString)") 154 + let settingsStorage = SettingsTestStorage() 155 + let localStorage = RepositoryLocalSettingsTestStorage() 156 + let settingsFileURL = URL(fileURLWithPath: "/tmp/supacode-settings-\(UUID().uuidString).json") 157 + let repositorySettingsURL = SupacodePaths.repositorySettingsURL(for: rootURL) 158 + 159 + // Pre-seed a per-repo settings file so save() writes through to it 160 + // instead of falling back to the global settings file. 161 + let seedData = try #require(try? JSONEncoder().encode(RepositorySettings.default)) 162 + try #require(try? localStorage.save(seedData, at: repositorySettingsURL)) 163 + 164 + let store = TestStore( 165 + initialState: RepositorySettingsFeature.State( 166 + rootURL: rootURL, 167 + repositoryKind: .plain, 168 + settings: .default, 169 + userSettings: .default 170 + ) 171 + ) { 172 + RepositorySettingsFeature() 173 + } withDependencies: { 174 + $0.settingsFileStorage = settingsStorage.storage 175 + $0.settingsFileURL = settingsFileURL 176 + $0.repositoryLocalSettingsStorage = localStorage.storage 177 + } 178 + 179 + await store.send(.binding(.set(\.settings.customTitle, "My Custom Repo"))) { 180 + $0.settings.customTitle = "My Custom Repo" 181 + } 182 + await store.receive(\.delegate.settingsChanged) 183 + 184 + let savedData = try #require(localStorage.data(at: repositorySettingsURL)) 185 + let decoded = try JSONDecoder().decode(RepositorySettings.self, from: savedData) 186 + #expect(decoded.customTitle == "My Custom Repo") 187 + } 188 + 189 + @Test(.dependencies) func customTitleWhitespaceOnlyPersistsAsNil() async throws { 190 + let rootURL = URL(fileURLWithPath: "/tmp/repo-\(UUID().uuidString)") 191 + let settingsStorage = SettingsTestStorage() 192 + let localStorage = RepositoryLocalSettingsTestStorage() 193 + let settingsFileURL = URL(fileURLWithPath: "/tmp/supacode-settings-\(UUID().uuidString).json") 194 + let repositorySettingsURL = SupacodePaths.repositorySettingsURL(for: rootURL) 195 + 196 + let seedData = try #require(try? JSONEncoder().encode(RepositorySettings.default)) 197 + try #require(try? localStorage.save(seedData, at: repositorySettingsURL)) 198 + 199 + let store = TestStore( 200 + initialState: RepositorySettingsFeature.State( 201 + rootURL: rootURL, 202 + repositoryKind: .plain, 203 + settings: .default, 204 + userSettings: .default 205 + ) 206 + ) { 207 + RepositorySettingsFeature() 208 + } withDependencies: { 209 + $0.settingsFileStorage = settingsStorage.storage 210 + $0.settingsFileURL = settingsFileURL 211 + $0.repositoryLocalSettingsStorage = localStorage.storage 212 + } 213 + 214 + await store.send(.binding(.set(\.settings.customTitle, " "))) { 215 + $0.settings.customTitle = " " 216 + } 217 + await store.receive(\.delegate.settingsChanged) 218 + 219 + let savedData = try #require(localStorage.data(at: repositorySettingsURL)) 220 + let decoded = try JSONDecoder().decode(RepositorySettings.self, from: savedData) 221 + #expect(decoded.customTitle == nil) 222 + } 223 + 152 224 @Test(.dependencies) func taskLoadsLatestUserSettingsAfterAsyncGitProbe() async throws { 153 225 let rootURL = URL(fileURLWithPath: "/tmp/repo-\(UUID().uuidString)") 154 226 let settingsStorage = SettingsTestStorage() ··· 167 239 command: "echo updated", 168 240 execution: .shellScript, 169 241 shortcut: nil 170 - ) 242 + ), 171 243 ] 172 244 ) 173 245