diff --git a/docs-ai/061-native-toolbar-controls/toolbar-controls.md b/docs-ai/061-native-toolbar-controls/toolbar-controls.md index 2244a46c8..8d66d99ff 100644 --- a/docs-ai/061-native-toolbar-controls/toolbar-controls.md +++ b/docs-ai/061-native-toolbar-controls/toolbar-controls.md @@ -32,7 +32,7 @@ exposes `ToolbarContent.sharedBackgroundVisibility(_:)` only on macOS 26+. The a | Main leading cluster (Normal, Shelf, Canvas) | `supacode/Features/Repositories/Views/WorktreeDetailView.swift` | Agents + Quick Launch use `ToolbarItemGroup(placement: .navigation)`. Bell + update use one isolated `ToolbarItem(placement: .navigation)`. | Native Agents group; verified composite exception for bell/update. | | Main principal status | `supacode/Features/Repositories/Views/WorktreeDetailView.swift`, `ToolbarStatusView.swift` | One `.principal` status projection. | Correct: display-only content is not an action group. | | Normal/Shelf trailing actions | `supacode/Features/Repositories/Views/WorktreeDetailView.swift`, `WorktreeDetailToolbarViews.swift` | Open action group, fixed spacers, Run item, inline custom-command group, overflow item. | Correct: semantic actions retain native toolbar ownership. | -| Canvas trailing actions | `supacode/Features/Repositories/Views/WorktreeDetailView.swift` | One `.primaryAction` item with a dynamic Run/custom-command `HStack`. | Approved stability exception: changing the number of toolbar items causes NSToolbar insert/remove animation and visible overflow. | +| Canvas trailing actions | `supacode/Features/Repositories/Views/WorktreeDetailView.swift` | One `.primaryAction` item with a dynamic Run/custom-command `HStack`. The `HStack` spacing and horizontal padding (`CanvasCommandCluster`) add back the per-item inset that NSToolbar gives each Normal/Shelf item, so the capsule matches pixel for pixel (verified on macOS 27). | Approved stability exception: changing the number of toolbar items causes NSToolbar insert/remove animation and visible overflow. | | Remote Mirror | `supacode/Features/RemoteMirror/RemoteMirrorPaneView.swift` | Navigation Host button with native title/subtitle, principal connection status matching local status styling, primary action group for recovery, display size, history and disconnect. | Native window toolbar; display size is a stateful toggle and disconnect uses a red icon. | | Diff window | `supacode/Features/DiffView/DiffWindowContentView.swift` | Navigation sidebar toggle; principal and primary segmented pickers. | Correct native items; no custom glass. | | Sidebar | `supacode/Features/Repositories/Views/SidebarListView.swift` | Automatic Add Repository/Workspace item. | Correct single action. | diff --git a/supacode/Features/Repositories/Views/WorktreeDetailView.swift b/supacode/Features/Repositories/Views/WorktreeDetailView.swift index 6872eaba3..741ab2405 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailView.swift @@ -312,7 +312,6 @@ struct WorktreeDetailView: View { let showRunButton = state.showRunButtonInToolbar && (state.runScriptIsRunning || state.runScriptEnabled) - let overflowCommands = Array(state.customCommands.enumerated().dropFirst(3)) // A fixed separator keeps the dynamic Run + Custom Command cluster distinct // from other trailing actions, mirroring the Normal toolbar spacing. // @@ -336,59 +335,67 @@ struct WorktreeDetailView: View { if showRunButton || !state.customCommands.isEmpty { ToolbarSpacer(.fixed) ToolbarItem(placement: .primaryAction) { - // `spacing: 0` keeps the cluster as tight as the Normal toolbar's - // ToolbarItemGroup (whose buttons sit nearly flush on macOS 26); the - // buttons' own internal padding provides the visible gap. - HStack(spacing: 0) { - if showRunButton { - RunScriptToolbarButton( - isRunning: state.runScriptIsRunning, - isEnabled: state.runScriptEnabled, - runHelpText: AppShortcuts.helpText( - title: "Run Script", - commandID: AppShortcuts.CommandID.runScript, - in: store.resolvedKeybindings - ), - stopHelpText: AppShortcuts.helpText( - title: "Stop Script", - commandID: AppShortcuts.CommandID.stopScript, - in: store.resolvedKeybindings - ), - runShortcut: store.resolvedKeybindings.display(for: AppShortcuts.CommandID.runScript), - stopShortcut: store.resolvedKeybindings.display( - for: AppShortcuts.CommandID.stopScript), - runAction: { store.send(.runScript) }, - stopAction: { store.send(.stopRunScript) } - ) - } - ForEach(Array(state.customCommands.enumerated().prefix(3)), id: \.element.id) { _, command in - UserCustomCommandToolbarButton( - title: command.command.resolvedTitle, - systemImage: command.command.resolvedSystemImage, - source: command.source, - shortcut: store.resolvedKeybindings.display( - for: command.keybindingID - ), - isEnabled: command.command.hasRunnableCommand, - action: { - store.send(.runCustomCommand(command.id)) - } - ) + canvasCommandCluster(state: state, showRunButton: showRunButton) + } + } + } + + @ViewBuilder + private func canvasCommandCluster(state: ToolbarSharedState, showRunButton: Bool) -> some View { + let overflowCommands = Array(state.customCommands.enumerated().dropFirst(3)) + // NSToolbar gives each item of the Normal toolbar its own horizontal + // inset. This single item gets that inset only once, so the spacing and + // the outer padding add it back between and around the buttons to + // match the Normal toolbar. + HStack(spacing: CanvasCommandCluster.itemSpacing) { + if showRunButton { + RunScriptToolbarButton( + isRunning: state.runScriptIsRunning, + isEnabled: state.runScriptEnabled, + runHelpText: AppShortcuts.helpText( + title: "Run Script", + commandID: AppShortcuts.CommandID.runScript, + in: store.resolvedKeybindings + ), + stopHelpText: AppShortcuts.helpText( + title: "Stop Script", + commandID: AppShortcuts.CommandID.stopScript, + in: store.resolvedKeybindings + ), + runShortcut: store.resolvedKeybindings.display(for: AppShortcuts.CommandID.runScript), + stopShortcut: store.resolvedKeybindings.display( + for: AppShortcuts.CommandID.stopScript), + runAction: { store.send(.runScript) }, + stopAction: { store.send(.stopRunScript) } + ) + } + ForEach(Array(state.customCommands.enumerated().prefix(3)), id: \.element.id) { _, command in + UserCustomCommandToolbarButton( + title: command.command.resolvedTitle, + systemImage: command.command.resolvedSystemImage, + source: command.source, + shortcut: store.resolvedKeybindings.display( + for: command.keybindingID + ), + isEnabled: command.command.hasRunnableCommand, + action: { + store.send(.runCustomCommand(command.id)) } - if !overflowCommands.isEmpty { - CustomCommandOverflowButton( - entries: overflowCommands.map { $0.element }, - shortcutDisplay: { command in - store.resolvedKeybindings.display(for: command.keybindingID) - }, - onRunCustomCommand: { id in - store.send(.runCustomCommand(id)) - } - ) + ) + } + if !overflowCommands.isEmpty { + CustomCommandOverflowButton( + entries: overflowCommands.map { $0.element }, + shortcutDisplay: { command in + store.resolvedKeybindings.display(for: command.keybindingID) + }, + onRunCustomCommand: { id in + store.send(.runCustomCommand(id)) } - } + ) } } + .padding(.horizontal, CanvasCommandCluster.edgeInset) } /// The target pane's detected agent, feeding the Agents capsule. nil @@ -1298,3 +1305,10 @@ struct WorktreeDetailView: View { return nil } } + +/// Spacing that makes the single-item Canvas command cluster match the +/// per-item insets of the Normal toolbar. +private enum CanvasCommandCluster { + static let itemSpacing: CGFloat = 3.5 + static let edgeInset: CGFloat = 3 +}