Skip to content

Commit 2b9fe41

Browse files
jsonifyclaude
andcommitted
feat: Implement pause/resume UI controls for automation
- Add pause/resume state management to ClickItViewModel - Implement pauseAutomation() and resumeAutomation() methods - Add isPaused property and canPause/canResume computed properties - Update AppStatus enum with .paused case (orange color) - Redesign StatusHeaderCard with separate pause/resume and stop buttons - Integrate with ElapsedTimeManager pause/resume functionality - Update visual feedback overlay to show paused state (dimmed) - Add comprehensive test suite for pause/resume functionality - Handle external automation stops to clear pause state - Add testing helper method startAutomationForTesting() 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 3fd5c08 commit 2b9fe41

File tree

4 files changed

+466
-27
lines changed

4 files changed

+466
-27
lines changed

.agent-os/specs/2025-07-22-phase1-completion/tasks.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ These are the tasks to be completed for the spec detailed in @.agent-os/specs/20
77
88
## Tasks
99

10-
- [ ] 1. **Implement Pause/Resume UI Controls**
11-
- [ ] 1.1 Write tests for pause/resume button components and state management
12-
- [ ] 1.2 Add pause/resume buttons to main automation control panel in ClickItViewModel
13-
- [ ] 1.3 Integrate pause/resume functionality with ElapsedTimeManager and ClickCoordinator
14-
- [ ] 1.4 Update visual feedback overlay to reflect pause/resume states
15-
- [ ] 1.5 Ensure session statistics preservation during pause state
16-
- [ ] 1.6 Verify all tests pass and UI responds correctly
10+
- [x] 1. **Implement Pause/Resume UI Controls**
11+
- [x] 1.1 Write tests for pause/resume button components and state management
12+
- [x] 1.2 Add pause/resume buttons to main automation control panel in ClickItViewModel
13+
- [x] 1.3 Integrate pause/resume functionality with ElapsedTimeManager and ClickCoordinator
14+
- [x] 1.4 Update visual feedback overlay to reflect pause/resume states
15+
- [x] 1.5 Ensure session statistics preservation during pause state
16+
- [x] 1.6 Verify all tests pass and UI responds correctly
1717

1818
- [ ] 2. **Build Enhanced Preset Management System**
1919
- [ ] 2.1 Write tests for PresetManager and PresetConfiguration data structures

Sources/ClickIt/UI/Components/StatusHeaderCard.swift

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,71 @@ struct StatusHeaderCard: View {
6666
)
6767
}
6868

