Add minimal sidebar detail toggles (#1312)

* Add minimal sidebar detail toggles

* Address sidebar review comments
This commit is contained in:
Lawrence Chen 2026-03-12 21:22:51 -07:00 committed by GitHub
parent ac98625ebe
commit 9b0bf2f66d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 317 additions and 12 deletions

View file

@ -41436,6 +41436,57 @@
}
}
},
"settings.app.hideAllSidebarDetails": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hide All Sidebar Details"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サイドバーの詳細をすべて隠す"
}
}
}
},
"settings.app.hideAllSidebarDetails.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show secondary workspace details as controlled by the toggles below."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "下のトグル設定に従って、ワークスペースの補助情報を表示します。"
}
}
}
},
"settings.app.hideAllSidebarDetails.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show only the workspace title row. Overrides the detail toggles below."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペースのタイトル行だけを表示します。下の詳細トグルより優先されます。"
}
}
}
},
"settings.app.renameSelectsName": {
"extractionState": "manual",
"localizations": {
@ -42679,6 +42730,40 @@
}
}
},
"settings.app.showNotificationMessage": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show Notification Message in Sidebar"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サイドバーに通知メッセージを表示"
}
}
}
},
"settings.app.showNotificationMessage.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Display the latest notification message below the workspace title."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペース名の下に最新の通知メッセージを表示します。"
}
}
}
},
"settings.app.showPorts": {
"extractionState": "manual",
"localizations": {

View file

@ -7162,11 +7162,22 @@ struct VerticalTabsSidebar: View {
@StateObject private var dragFailsafeMonitor = SidebarDragFailsafeMonitor()
@State private var draggedTabId: UUID?
@State private var dropIndicator: SidebarDropIndicator?
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey)
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
/// Space at top of sidebar for traffic light buttons
private let trafficLightPadding: CGFloat = 28
private let tabRowSpacing: CGFloat = 2
private var showsSidebarNotificationMessage: Bool {
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: sidebarShowNotificationMessage,
hideAllDetails: sidebarHideAllDetails
)
}
var body: some View {
VStack(spacing: 0) {
GeometryReader { proxy in
@ -7187,7 +7198,10 @@ struct VerticalTabsSidebar: View {
tabCount: tabManager.tabs.count,
unreadCount: notificationStore.unreadCount(forTabId: tab.id),
latestNotificationText: {
guard let notification = notificationStore.latestNotification(forTabId: tab.id) else { return nil }
guard showsSidebarNotificationMessage,
let notification = notificationStore.latestNotification(forTabId: tab.id) else {
return nil
}
let text = notification.body.isEmpty ? notification.title : notification.body
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
@ -9493,6 +9507,8 @@ private struct TabItemView: View, Equatable {
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
@AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
private var activeTabIndicatorStyleRaw = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
@ -9591,17 +9607,30 @@ private struct TabItemView: View, Equatable {
)
}
private var visibleAuxiliaryDetails: SidebarWorkspaceAuxiliaryDetailVisibility {
SidebarWorkspaceAuxiliaryDetailVisibility.resolved(
showMetadata: sidebarShowMetadata,
showLog: sidebarShowLog,
showProgress: sidebarShowProgress,
showBranchDirectory: sidebarShowBranchDirectory,
showPullRequests: sidebarShowPullRequest,
showPorts: sidebarShowPorts,
hideAllDetails: sidebarHideAllDetails
)
}
var body: some View {
let closeWorkspaceTooltip = String(localized: "sidebar.closeWorkspace.tooltip", defaultValue: "Close Workspace")
let accessibilityHintText = String(localized: "sidebar.workspace.accessibilityHint", defaultValue: "Activate to focus this workspace. Drag to reorder, or use Move Up and Move Down actions.")
let moveUpActionText = String(localized: "sidebar.workspace.moveUpAction", defaultValue: "Move Up")
let moveDownActionText = String(localized: "sidebar.workspace.moveDownAction", defaultValue: "Move Down")
let latestNotificationSubtitle = latestNotificationText
let orderedPanelIds: [UUID]? = (sidebarShowBranchDirectory || sidebarShowPullRequest)
let detailVisibility = visibleAuxiliaryDetails
let orderedPanelIds: [UUID]? = (detailVisibility.showsBranchDirectory || detailVisibility.showsPullRequests)
? tab.sidebarOrderedPanelIds()
: nil
let compactGitBranchSummaryText: String? = {
guard sidebarShowBranchDirectory,
guard detailVisibility.showsBranchDirectory,
!sidebarBranchVerticalLayout,
sidebarShowGitBranch,
let orderedPanelIds else {
@ -9610,7 +9639,7 @@ private struct TabItemView: View, Equatable {
return gitBranchSummaryText(orderedPanelIds: orderedPanelIds)
}()
let compactDirectorySummaryText: String? = {
guard sidebarShowBranchDirectory,
guard detailVisibility.showsBranchDirectory,
!sidebarBranchVerticalLayout,
let orderedPanelIds else {
return nil
@ -9622,7 +9651,7 @@ private struct TabItemView: View, Equatable {
directorySummary: compactDirectorySummaryText
)
let branchDirectoryLines: [VerticalBranchDirectoryLine] = {
guard sidebarShowBranchDirectory,
guard detailVisibility.showsBranchDirectory,
sidebarBranchVerticalLayout,
let orderedPanelIds else {
return []
@ -9631,7 +9660,7 @@ private struct TabItemView: View, Equatable {
}()
let branchLinesContainBranch = sidebarShowGitBranch && branchDirectoryLines.contains { $0.branch != nil }
let pullRequestRows: [PullRequestDisplay] = {
guard sidebarShowPullRequest, let orderedPanelIds else { return [] }
guard detailVisibility.showsPullRequests, let orderedPanelIds else { return [] }
return pullRequestDisplays(orderedPanelIds: orderedPanelIds)
}()
@ -9709,7 +9738,7 @@ private struct TabItemView: View, Equatable {
.multilineTextAlignment(.leading)
}
if sidebarShowMetadata {
if detailVisibility.showsMetadata {
let metadataEntries = tab.sidebarStatusEntriesInDisplayOrder()
let metadataBlocks = tab.sidebarMetadataBlocksInDisplayOrder()
if !metadataEntries.isEmpty {
@ -9731,7 +9760,7 @@ private struct TabItemView: View, Equatable {
}
// Latest log entry
if sidebarShowLog, let latestLog = tab.logEntries.last {
if detailVisibility.showsLog, let latestLog = tab.logEntries.last {
HStack(spacing: 4) {
Image(systemName: logLevelIcon(latestLog.level))
.font(.system(size: 8))
@ -9746,7 +9775,7 @@ private struct TabItemView: View, Equatable {
}
// Progress bar
if sidebarShowProgress, let progress = tab.progress {
if detailVisibility.showsProgress, let progress = tab.progress {
VStack(alignment: .leading, spacing: 2) {
GeometryReader { geo in
ZStack(alignment: .leading) {
@ -9770,7 +9799,7 @@ private struct TabItemView: View, Equatable {
}
// Branch + directory row
if sidebarShowBranchDirectory {
if detailVisibility.showsBranchDirectory {
if sidebarBranchVerticalLayout {
if !branchDirectoryLines.isEmpty {
HStack(alignment: .top, spacing: 3) {
@ -9824,7 +9853,7 @@ private struct TabItemView: View, Equatable {
}
// Pull request rows
if sidebarShowPullRequest, !pullRequestRows.isEmpty {
if detailVisibility.showsPullRequests, !pullRequestRows.isEmpty {
VStack(alignment: .leading, spacing: 1) {
ForEach(pullRequestRows) { pullRequest in
Button(action: {
@ -9853,7 +9882,7 @@ private struct TabItemView: View, Equatable {
}
// Ports row
if sidebarShowPorts, !tab.listeningPorts.isEmpty {
if detailVisibility.showsPorts, !tab.listeningPorts.isEmpty {
Text(tab.listeningPorts.map { ":\($0)" }.joined(separator: ", "))
.font(.system(size: 10, design: .monospaced))
.foregroundColor(activeSecondaryColor(0.75))

View file

@ -63,6 +63,72 @@ enum SidebarBranchLayoutSettings {
}
}
enum SidebarWorkspaceDetailSettings {
static let hideAllDetailsKey = "sidebarHideAllDetails"
static let showNotificationMessageKey = "sidebarShowNotificationMessage"
static let defaultHideAllDetails = false
static let defaultShowNotificationMessage = true
static func hidesAllDetails(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: hideAllDetailsKey) == nil {
return defaultHideAllDetails
}
return defaults.bool(forKey: hideAllDetailsKey)
}
static func showsNotificationMessage(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: showNotificationMessageKey) == nil {
return defaultShowNotificationMessage
}
return defaults.bool(forKey: showNotificationMessageKey)
}
static func resolvedNotificationMessageVisibility(
showNotificationMessage: Bool,
hideAllDetails: Bool
) -> Bool {
showNotificationMessage && !hideAllDetails
}
}
struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable {
let showsMetadata: Bool
let showsLog: Bool
let showsProgress: Bool
let showsBranchDirectory: Bool
let showsPullRequests: Bool
let showsPorts: Bool
static let hidden = Self(
showsMetadata: false,
showsLog: false,
showsProgress: false,
showsBranchDirectory: false,
showsPullRequests: false,
showsPorts: false
)
static func resolved(
showMetadata: Bool,
showLog: Bool,
showProgress: Bool,
showBranchDirectory: Bool,
showPullRequests: Bool,
showPorts: Bool,
hideAllDetails: Bool
) -> Self {
guard !hideAllDetails else { return .hidden }
return Self(
showsMetadata: showMetadata,
showsLog: showLog,
showsProgress: showProgress,
showsBranchDirectory: showBranchDirectory,
showsPullRequests: showPullRequests,
showsPorts: showPorts
)
}
}
enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable {
case leftRail
case solidFill

View file

@ -3077,6 +3077,10 @@ struct SettingsView: View {
private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey)
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
@AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
@ -3648,6 +3652,19 @@ struct SettingsView: View {
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.hideAllSidebarDetails", defaultValue: "Hide All Sidebar Details"),
subtitle: sidebarHideAllDetails
? String(localized: "settings.app.hideAllSidebarDetails.subtitleOn", defaultValue: "Show only the workspace title row. Overrides the detail toggles below.")
: String(localized: "settings.app.hideAllSidebarDetails.subtitleOff", defaultValue: "Show secondary workspace details as controlled by the toggles below.")
) {
Toggle("", isOn: $sidebarHideAllDetails)
.labelsHidden()
.controlSize(.small)
}
SettingsCardDivider()
SettingsPickerRow(
String(localized: "settings.app.sidebarBranchLayout", defaultValue: "Sidebar Branch Layout"),
subtitle: sidebarBranchVerticalLayout
@ -3659,6 +3676,19 @@ struct SettingsView: View {
Text(String(localized: "settings.app.sidebarBranchLayout.vertical", defaultValue: "Vertical")).tag(true)
Text(String(localized: "settings.app.sidebarBranchLayout.inline", defaultValue: "Inline")).tag(false)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.showNotificationMessage", defaultValue: "Show Notification Message in Sidebar"),
subtitle: String(localized: "settings.app.showNotificationMessage.subtitle", defaultValue: "Display the latest notification message below the workspace title.")
) {
Toggle("", isOn: $sidebarShowNotificationMessage)
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3670,6 +3700,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3681,6 +3712,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3694,6 +3726,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3705,6 +3738,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3716,6 +3750,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3727,6 +3762,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
SettingsCardDivider()
@ -3738,6 +3774,7 @@ struct SettingsView: View {
.labelsHidden()
.controlSize(.small)
}
.disabled(sidebarHideAllDetails)
}
SettingsSectionHeader(title: String(localized: "settings.section.workspaceColors", defaultValue: "Workspace Colors"))
@ -4376,6 +4413,8 @@ struct SettingsView: View {
alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
sidebarShowBranchDirectory = true

View file

@ -4664,6 +4664,92 @@ final class SidebarBranchLayoutSettingsTests: XCTestCase {
}
}
final class SidebarWorkspaceDetailSettingsTests: XCTestCase {
func testDefaultPreferencesWhenUnset() {
let suiteName = "SidebarWorkspaceDetailSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertFalse(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults))
XCTAssertTrue(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults))
XCTAssertTrue(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults),
hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)
)
)
}
func testStoredPreferencesOverrideDefaults() {
let suiteName = "SidebarWorkspaceDetailSettingsTests.Stored.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(true, forKey: SidebarWorkspaceDetailSettings.hideAllDetailsKey)
defaults.set(false, forKey: SidebarWorkspaceDetailSettings.showNotificationMessageKey)
XCTAssertTrue(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults))
XCTAssertFalse(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults))
XCTAssertFalse(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults),
hideAllDetails: false
)
)
XCTAssertFalse(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: true,
hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)
)
)
}
}
final class SidebarWorkspaceAuxiliaryDetailVisibilityTests: XCTestCase {
func testResolvedVisibilityPreservesPerRowTogglesWhenDetailsAreShown() {
XCTAssertEqual(
SidebarWorkspaceAuxiliaryDetailVisibility.resolved(
showMetadata: true,
showLog: false,
showProgress: true,
showBranchDirectory: false,
showPullRequests: true,
showPorts: false,
hideAllDetails: false
),
SidebarWorkspaceAuxiliaryDetailVisibility(
showsMetadata: true,
showsLog: false,
showsProgress: true,
showsBranchDirectory: false,
showsPullRequests: true,
showsPorts: false
)
)
}
func testResolvedVisibilityHidesAllAuxiliaryRowsWhenDetailsAreHidden() {
XCTAssertEqual(
SidebarWorkspaceAuxiliaryDetailVisibility.resolved(
showMetadata: true,
showLog: true,
showProgress: true,
showBranchDirectory: true,
showPullRequests: true,
showPorts: true,
hideAllDetails: true
),
.hidden
)
}
}
final class SidebarActiveTabIndicatorSettingsTests: XCTestCase {
func testDefaultStyleWhenUnset() {
let suiteName = "SidebarActiveTabIndicatorSettingsTests.Default.\(UUID().uuidString)"