Skip to content

Commit a4c879f

Browse files
committed
Add Start/Stop Task Toolbar Group
1 parent 9d74f8a commit a4c879f

File tree

6 files changed

+216
-38
lines changed

6 files changed

+216
-38
lines changed

CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ final class CodeEditSplitViewController: NSSplitViewController {
9797

9898
private func makeNavigator(view: some View) -> NSSplitViewItem {
9999
let navigator = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: view))
100-
navigator.titlebarSeparatorStyle = .none
100+
if #unavailable(macOS 26) {
101+
navigator.titlebarSeparatorStyle = .none
102+
}
101103
navigator.isSpringLoaded = true
102104
navigator.minimumThickness = Self.minSidebarWidth
103105
navigator.collapseBehavior = .useConstraints

CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,34 @@ extension CodeEditWindowController {
1515
toolbar.delegate = self
1616
toolbar.displayMode = .labelOnly
1717
toolbar.showsBaselineSeparator = false
18-
self.window?.titleVisibility = toolbarCollapsed ? .visible : .hidden
19-
self.window?.toolbarStyle = .unifiedCompact
18+
self.window?.titleVisibility = toolbarCollapsed ? .visible : .hidden
19+
if #available(macOS 26, *) {
20+
self.window?.toolbarStyle = .unified
21+
} else {
22+
self.window?.toolbarStyle = .unifiedCompact
23+
}
2024
self.window?.titlebarSeparatorStyle = .automatic
2125
self.window?.toolbar = toolbar
2226
}
2327

2428
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
25-
[
29+
var items: [NSToolbarItem.Identifier] = [
2630
.toggleFirstSidebarItem,
2731
.flexibleSpace,
28-
.stopTaskSidebarItem,
29-
.startTaskSidebarItem,
32+
]
33+
34+
if #available(macOS 26, *) {
35+
items += [
36+
.taskSidebarItem
37+
]
38+
} else {
39+
items += [
40+
.stopTaskSidebarItem,
41+
.startTaskSidebarItem,
42+
]
43+
}
44+
45+
items += [
3046
.sidebarTrackingSeparator,
3147
.branchPicker,
3248
.flexibleSpace,
@@ -37,10 +53,12 @@ extension CodeEditWindowController {
3753
.flexibleSpace,
3854
.toggleLastSidebarItem
3955
]
56+
57+
return items
4058
}
4159

4260
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
43-
[
61+
var items: [NSToolbarItem.Identifier] = [
4462
.toggleFirstSidebarItem,
4563
.sidebarTrackingSeparator,
4664
.flexibleSpace,
@@ -49,9 +67,20 @@ extension CodeEditWindowController {
4967
.branchPicker,
5068
.activityViewer,
5169
.notificationItem,
52-
.startTaskSidebarItem,
53-
.stopTaskSidebarItem
5470
]
71+
72+
if #available(macOS 26, *) {
73+
items += [
74+
.taskSidebarItem
75+
]
76+
} else {
77+
items += [
78+
.startTaskSidebarItem,
79+
.stopTaskSidebarItem
80+
]
81+
}
82+
83+
return items
5584
}
5685

