diff --git a/CHANGELOG.md b/CHANGELOG.md index 64bd5ea6e..58634d8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - TUI: migrate key handling to the updated pi-tui Key matcher API. - macOS: prefer gateway config reads/writes in local mode (fall back to disk if the gateway is unavailable). - macOS: local gateway now connects via tailnet IP when bind mode is `tailnet`/`auto`. -- macOS: Connections removes the sidebar toggle from the Settings toolbar to avoid overflow. +- macOS: Connections settings now use a custom sidebar to avoid toolbar toggle issues, with rounded styling and full-width row hit targets. - macOS: drop deprecated `afterMs` from agent wait params to match gateway schema. - Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json. - Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding. diff --git a/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift b/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift index 3fb1bc0fd..0527394fb 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift @@ -2,13 +2,11 @@ import SwiftUI extension ConnectionsSettings { var body: some View { - NavigationSplitView { + HStack(spacing: 0) { self.sidebar - } detail: { self.detail } - .navigationSplitViewStyle(.balanced) - .toolbar(removing: .sidebarToggle) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear { self.store.start() self.ensureSelection() @@ -20,27 +18,30 @@ extension ConnectionsSettings { } private var sidebar: some View { - List(selection: self.$selectedProvider) { - if !self.enabledProviders.isEmpty { - Section("Configured") { + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + if !self.enabledProviders.isEmpty { + self.sidebarSectionHeader("Configured") ForEach(self.enabledProviders) { provider in self.sidebarRow(provider) - .tag(provider) } } - } - if !self.availableProviders.isEmpty { - Section("Available") { + if !self.availableProviders.isEmpty { + self.sidebarSectionHeader("Available") ForEach(self.availableProviders) { provider in self.sidebarRow(provider) - .tag(provider) } } } + .padding(.vertical, 10) + .padding(.horizontal, 10) } - .listStyle(.sidebar) - .frame(minWidth: 220, idealWidth: 240, maxWidth: 280) + .frame(minWidth: 220, idealWidth: 240, maxWidth: 280, maxHeight: .infinity, alignment: .topLeading) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(nsColor: .windowBackgroundColor))) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) } private var detail: some View { @@ -81,18 +82,41 @@ extension ConnectionsSettings { } private func sidebarRow(_ provider: ConnectionProvider) -> some View { - HStack(spacing: 8) { - Circle() - .fill(self.providerTint(provider)) - .frame(width: 8, height: 8) - VStack(alignment: .leading, spacing: 2) { - Text(provider.title) - Text(self.providerSummary(provider)) - .font(.caption) - .foregroundStyle(.secondary) + let isSelected = self.selectedProvider == provider + return Button { + self.selectedProvider = provider + } label: { + HStack(spacing: 8) { + Circle() + .fill(self.providerTint(provider)) + .frame(width: 8, height: 8) + VStack(alignment: .leading, spacing: 2) { + Text(provider.title) + Text(self.providerSummary(provider)) + .font(.caption) + .foregroundStyle(.secondary) + } } + .padding(.vertical, 4) + .padding(.horizontal, 6) + .frame(maxWidth: .infinity, alignment: .leading) + .background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .background(Color.clear) // ensure full-width hit test area + .contentShape(Rectangle()) } - .padding(.vertical, 4) + .frame(maxWidth: .infinity, alignment: .leading) + .buttonStyle(.plain) + .contentShape(Rectangle()) + } + + private func sidebarSectionHeader(_ title: String) -> some View { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + .textCase(.uppercase) + .padding(.horizontal, 4) + .padding(.top, 2) } private func detailHeader(for provider: ConnectionProvider) -> some View { diff --git a/apps/macos/Sources/Clawdbot/SettingsRootView.swift b/apps/macos/Sources/Clawdbot/SettingsRootView.swift index da53a3c5c..c99e88bab 100644 --- a/apps/macos/Sources/Clawdbot/SettingsRootView.swift +++ b/apps/macos/Sources/Clawdbot/SettingsRootView.swift @@ -75,7 +75,6 @@ struct SettingsRootView: View { } .padding(.horizontal, 28) .padding(.vertical, 22) - .background(SettingsToolbarCleaner()) .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onReceive(NotificationCenter.default.publisher(for: .clawdbotSelectSettingsTab)) { note in diff --git a/apps/macos/Sources/Clawdbot/SettingsToolbarCleaner.swift b/apps/macos/Sources/Clawdbot/SettingsToolbarCleaner.swift deleted file mode 100644 index a209c89c8..000000000 --- a/apps/macos/Sources/Clawdbot/SettingsToolbarCleaner.swift +++ /dev/null @@ -1,23 +0,0 @@ -import AppKit -import SwiftUI - -struct SettingsToolbarCleaner: NSViewRepresentable { - func makeNSView(context: Context) -> NSView { - NSView() - } - - func updateNSView(_ nsView: NSView, context: Context) { - DispatchQueue.main.async { - guard let toolbar = nsView.window?.toolbar else { return } - let items = toolbar.items - for (index, item) in items.enumerated().reversed() { - let isSidebarToggle = - item.itemIdentifier == .toggleSidebar - || item.itemIdentifier.rawValue == "com.apple.NSToolbarShowSidebarItem" - if isSidebarToggle { - toolbar.removeItem(at: index) - } - } - } - } -}