69-
// Primary Action Button
70-
Button(action: {
71-
if viewModel.isRunning {
72-
viewModel.stopAutomation()
73-
} else {
74-
viewModel.startAutomation()
75-
}
76-
}) {
77-
HStack(spacing: 8) {
78-
Image(systemName: viewModel.isRunning ? "stop.fill" : "play.fill")
79-
.font(.system(size: 16, weight: .medium))
69+
// Control Buttons
70+
if viewModel.isRunning || viewModel.isPaused {
71+
// Running/Paused state: Show Pause/Resume and Stop buttons
72+
HStack(spacing: 12) {
73+
// Pause/Resume Button
74+
Button(action: {
75+
if viewModel.canPause {
76+
viewModel.pauseAutomation()
77+
} else if viewModel.canResume {
78+
viewModel.resumeAutomation()
79+
}
80+
}) {
81+
HStack(spacing: 6) {
82+
Image(systemName: viewModel.isPaused ? "play.fill" : "pause.fill")
83+
.font(.system(size: 14, weight: .medium))
84+
85+
Text(viewModel.isPaused ? "Resume" : "Pause")
86+
.font(.subheadline)
87+
.fontWeight(.medium)
88+
}
89+
.frame(maxWidth: .infinity)
90+
.frame(height: 36)
91+
}
92+
.buttonStyle(.bordered)
93+
.disabled(!viewModel.canPause && !viewModel.canResume)
94+
.tint(viewModel.isPaused ? .green : .orange)
8095

81-
Text(viewModel.isRunning ? "Stop Automation" : "Start Automation")
82-
.font(.headline)
83-
.fontWeight(.medium)
96+
// Stop Button
97+
Button(action: {
98+
viewModel.stopAutomation()
99+
}) {
100+
HStack(spacing: 6) {
101+
Image(systemName: "stop.fill")
102+
.font(.system(size: 14, weight: .medium))
103+
104+
Text("Stop")
105+
.font(.subheadline)
106+
.fontWeight(.medium)
107+
}
108+
.frame(maxWidth: .infinity)
109+
.frame(height: 36)
110+
}
111+
.buttonStyle(.borderedProminent)
112+
.tint(.red)
113+
}
114+
} else {
115+
// Ready state: Show Start button
116+
Button(action: {
117+
viewModel.startAutomation()
118+
}) {
119+
HStack(spacing: 8) {
120+
Image(systemName: "play.fill")
121+
.font(.system(size: 16, weight: .medium))
122+
123+
Text("Start Automation")
124+
.font(.headline)
125+
.fontWeight(.medium)
126+
}
127+
.frame(maxWidth: .infinity)
128+
.frame(height: 44)
84129
}
85-
.frame(maxWidth: .infinity)
86-
.frame(height: 44)
130+
.buttonStyle(.borderedProminent)
131+
.disabled(!viewModel.canStartAutomation)
132+
.tint(.green)
87133
}
88-
.buttonStyle(.borderedProminent)
89-
.disabled(!viewModel.canStartAutomation && !viewModel.isRunning)
90-
.tint(viewModel.isRunning ? .red : .green)
91134
}
92135
.padding(16)
93136
.background(Color(NSColor.controlBackgroundColor))

Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class ClickItViewModel: ObservableObject {
1515
// MARK: - Published Properties
1616
@Published var targetPoint: CGPoint?
1717
@Published var isRunning = false
18+
@Published var isPaused = false
1819
@Published var appStatus: AppStatus = .ready
1920

2021
// Configuration Properties
@@ -69,6 +70,14 @@ class ClickItViewModel: ObservableObject {
6970
return total >= 1 && total <= 3600 // 1 second to 60 minutes
7071
}
7172

73+
var canPause: Bool {
74+
isRunning && !isPaused
75+
}
76+
77+
var canResume: Bool {
78+
isPaused && !isRunning
79+
}
80+
7281
// MARK: - Dependencies
7382
private let clickCoordinator = ClickCoordinator.shared
7483

@@ -151,9 +160,83 @@ class ClickItViewModel: ObservableObject {
151160
clickCoordinator.stopAutomation()
152161
cancelTimer() // Also cancel any active timer
153162
isRunning = false
163+
isPaused = false
154164
appStatus = .ready
155165
}
156166

167+
func pauseAutomation() {
168+
guard isRunning && !isPaused else { return }
169+
170+
clickCoordinator.stopAutomation()
171+
ElapsedTimeManager.shared.pauseTracking()
172+
173+
// Update visual feedback to show paused state (dimmed)
174+
if showVisualFeedback, let point = targetPoint {
175+
VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: false)
176+
}
177+
178+
isRunning = false
179+
isPaused = true
180+
appStatus = .paused
181+
}
182+
183+
func resumeAutomation() {
184+
guard isPaused && !isRunning else { return }
185+
186+
// Resume elapsed time tracking
187+
ElapsedTimeManager.shared.resumeTracking()
188+
189+
// Restart automation with current configuration
190+
guard let point = targetPoint else { return }
191+
192+
let config = AutomationConfiguration(
193+
location: point,
194+
clickType: clickType,
195+
clickInterval: Double(totalMilliseconds) / 1000.0,
196+
targetApplication: nil,
197+
maxClicks: durationMode == .clickCount ? maxClicks : nil,
198+
maxDuration: durationMode == .timeLimit ? durationSeconds : nil,
199+
stopOnError: stopOnError,
200+
randomizeLocation: randomizeLocation,
201+
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
202+
showVisualFeedback: showVisualFeedback,
203+
useDynamicMouseTracking: false
204+
)
205+
206+
clickCoordinator.startAutomation(with: config)
207+
isRunning = true
208+
isPaused = false
209+
appStatus = .running
210+
211+
// Update visual feedback to show active state
212+
if showVisualFeedback {
213+
VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: true)
214+
}
215+
}
216+
217+
// MARK: - Testing Methods
218+
func startAutomationForTesting() {
219+
guard let point = targetPoint else { return }
220+
221+
let config = AutomationConfiguration(
222+
location: point,
223+
clickType: clickType,
224+
clickInterval: Double(totalMilliseconds) / 1000.0,
225+
targetApplication: nil,
226+
maxClicks: durationMode == .clickCount ? maxClicks : nil,
227+
maxDuration: durationMode == .timeLimit ? durationSeconds : nil,
228+
stopOnError: stopOnError,
229+
randomizeLocation: randomizeLocation,
230+
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
231+
showVisualFeedback: false, // Disable visual feedback for tests
232+
useDynamicMouseTracking: false
233+
)
234+
235+
clickCoordinator.startAutomation(with: config)
236+
isRunning = true
237+
appStatus = .running
238+
}
239+
157240
func resetConfiguration() {
158241
intervalHours = 0
159242
intervalMinutes = 0
@@ -189,9 +272,10 @@ class ClickItViewModel: ObservableObject {
189272
guard let self = self else { return }
190273

191274
// Sync ViewModel state with ClickCoordinator state
192-
if !isActive && self.isRunning {
275+
if !isActive && (self.isRunning || self.isPaused) {
193276
print("ClickItViewModel: Automation stopped externally (e.g., DELETE key), updating UI state")
194277
self.isRunning = false
278+
self.isPaused = false
195279
self.appStatus = .ready
196280
// Also cancel any active timer when automation stops
197281
self.cancelTimer()
@@ -349,6 +433,7 @@ enum TimerMode {
349433
enum AppStatus {
350434
case ready
351435
case running
436+
case paused
352437
case error(String)
353438

354439
var displayText: String {
@@ -357,6 +442,8 @@ enum AppStatus {
357442
return "Ready"
358443
case .running:
359444
return "Running"
445+
case .paused:
446+
return "Paused"
360447
case .error(let message):
361448
return "Error: \(message)"
362449
}
@@ -368,6 +455,8 @@ enum AppStatus {
368455
return .green
369456
case .running:
370457
return .blue
458+
case .paused:
459+
return .orange
371460
case .error:
372461
return .red
373462
}

0 commit comments

Comments
 (0)