From 18de41f29c590f758cb341fee1f9ec0dbee20edb Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 May 2026 00:10:15 +0900 Subject: [PATCH] Improve the zero-repository empty state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar: drop the misleading "Add your first repository" arrow hint (the Add Repository button moved to the toolbar) and show a centered "Repositories you add will appear here" placeholder instead; the detail pane's EmptyStateView carries the actual Add Repository button. - EmptyStateView: use the folder.badge.plus icon to match the toolbar's Add Repository control (was `tray`). - Force Normal view with zero repositories: `isShowingShelf` now also requires at least one repository, so a zero-repo "Default View = Shelf" launch can't stick on an empty Shelf — covering the launch race where the repository snapshot flips `isShelfActive` on before the empty entries reconcile repos back to zero. Also disable the Canvas/Shelf view-mode buttons while there are no repositories. - Nudge the sidebar (40pt) and active-agents (8pt) empty placeholders up for visual balance. - Add a test pinning the isShowingShelf zero-repo guard. --- .../Views/ActiveAgentsPanel.swift | 2 + .../Reducer/RepositoriesFeature.swift | 8 ++- .../Repositories/Views/EmptyStateView.swift | 2 +- .../Repositories/Views/SidebarListView.swift | 55 ++++++++++--------- supacodeTests/ShelfFeatureTests.swift | 28 ++++++++++ 5 files changed, 68 insertions(+), 27 deletions(-) diff --git a/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift b/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift index 0ed7a856..46562ac3 100644 --- a/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift +++ b/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift @@ -33,6 +33,8 @@ struct ActiveAgentsPanel: View { Text("New agents will appear here") .font(.callout) .foregroundStyle(.secondary) + // Nudge up slightly off dead-center for better visual balance. + .offset(y: -8) Spacer(minLength: 0) } else { ScrollView { diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift index 97106a29..ff0c219a 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift @@ -1706,7 +1706,13 @@ extension RepositoriesFeature.State { } var isShowingShelf: Bool { - isShelfActive + // Shelf needs at least one repository to render. Guarding here (not just + // on entry) also covers the launch race where the repository snapshot + // briefly repopulates books and flips `isShelfActive` on before the empty + // entries file reconciles repos back to zero — without this a zero-repo + // launch with "Default View = Shelf" would stick on an empty Shelf instead + // of falling back to Normal. + isShelfActive && !repositories.isEmpty } var topSegment: TopSegment { diff --git a/supacode/Features/Repositories/Views/EmptyStateView.swift b/supacode/Features/Repositories/Views/EmptyStateView.swift index 8d800bbf..7569678b 100644 --- a/supacode/Features/Repositories/Views/EmptyStateView.swift +++ b/supacode/Features/Repositories/Views/EmptyStateView.swift @@ -8,7 +8,7 @@ struct EmptyStateView: View { var body: some View { let shortcutDisplay = AppShortcuts.display(for: AppShortcuts.CommandID.openRepository, in: resolvedKeybindings) ContentUnavailableView { - Label("Open a repository or folder", systemImage: "tray") + Label("Open a repository or folder", systemImage: "folder.badge.plus") } description: { Text(promptText(shortcutDisplay: shortcutDisplay)) } actions: { diff --git a/supacode/Features/Repositories/Views/SidebarListView.swift b/supacode/Features/Repositories/Views/SidebarListView.swift index 22516ff4..6e433b27 100644 --- a/supacode/Features/Repositories/Views/SidebarListView.swift +++ b/supacode/Features/Repositories/Views/SidebarListView.swift @@ -79,9 +79,10 @@ struct SidebarListView: View { ScrollViewReader { scrollProxy in ScrollView { LazyVStack(spacing: 0) { - if repositoryItems.isEmpty { - emptyRepositoryHint() - } else { + // When there are no repositories the sidebar stays empty — the + // detail pane's `EmptyStateView` ("Open a repository or folder") + // carries the prompt and the Add Repository button instead. + if !repositoryItems.isEmpty { repositoryListHeader( action: repositoryListHeaderAction, expandableRepositoryIDs: expandableRepositoryIDs @@ -129,6 +130,18 @@ struct SidebarListView: View { } .padding(.vertical, 4) } + .overlay { + if repositoryItems.isEmpty { + Text("Repositories you add will appear here") + .font(.callout) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + // Sit above dead-center for better visual balance in the tall panel. + .offset(y: -40) + .accessibilityAddTraits(.isStaticText) + } + } .overlay(alignment: .bottom) { ActiveAgentsPanel( store: store.scope(state: \.activeAgents, action: \.activeAgents), @@ -192,13 +205,15 @@ struct SidebarListView: View { .canvas, systemImage: "square.grid.2x2", title: "Canvas", - shortcutCommandID: AppShortcuts.CommandID.toggleCanvas + shortcutCommandID: AppShortcuts.CommandID.toggleCanvas, + requiresRepository: true ) topSegmentButton( .shelf, systemImage: "distribute.horizontal.fill", title: "Shelf", - shortcutCommandID: AppShortcuts.CommandID.toggleShelf + shortcutCommandID: AppShortcuts.CommandID.toggleShelf, + requiresRepository: true ) } .background(.thinMaterial, in: Capsule()) @@ -210,11 +225,17 @@ struct SidebarListView: View { _ segment: TopSegment, systemImage: String, title: String, - shortcutCommandID: String? = nil + shortcutCommandID: String? = nil, + requiresRepository: Bool = false ) -> some View { let isSelected = store.topSegment == segment + // Canvas and Shelf need at least one repository; with none, only Normal + // (Default) is available, so disable them. + let isDisabled = requiresRepository && store.repositories.isEmpty let helpText = - shortcutCommandID.map { + isDisabled + ? "\(title) — add a repository first" + : shortcutCommandID.map { AppShortcuts.helpText(title: title, commandID: $0, in: resolvedKeybindings) } ?? title return Button { @@ -233,8 +254,10 @@ struct SidebarListView: View { } } .contentShape(.capsule) + .opacity(isDisabled ? 0.35 : 1) } .buttonStyle(.plain) + .disabled(isDisabled) .help(helpText) .accessibilityLabel(Text(title)) } @@ -290,24 +313,6 @@ struct SidebarListView: View { .padding(.bottom, 4) } - private func emptyRepositoryHint() -> some View { - HStack(spacing: 6) { - Spacer(minLength: 0) - Text("Add your first repository") - .font(.caption) - .foregroundStyle(.secondary) - Image(systemName: "arrow.turn.right.up") - .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) - .symbolEffect(.pulse, options: .repeating) - .accessibilityHidden(true) - } - .padding(.leading, 12) - .padding(.trailing, 14) - .padding(.top, 2) - .padding(.bottom, 6) - } - @ViewBuilder private func repositoryItemView( _ item: SidebarItem, diff --git a/supacodeTests/ShelfFeatureTests.swift b/supacodeTests/ShelfFeatureTests.swift index 4d8570ea..2a942fc6 100644 --- a/supacodeTests/ShelfFeatureTests.swift +++ b/supacodeTests/ShelfFeatureTests.swift @@ -929,4 +929,32 @@ struct ShelfFeatureTests { // No `.toggleShelf` here — the Layout Restore path is responsible. await store.finish() } + + @Test func isShowingShelfRequiresAtLeastOneRepository() { + let rootURL = URL(fileURLWithPath: "/tmp/repo") + let repository = Repository( + id: rootURL.path(percentEncoded: false), + rootURL: rootURL, + name: "repo", + worktrees: [] + ) + + // Active shelf with zero repositories falls back to Normal — covers the + // zero-repo launch race where `isShelfActive` is flipped on (from the + // repository snapshot) before the empty entries file reconciles repos + // back to empty. + var empty = RepositoriesFeature.State() + empty.isShelfActive = true + #expect(empty.repositories.isEmpty) + #expect(empty.isShowingShelf == false) + + // With a repository present, the active shelf renders. + var withRepo = RepositoriesFeature.State(repositories: [repository]) + withRepo.isShelfActive = true + #expect(withRepo.isShowingShelf == true) + + // Inactive shelf is never showing. + withRepo.isShelfActive = false + #expect(withRepo.isShowingShelf == false) + } } -- 2.51.2