Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-ai/061-native-toolbar-controls/toolbar-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
114 changes: 64 additions & 50 deletions supacode/Features/Repositories/Views/WorktreeDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Loading