diff --git a/supacode/Features/Repositories/Views/AgentsToolbarButton.swift b/supacode/Features/Repositories/Views/AgentsToolbarButton.swift index 7f36462e..6e35a649 100644 --- a/supacode/Features/Repositories/Views/AgentsToolbarButton.swift +++ b/supacode/Features/Repositories/Views/AgentsToolbarButton.swift @@ -39,13 +39,13 @@ struct AgentsLauncherItem: Equatable, Identifiable { /// toolbars flatten custom menu labels to their text, dropping the badge, so /// the popover is the durable container here. /// -/// When launcher profiles exist, the capsule grows a trailing quick-launch -/// segment (mirroring the Open In split button on the far side): one click -/// starts the Recommended profile — the popover list's leader — without -/// opening the popover. Both segments share a single glass capsule; the -/// system split look can't be reused here because it requires separate -/// toolbar items in one shared-background group, and this control already -/// opts out of that group to stay clear of the branch title. +/// The button carries no background of its own: it sits in a shared-glass +/// toolbar group next to `AgentsQuickLaunchButton`, which renders the pair +/// as one system split control — the same look as the trailing Open In + +/// chevron pair. The branch title stays out of that capsule by opting out of +/// the group background itself (see the toolbar site): a fixed +/// `ToolbarSpacer` cannot split the navigation group, even with an explicit +/// `.navigation` placement. struct AgentsToolbarButton: View { let capsule: AgentsCapsuleState? let launcherItems: [AgentsLauncherItem] @@ -53,85 +53,33 @@ struct AgentsToolbarButton: View { let onLaunchProfile: (AgentProfile.ID) -> Void let onManageProfiles: () -> Void @State private var isPopoverPresented = false - @State private var isHovered = false - @State private var isQuickLaunchHovered = false - - /// The popover list's leading row (Recommended first, docs-ai 053), so the - /// segment and the popover always agree on what one click launches. - private var quickLaunchItem: AgentsLauncherItem? { launcherItems.first } var body: some View { - HStack(spacing: 0) { - Button { - isPopoverPresented.toggle() - } label: { - label - .padding(.leading, 10) - .padding(.trailing, quickLaunchItem == nil ? 10 : 8) - .padding(.vertical, 8) - .contentShape(.rect) - } - .buttonStyle(.plain) - .help(helpText) - .accessibilityLabel(accessibilityText) - .popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) { - AgentsPopoverContent( - capsule: capsule, - launcherItems: launcherItems, - onHandOff: { - isPopoverPresented = false - onHandOff() - }, - onLaunchProfile: { id in - isPopoverPresented = false - onLaunchProfile(id) - }, - onManageProfiles: { - isPopoverPresented = false - onManageProfiles() - } - ) - } - if let quickLaunchItem { - Rectangle() - .fill(.separator) - .frame(width: 1) - .padding(.vertical, 8) - Button { - onLaunchProfile(quickLaunchItem.id) - } label: { - Image(systemName: "play.circle") - .font(.title3.weight(.medium)) - .foregroundStyle(isQuickLaunchHovered ? .primary : .secondary) - .padding(.leading, 8) - .padding(.trailing, 10) - .padding(.vertical, 8) - .contentShape(.rect) + Button { + isPopoverPresented.toggle() + } label: { + label + } + .help(helpText) + .accessibilityLabel(accessibilityText) + .popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) { + AgentsPopoverContent( + capsule: capsule, + launcherItems: launcherItems, + onHandOff: { + isPopoverPresented = false + onHandOff() + }, + onLaunchProfile: { id in + isPopoverPresented = false + onLaunchProfile(id) + }, + onManageProfiles: { + isPopoverPresented = false + onManageProfiles() } - .buttonStyle(.plain) - .onHover { isQuickLaunchHovered = $0 } - .help("Launch \(quickLaunchItem.name) in this worktree") - .accessibilityLabel("Launch \(quickLaunchItem.name)") - } + ) } - // The item opts out of the navigation group's shared background - // (`sharedBackgroundVisibility(.hidden)`) to stay separate from the - // branch title, and draws its own glass capsule. `.plain` + an explicit - // glass background keeps the horizontal padding as tight as the other - // toolbar buttons; `.buttonStyle(.glass)` pads noticeably wider. - // - // Hover feedback must live in the glass material itself: a translucent - // fill layered under `glassEffect` gets swallowed by the material - // compositing, and `.interactive()` only adds press feedback on macOS. - // The material tints as one pill; the quick-launch segment signals its - // own hover by promoting its symbol from secondary to primary. - .glassEffect( - isHovered - ? .regular.tint(.primary.opacity(0.12)).interactive() - : .regular.interactive(), - in: Capsule() - ) - .onHover { isHovered = $0 } } /// Mirrors `WorktreeDetailTitleView`'s label metrics (title3 medium, @@ -177,6 +125,29 @@ struct AgentsToolbarButton: View { } } +/// Quick-launch half of the Agents split control: one click starts the +/// Recommended profile — the launcher list's leading row (docs-ai 053) — so +/// the segment and the popover always agree on what it launches. Lives as its +/// own toolbar item in the Agents shared-glass group, which is what produces +/// the system split-button look (and per-segment hover) next to +/// `AgentsToolbarButton`. +struct AgentsQuickLaunchButton: View { + let item: AgentsLauncherItem + let onLaunch: (AgentProfile.ID) -> Void + + var body: some View { + Button { + onLaunch(item.id) + } label: { + Image(systemName: "play.circle") + .font(.title3.weight(.medium)) + .foregroundStyle(.secondary) + } + .help("Launch \(item.name) in this worktree") + .accessibilityLabel("Launch \(item.name)") + } +} + /// The agent-actions popover. Hand-off leads when an agent is detected; the /// launcher rows follow under one "New agent in this worktree" section header /// (recommended profile first) so the shared purpose is stated once instead diff --git a/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift b/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift index ecc28c84..ca535228 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift @@ -13,6 +13,14 @@ struct WorktreeDetailTitleView: View { var body: some View { titleButton + // The item opts out of the navigation group's shared glass background + // (`sharedBackgroundVisibility(.hidden)` at the toolbar site) so the + // Agents pair can render as one system split control in that group, + // and draws its own capsule instead. Hover feedback must live in the + // glass material itself: translucent fills layered under `glassEffect` + // are swallowed by material compositing (docs-ai 049). + .buttonStyle(.plain) + .glassEffect(titleGlass, in: Capsule()) .onHover { hovering in isHovered = hovering } @@ -68,6 +76,15 @@ struct WorktreeDetailTitleView: View { isPresented = true } + /// Interactive glass (press feedback, hover tint) only when the title is a + /// real rename button; the folder/workspace variant stays a static pill. + private var titleGlass: Glass { + guard title.supportsRename else { return .regular } + return isHovered + ? .regular.tint(.primary.opacity(0.12)).interactive() + : .regular.interactive() + } + private var labelContent: some View { HStack(spacing: 6) { Image(systemName: (title.supportsRename && isHovered) ? "pencil" : title.systemImage) @@ -77,6 +94,9 @@ struct WorktreeDetailTitleView: View { Text(title.text) } .font(.title3.weight(.medium)) + .padding(.horizontal, 10) + .padding(.vertical, 8) + .contentShape(Capsule()) } } diff --git a/supacode/Features/Repositories/Views/WorktreeDetailView.swift b/supacode/Features/Repositories/Views/WorktreeDetailView.swift index 5dc1aea9..87c4e40b 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailView.swift @@ -914,12 +914,14 @@ struct WorktreeDetailView: View { @Environment(\.resolvedKeybindings) private var resolvedKeybindings var body: some ToolbarContent { - // `.sharedBackgroundVisibility(.hidden)` keeps the capsule out of the - // navigation group's shared glass background — adjacent items in the - // same placement would otherwise merge with the branch title into one - // capsule-shaped control (a fixed ToolbarSpacer does not split the - // navigation group). - ToolbarItem(placement: .navigation) { + // The Agents button and its quick-launch segment share the navigation + // group's glass background, which renders them as one system split + // control (like the trailing Open In + chevron pair). Nothing can split + // that group — a fixed ToolbarSpacer is ignored even with an explicit + // `.navigation` placement (re-verified 2026-08; docs-ai 049) — so the + // branch title opts out via `.sharedBackgroundVisibility(.hidden)` and + // draws its own glass capsule instead. + ToolbarItemGroup(placement: .navigation) { AgentsToolbarButton( capsule: toolbarState.agentsCapsule, launcherItems: toolbarState.agentsLauncherItems, @@ -927,8 +929,10 @@ struct WorktreeDetailView: View { onLaunchProfile: onLaunchProfile, onManageProfiles: onManageProfiles ) + if let quickLaunchItem = toolbarState.agentsLauncherItems.first { + AgentsQuickLaunchButton(item: quickLaunchItem, onLaunch: onLaunchProfile) + } } - .sharedBackgroundVisibility(.hidden) ToolbarItem(placement: .navigation) { WorktreeDetailTitleView( @@ -938,6 +942,7 @@ struct WorktreeDetailView: View { onConsumeExternalRenamePrompt: onConsumeExternalRenamePrompt ) } + .sharedBackgroundVisibility(.hidden) ToolbarItem(placement: .principal) { ToolbarStatusView(