5786
func toggleToolbar() {
@@ -88,7 +117,6 @@ extension CodeEditWindowController {
88117
)
89118
case .toggleFirstSidebarItem:
90119
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.toggleFirstSidebarItem)
91-
toolbarItem.label = "Navigator Sidebar"
92120
toolbarItem.paletteLabel = " Navigator Sidebar"
93121
toolbarItem.toolTip = "Hide or show the Navigator"
94122
toolbarItem.isBordered = true
@@ -102,7 +130,6 @@ extension CodeEditWindowController {
102130
return toolbarItem
103131
case .toggleLastSidebarItem:
104132
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.toggleLastSidebarItem)
105-
toolbarItem.label = "Inspector Sidebar"
106133
toolbarItem.paletteLabel = "Inspector Sidebar"
107134
toolbarItem.toolTip = "Hide or show the Inspectors"
108135
toolbarItem.isBordered = true
@@ -115,30 +142,9 @@ extension CodeEditWindowController {
115142

116143
return toolbarItem
117144
case .stopTaskSidebarItem:
118-
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.stopTaskSidebarItem)
119-
120-
guard let taskManager = workspace?.taskManager
121-
else { return nil }
122-
123-
let view = NSHostingView(
124-
rootView: StopTaskToolbarButton(taskManager: taskManager)
125-
)
126-
toolbarItem.view = view
127-
128-
return toolbarItem
145+
return stopTaskSidebarItem()
129146
case .startTaskSidebarItem:
130-
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.startTaskSidebarItem)
131-
132-
guard let taskManager = workspace?.taskManager else { return nil }
133-
guard let workspace = workspace else { return nil }
134-
135-
let view = NSHostingView(
136-
rootView: StartTaskToolbarButton(taskManager: taskManager)
137-
.environmentObject(workspace)
138-
)
139-
toolbarItem.view = view
140-
141-
return toolbarItem
147+
return startTaskSidebarItem()
142148
case .branchPicker:
143149
let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker)
144150
let view = NSHostingView(
@@ -147,7 +153,7 @@ extension CodeEditWindowController {
147153
)
148154
)
149155
toolbarItem.view = view
150-
156+
toolbarItem.isBordered = false
151157
return toolbarItem
152158
case .activityViewer:
153159
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.activityViewer)
@@ -187,8 +193,53 @@ extension CodeEditWindowController {
187193
)
188194
toolbarItem.view = view
189195
return toolbarItem
196+
case .taskSidebarItem:
197+
guard #available(macOS 26, *) else {
198+
fatalError("Unified task sidebar item used on pre-tahoe platform.")
199+
}
200+
guard let workspace,
201+
let stop = StopTaskToolbarItem(workspace: workspace) else {
202+
return nil
203+
}
204+
let start = StartTaskToolbarItem(workspace: workspace)
205+
206+
let group = NSToolbarItemGroup(itemIdentifier: .taskSidebarItem)
207+
group.isBordered = true
208+
group.controlRepresentation = .expanded
209+
group.selectionMode = .momentary
210+
group.subitems = [stop, start]
211+
212+
return group
190213
default:
191214
return NSToolbarItem(itemIdentifier: itemIdentifier)
192215
}
193216
}
217+
218+
private func stopTaskSidebarItem() -> NSToolbarItem? {
219+
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.stopTaskSidebarItem)
220+
221+
guard let taskManager = workspace?.taskManager else { return nil }
222+
223+
let view = NSHostingView(
224+
rootView: StopTaskToolbarButton(taskManager: taskManager)
225+
)
226+
toolbarItem.view = view
227+
228+
return toolbarItem
229+
}
230+
231+
private func startTaskSidebarItem() -> NSToolbarItem? {
232+
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.startTaskSidebarItem)
233+
234+
guard let taskManager = workspace?.taskManager else { return nil }
235+
guard let workspace = workspace else { return nil }
236+
237+
let view = NSHostingView(
238+
rootView: StartTaskToolbarButton(taskManager: taskManager)
239+
.environmentObject(workspace)
240+
)
241+
toolbarItem.view = view
242+
243+
return toolbarItem
244+
}
194245
}

CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,6 @@ extension NSToolbarItem.Identifier {
121121
static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker")
122122
static let activityViewer: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ActivityViewer")
123123
static let notificationItem = NSToolbarItem.Identifier("notificationItem")
124+
125+
static let taskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("TaskSidebarItem")
124126
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// StartTaskToolbarItem.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 8/28/25.
6+
//
7+
8+
import AppKit
9+
10+
@available(macOS 26, *)
11+
final class StartTaskToolbarItem: NSToolbarItem {
12+
private weak var workspace: WorkspaceDocument?
13+
14+
private var utilityAreaCollapsed: Bool {
15+
workspace?.utilityAreaModel?.isCollapsed ?? true
16+
}
17+
18+
init(workspace: WorkspaceDocument) {
19+
self.workspace = workspace
20+
super.init(itemIdentifier: NSToolbarItem.Identifier("StartTaskToolbarItem"))
21+
22+
image = NSImage(systemSymbolName: "play.fill", accessibilityDescription: nil)
23+
let config = NSImage.SymbolConfiguration(pointSize: 14, weight: .regular)
24+
image = image?.withSymbolConfiguration(config) ?? image
25+
26+
paletteLabel = "Start Task"
27+
toolTip = "Run the selected task"
28+
target = self
29+
action = #selector(startTask)
30+
isBordered = true
31+
}
32+
33+
@objc
34+
func startTask() {
35+
guard let taskManager = workspace?.taskManager else { return }
36+
37+
taskManager.executeActiveTask()
38+
if utilityAreaCollapsed {
39+
CommandManager.shared.executeCommand("open.drawer")
40+
}
41+
workspace?.utilityAreaModel?.selectedTab = .debugConsole
42+
taskManager.taskShowingOutput = taskManager.selectedTaskID
43+
}
44+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// StopTaskToolbarItem.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 8/28/25.
6+
//
7+
8+
import AppKit
9+
import Combine
10+
11+
@available(macOS 26, *)
12+
final class StopTaskToolbarItem: NSToolbarItem {
13+
private weak var workspace: WorkspaceDocument?
14+
15+
private var taskManager: TaskManager? {
16+
workspace?.taskManager
17+
}
18+
19+
/// The listener that listens to the active task's status publisher. Is updated frequently as the active task
20+
/// changes.
21+
private var statusListener: AnyCancellable?
22+
private var otherListeners: Set<AnyCancellable> = []
23+
24+
init?(workspace: WorkspaceDocument) {
25+
guard let taskManager = workspace.taskManager else { return nil }
26+
27+
self.workspace = workspace
28+
super.init(itemIdentifier: NSToolbarItem.Identifier("StopTaskToolbarItem"))
29+
30+
image = NSImage(systemSymbolName: "stop.fill", accessibilityDescription: nil)
31+
let config = NSImage.SymbolConfiguration(pointSize: 14, weight: .regular)
32+
image = image?.withSymbolConfiguration(config) ?? image
33+
34+
paletteLabel = "Stop Task"
35+
toolTip = "Stop the selected task"
36+
target = self
37+
isEnabled = false
38+
isBordered = true
39+
40+
taskManager.$selectedTaskID.sink { [weak self] selectedId in
41+
self?.updateStatusListener(activeTasks: taskManager.activeTasks, selectedId: selectedId)
42+
}
43+
.store(in: &otherListeners)
44+
45+
taskManager.$activeTasks.sink { [weak self] activeTasks in
46+
self?.updateStatusListener(activeTasks: activeTasks, selectedId: taskManager.selectedTaskID)
47+
}
48+
.store(in: &otherListeners)
49+
50+
updateStatusListener(activeTasks: taskManager.activeTasks, selectedId: taskManager.selectedTaskID)
51+
}
52+
53+
/// Update the ``statusListener`` to listen to a potentially new active task.
54+
private func updateStatusListener(activeTasks: [UUID: CEActiveTask], selectedId: UUID?) {
55+
statusListener?.cancel()
56+
57+
if let status = activeTasks[selectedId ?? UUID()]?.status {
58+
updateForNewStatus(status)
59+
}
60+
61+
guard let id = selectedId else { return }
62+
statusListener = activeTasks[id]?.$status.sink { [weak self] status in
63+
self?.updateForNewStatus(status)
64+
}
65+
}
66+
67+
private func updateForNewStatus(_ status: CETaskStatus) {
68+
isEnabled = status == .running
69+
action = isEnabled ? #selector(stopTask) : nil
70+
}
71+
72+
@objc
73+
func stopTask() {
74+
taskManager?.terminateActiveTask()
75+
}
76+
77+
deinit {
78+
statusListener?.cancel()
79+
otherListeners.removeAll()
80+
}
81+
}

CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ struct StartTaskToolbarButton: View {
1111
@Environment(\.controlActiveState)
1212
private var activeState
1313

14-
@UpdatingWindowController var windowController: CodeEditWindowController?
15-
1614
@ObservedObject var taskManager: TaskManager
1715
@EnvironmentObject var workspace: WorkspaceDocument
1816

1917
var utilityAreaCollapsed: Bool {
20-
windowController?.workspace?.utilityAreaModel?.isCollapsed ?? true
18+
workspace.utilityAreaModel?.isCollapsed ?? true
2119
}
2220

2321
var body: some View {

0 commit comments

Comments
 (0)