···335335 var effects: [Effect<Action>] = [
336336 .send(.settings(.setSelection(.general))),
337337 .send(.commandPalette(.pruneRecency(recencyIDs))),
338338+ .send(.repositories(.refreshAllCustomTitles)),
338339 .run { _ in
339340 await terminalClient.send(.prune(ids))
340341 },
···353354 }
354355 var effects: [Effect<Action>] = [
355356 .send(.commandPalette(.pruneRecency(recencyIDs))),
357357+ .send(.repositories(.refreshAllCustomTitles)),
356358 .run { _ in
357359 await terminalClient.send(.prune(ids))
358360 },
···787789 }
788790789791 case .settings(.repositorySettings(.delegate(.settingsChanged(let rootURL)))):
792792+ // Always refresh the repo's custom title cache — display sites
793793+ // (sidebar, shelf, canvas, toolbar, settings list) read it from
794794+ // `RepositoriesFeature.State.repositoryCustomTitles` rather
795795+ // than subscribing to the per-repo settings file directly.
796796+ let refreshCustomTitle = Effect<Action>.send(.repositories(.refreshCustomTitle(rootURL)))
790797 guard let selectedWorktree = state.repositories.selectedTerminalWorktree,
791798 selectedWorktree.repositoryRootURL == rootURL
792799 else {
793793- return .none
800800+ return refreshCustomTitle
794801 }
795802 let worktreeID = selectedWorktree.id
796803 @Shared(.repositorySettings(rootURL)) var repositorySettings
797804 @Shared(.userRepositorySettings(rootURL)) var userRepositorySettings
798805 return .concatenate(
806806+ refreshCustomTitle,
799807 .send(.worktreeSettingsLoaded(repositorySettings, worktreeID: worktreeID)),
800808 .send(.worktreeUserSettingsLoaded(userRepositorySettings, worktreeID: worktreeID))
801809 )
+26-5
supacode/Features/Canvas/Views/CanvasView.swift
···77 @Environment(\.resolvedKeybindings) private var resolvedKeybindings
8899 let terminalManager: WorktreeTerminalManager
1010+ /// Per-repo display titles resolved by the parent reducer. Used to
1111+ /// override the folder-derived `Repository.name` on each card title
1212+ /// bar without subscribing to per-repo settings files on the
1313+ /// per-frame canvas hot path.
1414+ var repositoryCustomTitles: [Repository.ID: String] = [:]
1015 var onExitToTab: () -> Void = {}
1116 @State private var layoutStore = CanvasLayoutStore()
1217 @Shared(.repositoryAppearances) private var repositoryAppearances
···8590 let cardTotalHeight = resized.size.height + titleBarHeight
86918792 let repositoryAppearance = appearance(for: state.repositoryRootURL)
9393+ let resolvedRepositoryName = repositoryDisplayName(for: state.repositoryRootURL)
8894 CanvasCardView(
8989- repositoryName: Repository.name(for: state.repositoryRootURL),
9595+ repositoryName: resolvedRepositoryName,
9096 worktreeName: tab.title,
9197 repositoryIcon: repositoryAppearance.icon,
9298 repositoryColor: repositoryAppearance.color?.color,
···780786 /// dict. Returns `.empty` when no entry exists, which keeps cards
781787 /// visually identical to before the appearance feature shipped.
782788 private func appearance(for repositoryRootURL: URL) -> RepositoryAppearance {
783783- let id =
784784- PathPolicy.normalizePath(
785785- repositoryRootURL.path(percentEncoded: false), resolvingSymlinks: true
786786- ) ?? repositoryRootURL.path(percentEncoded: false)
789789+ let id = repositoryID(for: repositoryRootURL)
787790 return repositoryAppearances[id] ?? .empty
791791+ }
792792+793793+ /// Resolves the user-defined display title for the repo at this root
794794+ /// URL, falling back to `Repository.name(for:)` (folder name) when no
795795+ /// custom title was set. Reads from the static dictionary populated
796796+ /// by the parent reducer — no per-call `@Shared` subscription on the
797797+ /// canvas hot path.
798798+ private func repositoryDisplayName(for repositoryRootURL: URL) -> String {
799799+ let id = repositoryID(for: repositoryRootURL)
800800+ return repositoryCustomTitles[id] ?? Repository.name(for: repositoryRootURL)
801801+ }
802802+803803+ /// Mirrors the same path normalization the `Repository.ID` is built
804804+ /// from, so dict lookups match what the reducer stores.
805805+ private func repositoryID(for repositoryRootURL: URL) -> Repository.ID {
806806+ PathPolicy.normalizePath(
807807+ repositoryRootURL.path(percentEncoded: false), resolvingSymlinks: true
808808+ ) ?? repositoryRootURL.path(percentEncoded: false)
788809 }
789810}
790811
···11+import SwiftUI
22+33+/// Renders the repository display label, preferring a user-defined
44+/// `customTitle` over the folder-derived `fallbackName`.
55+///
66+/// Stateless on purpose: the source of truth for `customTitle` lives
77+/// in `RepositoriesFeature.State.repositoryCustomTitles` (refreshed by
88+/// the reducer when settings change), so display sites read a plain
99+/// string and avoid per-row `@Shared(.repositorySettings(...))`
1010+/// subscriptions on the hot path. Callers apply their own font /
1111+/// foreground style modifiers — this view stays appearance-agnostic.
1212+struct RepoDisplayName: View {
1313+ let fallbackName: String
1414+ var customTitle: String?
1515+ var tooltip: String?
1616+1717+ var body: some View {
1818+ Text(customTitle ?? fallbackName)
1919+ .help(tooltip ?? "")
2020+ }
2121+}
···3838 /// Clicking a previously-unopened worktree in the left navigation
3939 /// while in Shelf mode adds its ID here, which causes its spine to
4040 /// materialize (with the standard spine-flow animation).
4141- func orderedShelfBooks() -> [ShelfBook] {
4141+ /// Builds the ordered list of shelf books from current state.
4242+ ///
4343+ /// `customTitles` is an optional dictionary providing user-defined
4444+ /// display names per repository. Defaults to empty for callers that
4545+ /// don't care (e.g. legacy tests). The resolved `projectName` (and
4646+ /// `displayName` for plain folders) prefers the custom title when
4747+ /// present and falls back to `repository.name` otherwise.
4848+ func orderedShelfBooks(
4949+ customTitles: [Repository.ID: String] = [:]
5050+ ) -> [ShelfBook] {
4251 // `ShelfView.body` re-runs on every TCA state change, so this method
4352 // is on the per-frame hot path. The previous implementation built a
4453 // `Dictionary(uniqueKeysWithValues:)` per call and routed worktree
···5160 var books: [ShelfBook] = []
5261 for repositoryID in orderedRepositoryIDs() {
5362 guard let repository = repositories[id: repositoryID] else { continue }
6363+ let projectName = customTitles[repositoryID] ?? repository.name
5464 if repository.kind == .plain {
5565 guard openedWorktreeIDs.contains(repository.id) else { continue }
5666 books.append(
5767 ShelfBook(
5868 id: repository.id,
5969 repositoryID: repository.id,
6060- displayName: repository.name,
6161- projectName: repository.name,
7070+ displayName: projectName,
7171+ projectName: projectName,
6272 branchName: nil,
6373 kind: .plainFolder
6474 ))
···7181 id: worktree.id,
7282 repositoryID: repositoryID,
7383 displayName: worktree.name,
7474- projectName: repository.name,
8484+ projectName: projectName,
7585 branchName: worktree.name,
7686 kind: .worktree
7787 ))
···90100 id: pending.id,
91101 repositoryID: repositoryID,
92102 displayName: pending.progress.titleText,
9393- projectName: repository.name,
103103+ projectName: projectName,
94104 branchName: pending.progress.titleText,
95105 kind: .worktree
96106 ))
+1-1
supacode/Features/Shelf/Views/ShelfView.swift
···3131 // sanity-checking how often the root re-renders during animation.
3232 let _ = shelfLogger.event("ShelfView.body")
3333 let state = store.state
3434- let books = state.orderedShelfBooks()
3434+ let books = state.orderedShelfBooks(customTitles: state.repositoryCustomTitles)
3535 let openBookID = state.openShelfBookID
3636 let openIndex = openBookID.flatMap { id in
3737 books.firstIndex(where: { $0.id == id })