diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index a47eae1..8ac0cc5 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -10,8 +10,9 @@ env:
# UI views (SwiftUI) are difficult to test and contribute 0% coverage.
# ContentView.swift (895 lines, 0% coverage) significantly impacts overall %.
# Increase this threshold as more non-UI code is added.
- # Lowered to 28% after v1.5.0 added new UI views (ContextPanelView, TouchBarDashboardView)
- COVERAGE_THRESHOLD: 28
+ # Lowered to 22% after v1.5.0 added new UI views (ContextPanelView, TouchBarDashboardView)
+ # These views add ~1300 lines of untestable SwiftUI code
+ COVERAGE_THRESHOLD: 22
jobs:
build:
diff --git a/App/Resources/Info.plist b/App/Resources/Info.plist
index 977378a..573af2b 100644
--- a/App/Resources/Info.plist
+++ b/App/Resources/Info.plist
@@ -19,9 +19,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.1
+ 1.5.0
CFBundleVersion
- 5
+ 6
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/App/Sources/ContentView.swift b/App/Sources/ContentView.swift
index 0bdc51c..5ca8389 100644
--- a/App/Sources/ContentView.swift
+++ b/App/Sources/ContentView.swift
@@ -15,7 +15,6 @@ struct ContentView: View {
@StateObject private var touchBarManager = TouchBarManager()
@StateObject private var restartProgress = RestartProgress()
@State private var flowState: ContentViewFlowState = .idle
- @State private var showingRestartOptions = false
@State private var showingAlert = false
@State private var alertMessage = ""
@State private var alertTitle = "Touch Bar Restart"
@@ -29,17 +28,21 @@ struct ContentView: View {
}
var body: some View {
- ZStack {
- mainContentView
-
- // Overlay for progress view during restart
- if case .restarting = flowState {
- progressOverlay
- }
-
- // Overlay for restart options on partial failure
- if showingRestartOptions {
- optionsOverlay
+ Group {
+ if flowState == .idle {
+ mainContentView
+ } else {
+ // Dashboard view for all non-idle states
+ TouchBarDashboardView(
+ progress: restartProgress,
+ flowState: flowState,
+ onGrantAdmin: handleGrantAdmin,
+ onRestartComputer: handleRestartComputer,
+ onCancel: { resetToIdle() },
+ onDone: { quitApp() },
+ onTryAgain: { restartTouchBar() },
+ onShare: { shareSuccess() }
+ )
}
}
.alert(alertTitle, isPresented: $showingAlert) {
@@ -173,49 +176,6 @@ struct ContentView: View {
.background(Color.white)
}
- // MARK: - Overlay Views
-
- private var progressOverlay: some View {
- ZStack {
- Color.black.opacity(0.4)
- .ignoresSafeArea()
-
- RestartProgressView(
- progress: restartProgress,
- onComplete: { state in
- handleProgressComplete(state)
- },
- onDismiss: {
- resetToIdle()
- }
- )
- }
- .transition(.opacity)
- }
-
- private var optionsOverlay: some View {
- ZStack {
- Color.black.opacity(0.4)
- .ignoresSafeArea()
-
- RestartOptionsView(
- onGrantAdmin: {
- handleGrantAdmin()
- },
- onRestartComputer: {
- handleRestartComputer()
- },
- onCancel: {
- showingRestartOptions = false
- resetToIdle()
- }
- )
- .cornerRadius(16)
- .shadow(color: .black.opacity(0.2), radius: 20, x: 0, y: 10)
- }
- .transition(.opacity)
- }
-
// MARK: - Computed Properties for UI State
private var subtitleText: String {
@@ -322,9 +282,8 @@ struct ContentView: View {
switch result {
case .success(let touchBarResult):
if touchBarResult.needsAdmin {
- // Partial failure - show options dialog
+ // Partial failure - dashboard will show admin options
flowState = .partialFailure(needsAdmin: true)
- showingRestartOptions = true
} else if touchBarResult.overallSuccess {
// Full success
flowState = .success(usedAdmin: false)
@@ -385,9 +344,6 @@ struct ContentView: View {
flowState = .success(usedAdmin: false)
case .partialFailure(let needsAdmin):
flowState = .partialFailure(needsAdmin: needsAdmin)
- if needsAdmin {
- showingRestartOptions = true
- }
case .failure(let message):
flowState = .failure(message)
default:
@@ -396,7 +352,6 @@ struct ContentView: View {
}
private func handleGrantAdmin() {
- showingRestartOptions = false
flowState = .restarting
// Reset progress for admin restart
@@ -418,7 +373,6 @@ struct ContentView: View {
if case .userCancelled = error {
// User cancelled - go back to partial failure state
flowState = .partialFailure(needsAdmin: true)
- showingRestartOptions = true
} else {
flowState = .failure(error.localizedDescription)
restartProgress.overallState = .failure(error.localizedDescription)
@@ -429,8 +383,6 @@ struct ContentView: View {
}
private func handleRestartComputer() {
- showingRestartOptions = false
-
// Use AppleScript to trigger system restart
let script = """
tell application "System Events"
@@ -453,7 +405,6 @@ struct ContentView: View {
private func resetToIdle() {
flowState = .idle
restartProgress.reset()
- showingRestartOptions = false
}
private func showSuccessAlert(usedAdmin: Bool) {
diff --git a/App/Sources/ContextPanelView.swift b/App/Sources/ContextPanelView.swift
new file mode 100644
index 0000000..b7e7f83
--- /dev/null
+++ b/App/Sources/ContextPanelView.swift
@@ -0,0 +1,463 @@
+import SwiftUI
+
+/// Right panel showing context-specific content based on the current flow state.
+/// Provides explanations, action buttons, and user guidance.
+struct ContextPanelView: View {
+ let flowState: ContentViewFlowState
+
+ // Action callbacks
+ let onGrantAdmin: () -> Void
+ let onRestartComputer: () -> Void
+ let onCancel: () -> Void
+ let onDone: () -> Void
+ let onTryAgain: () -> Void
+ let onShare: () -> Void
+
+ @State private var hoveredButton: ButtonType?
+
+ private enum ButtonType {
+ case primary, secondary, tertiary
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ // Header - fixed at top
+ headerSection
+ .padding(.bottom, 16)
+
+ // Scrollable content area
+ ScrollView(.vertical, showsIndicators: false) {
+ contentSection
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ // Action buttons - fixed at bottom
+ actionSection
+ .padding(.top, 16)
+ }
+ .padding(24)
+ .transition(.opacity.combined(with: .scale(scale: 0.98)))
+ .animation(.easeInOut(duration: 0.3), value: flowState)
+ }
+
+ // MARK: - Header Section
+
+ @ViewBuilder
+ private var headerSection: some View {
+ HStack(spacing: 12) {
+ Image(systemName: headerIcon)
+ .font(.system(size: 28))
+ .foregroundColor(headerColor)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(headerTitle)
+ .font(.system(.title3, design: .default, weight: .semibold))
+ .foregroundColor(.primary)
+
+ if let subtitle = headerSubtitle {
+ Text(subtitle)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ private var headerIcon: String {
+ switch flowState {
+ case .idle:
+ return "wrench.and.screwdriver"
+ case .restarting:
+ return "gearshape.2"
+ case .success:
+ return "checkmark.seal.fill"
+ case .partialFailure:
+ return "exclamationmark.triangle.fill"
+ case .failure:
+ return "xmark.octagon.fill"
+ }
+ }
+
+ private var headerColor: Color {
+ switch flowState {
+ case .idle: return .blue
+ case .restarting: return .blue
+ case .success: return .green
+ case .partialFailure: return .orange
+ case .failure: return .red
+ }
+ }
+
+ private var headerTitle: String {
+ switch flowState {
+ case .idle:
+ return "Ready to Fix"
+ case .restarting:
+ return "Restarting Touch Bar"
+ case .success:
+ return "Touch Bar Fixed!"
+ case .partialFailure:
+ return "Admin Access Needed"
+ case .failure:
+ return "Restart Failed"
+ }
+ }
+
+ private var headerSubtitle: String? {
+ switch flowState {
+ case .restarting:
+ return "This takes a few seconds..."
+ case .success:
+ return "All services restarted successfully"
+ default:
+ return nil
+ }
+ }
+
+ // MARK: - Content Section
+
+ @ViewBuilder
+ private var contentSection: some View {
+ switch flowState {
+ case .idle:
+ idleContent
+ case .restarting:
+ restartingContent
+ case .success:
+ successContent
+ case .partialFailure:
+ adminNeededContent
+ case .failure(let message):
+ failureContent(message: message)
+ }
+ }
+
+ private var idleContent: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("What will happen:")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ bulletPoint("Control Strip - UI elements layer")
+ bulletPoint("Touch Bar Server - Core functionality")
+ bulletPoint("Display Refresh - Graphics rendering")
+ }
+
+ Text("This is safe and takes only a few seconds.")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .padding(.top, 4)
+ }
+ }
+
+ private var restartingContent: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("What's happening:")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ Text("TouchBarFix restarts three core Apple services that control your Touch Bar:")
+ .font(.callout)
+ .foregroundColor(.secondary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ bulletPoint("Control Strip - UI elements layer")
+ bulletPoint("Touch Bar Server - Core functionality")
+ bulletPoint("Display Refresh - Graphics rendering")
+ }
+
+ HStack(spacing: 8) {
+ ProgressView()
+ .scaleEffect(0.8)
+ Text("Please wait...")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding(.top, 8)
+ }
+ }
+
+ private var successContent: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("All Touch Bar services have been successfully restarted.")
+ .font(.callout)
+ .foregroundColor(.secondary)
+
+ Text("Your Touch Bar should now be fully responsive. If you still experience issues, try a full Mac restart.")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ private var adminNeededContent: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ // Why admin is necessary
+ explanationBlock(
+ title: "Why admin is necessary:",
+ content: "Touch Bar Server runs as root. Normal users can't restart root processes without permission."
+ )
+
+ // Why it's safe
+ explanationBlock(
+ title: "Why it's safe:",
+ content: "TouchBarFix only restarts Apple's built-in Touch Bar processes. No data is modified."
+ )
+
+ // Alternative
+ explanationBlock(
+ title: "Alternative:",
+ content: "A Mac restart will restart all services without admin access."
+ )
+ }
+ }
+
+ private func failureContent(message: String) -> some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("The following issues occurred:")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ Text(message)
+ .font(.callout)
+ .foregroundColor(.red)
+ .padding(12)
+ .background(Color.red.opacity(0.1))
+ .cornerRadius(8)
+
+ Text("Troubleshooting steps:")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+ .padding(.top, 4)
+
+ VStack(alignment: .leading, spacing: 6) {
+ numberedPoint(1, "Try restarting the Touch Bar again")
+ numberedPoint(2, "Restart your Mac manually")
+ numberedPoint(3, "Contact support if issue persists")
+ }
+ }
+ }
+
+ // MARK: - Action Section
+
+ @ViewBuilder
+ private var actionSection: some View {
+ switch flowState {
+ case .idle:
+ EmptyView()
+
+ case .restarting:
+ HStack {
+ Button("Cancel", action: onCancel)
+ .buttonStyle(.borderless)
+ .keyboardShortcut(.cancelAction)
+ Spacer()
+ }
+
+ case .success:
+ HStack(spacing: 12) {
+ Button(action: onDone) {
+ Text("Done")
+ .font(.headline)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 12)
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ .buttonStyle(.plain)
+ .keyboardShortcut(.defaultAction)
+ .scaleEffect(hoveredButton == .primary ? 1.02 : 1.0)
+ .onHover { hoveredButton = $0 ? .primary : nil }
+
+ Button(action: onShare) {
+ Text("Tell a Friend")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ }
+
+ case .partialFailure:
+ adminActionButtons
+
+ case .failure:
+ failureActionButtons
+ }
+ }
+
+ private var adminActionButtons: some View {
+ VStack(spacing: 8) {
+ // Primary: Grant Admin
+ Button(action: onGrantAdmin) {
+ HStack(spacing: 8) {
+ Image(systemName: "lock.shield.fill")
+ .font(.body)
+
+ VStack(alignment: .leading, spacing: 1) {
+ Text("Grant Admin Access")
+ .font(.subheadline)
+ .fontWeight(.semibold)
+ Text("(Recommended)")
+ .font(.caption2)
+ .foregroundColor(.white.opacity(0.8))
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 10)
+ .padding(.horizontal, 14)
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(8)
+ }
+ .buttonStyle(.plain)
+ .keyboardShortcut(.defaultAction)
+ .scaleEffect(hoveredButton == .primary ? 1.02 : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: hoveredButton)
+ .onHover { hoveredButton = $0 ? .primary : nil }
+
+ // Secondary: Restart Mac
+ Button(action: onRestartComputer) {
+ HStack(spacing: 8) {
+ Image(systemName: "arrow.clockwise.circle.fill")
+ .font(.body)
+
+ Text("Restart Mac")
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 8)
+ .padding(.horizontal, 14)
+ .background(Color(NSColor.controlBackgroundColor))
+ .foregroundColor(.primary)
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(Color.gray.opacity(0.3), lineWidth: 1)
+ )
+ .cornerRadius(8)
+ }
+ .buttonStyle(.plain)
+ .scaleEffect(hoveredButton == .secondary ? 1.02 : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: hoveredButton)
+ .onHover { hoveredButton = $0 ? .secondary : nil }
+
+ // Tertiary: Cancel (proper button, left-aligned)
+ HStack {
+ Button("Cancel", action: onCancel)
+ .buttonStyle(.borderless)
+ .keyboardShortcut(.cancelAction)
+ Spacer()
+ }
+ .padding(.top, 4)
+ }
+ }
+
+ private var failureActionButtons: some View {
+ VStack(spacing: 10) {
+ Button(action: onTryAgain) {
+ Text("Try Again")
+ .font(.headline)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 12)
+ .background(Color.orange)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ .buttonStyle(.plain)
+ .keyboardShortcut(.defaultAction)
+ .scaleEffect(hoveredButton == .primary ? 1.02 : 1.0)
+ .onHover { hoveredButton = $0 ? .primary : nil }
+
+ HStack {
+ Button("Close", action: onCancel)
+ .buttonStyle(.borderless)
+ .keyboardShortcut(.cancelAction)
+ Spacer()
+ }
+ .padding(.top, 8)
+ }
+ }
+
+ // MARK: - Helper Views
+
+ private func bulletPoint(_ text: String) -> some View {
+ HStack(alignment: .top, spacing: 8) {
+ Text("•")
+ .foregroundColor(.secondary)
+ Text(text)
+ .font(.callout)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ private func numberedPoint(_ number: Int, _ text: String) -> some View {
+ HStack(alignment: .top, spacing: 8) {
+ Text("\(number).")
+ .font(.callout)
+ .foregroundColor(.secondary)
+ .frame(width: 16, alignment: .trailing)
+ Text(text)
+ .font(.callout)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ private func explanationBlock(title: String, content: String) -> some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ Text(content)
+ .font(.callout)
+ .foregroundColor(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+}
+
+// MARK: - Preview
+
+#if DEBUG
+struct ContextPanelView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ ContextPanelView(
+ flowState: .idle,
+ onGrantAdmin: {},
+ onRestartComputer: {},
+ onCancel: {},
+ onDone: {},
+ onTryAgain: {},
+ onShare: {}
+ )
+ .frame(width: 400, height: 400)
+ .background(Color(NSColor.windowBackgroundColor))
+ .previewDisplayName("Idle")
+
+ ContextPanelView(
+ flowState: .partialFailure(needsAdmin: true),
+ onGrantAdmin: {},
+ onRestartComputer: {},
+ onCancel: {},
+ onDone: {},
+ onTryAgain: {},
+ onShare: {}
+ )
+ .frame(width: 400, height: 400)
+ .background(Color(NSColor.windowBackgroundColor))
+ .previewDisplayName("Needs Admin")
+ }
+ }
+}
+#endif
diff --git a/App/Sources/TouchBarDashboardView.swift b/App/Sources/TouchBarDashboardView.swift
new file mode 100644
index 0000000..f8fd28e
--- /dev/null
+++ b/App/Sources/TouchBarDashboardView.swift
@@ -0,0 +1,307 @@
+import SwiftUI
+
+/// Dashboard layout for Touch Bar restart process.
+/// Shows status panel on the left (always visible) and context panel on the right.
+struct TouchBarDashboardView: View {
+ @ObservedObject var progress: RestartProgress
+
+ /// Current flow state
+ let flowState: ContentViewFlowState
+
+ /// Callbacks for user actions
+ let onGrantAdmin: () -> Void
+ let onRestartComputer: () -> Void
+ let onCancel: () -> Void
+ let onDone: () -> Void
+ let onTryAgain: () -> Void
+ let onShare: () -> Void
+
+ var body: some View {
+ HStack(spacing: 0) {
+ // LEFT: Status Panel - Always visible
+ StatusPanelView(progress: progress)
+ .frame(width: 220)
+
+ // Divider
+ Rectangle()
+ .fill(Color(NSColor.separatorColor))
+ .frame(width: 1)
+
+ // RIGHT: Context Panel - Changes based on state
+ ContextPanelView(
+ flowState: flowState,
+ onGrantAdmin: onGrantAdmin,
+ onRestartComputer: onRestartComputer,
+ onCancel: onCancel,
+ onDone: onDone,
+ onTryAgain: onTryAgain,
+ onShare: onShare
+ )
+ .frame(maxWidth: .infinity)
+ }
+ .frame(width: 640, height: 400)
+ .background(Color(NSColor.windowBackgroundColor))
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.15), radius: 10, x: 0, y: 4)
+ }
+}
+
+// MARK: - Status Panel (Left Side)
+
+/// Left panel showing process status - always visible during restart flow
+struct StatusPanelView: View {
+ @ObservedObject var progress: RestartProgress
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ // Header
+ Text("Status")
+ .font(.system(.title3, design: .default, weight: .semibold))
+ .foregroundColor(.primary)
+
+ // Process list
+ VStack(spacing: 8) {
+ ForEach(progress.processes) { process in
+ CompactProcessRow(process: process)
+ }
+ }
+
+ Spacer()
+
+ // Overall status summary
+ statusSummary
+ }
+ .padding(16)
+ .background(Color(NSColor.controlBackgroundColor).opacity(0.3))
+ }
+
+ @ViewBuilder
+ private var statusSummary: some View {
+ HStack(spacing: 8) {
+ Image(systemName: summaryIcon)
+ .foregroundColor(summaryColor)
+
+ Text(summaryText)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding(.vertical, 8)
+ .padding(.horizontal, 12)
+ .background(summaryColor.opacity(0.1))
+ .cornerRadius(8)
+ }
+
+ private var summaryIcon: String {
+ switch progress.overallState {
+ case .idle:
+ return "circle.dashed"
+ case .restarting:
+ return "arrow.triangle.2.circlepath"
+ case .success:
+ return "checkmark.seal.fill"
+ case .partialFailure:
+ return "exclamationmark.triangle.fill"
+ case .failure:
+ return "xmark.octagon.fill"
+ }
+ }
+
+ private var summaryColor: Color {
+ switch progress.overallState {
+ case .idle:
+ return .gray
+ case .restarting:
+ return .blue
+ case .success:
+ return .green
+ case .partialFailure:
+ return .orange
+ case .failure:
+ return .red
+ }
+ }
+
+ private var summaryText: String {
+ switch progress.overallState {
+ case .idle:
+ return "Ready"
+ case .restarting:
+ return "In Progress..."
+ case .success:
+ return "All Complete"
+ case .partialFailure:
+ return "Partial Success"
+ case .failure:
+ return "Failed"
+ }
+ }
+}
+
+// MARK: - Compact Process Row
+
+/// Compact version of process status row for the left panel
+struct CompactProcessRow: View {
+ let process: UIProcessInfo
+ @State private var isPulsing = false
+
+ var body: some View {
+ HStack(spacing: 10) {
+ statusIndicator
+ .frame(width: 16, height: 16)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(process.displayName)
+ .font(.system(.subheadline, design: .default, weight: .medium))
+ .foregroundColor(.primary)
+
+ Text(statusText)
+ .font(.caption2)
+ .foregroundColor(statusColor)
+ }
+
+ Spacer()
+ }
+ .padding(.vertical, 6)
+ .padding(.horizontal, 10)
+ .background(backgroundColor)
+ .cornerRadius(6)
+ .onAppear {
+ if case .inProgress = process.status {
+ isPulsing = true
+ }
+ }
+ .onChange(of: process.status) { newStatus in
+ isPulsing = (newStatus == .inProgress)
+ }
+ }
+
+ @ViewBuilder
+ private var statusIndicator: some View {
+ switch process.status {
+ case .pending:
+ Circle()
+ .stroke(Color.gray.opacity(0.5), lineWidth: 1.5)
+ case .inProgress:
+ Circle()
+ .fill(Color.blue)
+ .scaleEffect(isPulsing ? 1.2 : 1.0)
+ .opacity(isPulsing ? 0.7 : 1.0)
+ .animation(
+ .easeInOut(duration: 0.6)
+ .repeatForever(autoreverses: true),
+ value: isPulsing
+ )
+ case .success:
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundColor(.green)
+ .font(.system(size: 14, weight: .semibold))
+ case .failed:
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.red)
+ .font(.system(size: 14, weight: .semibold))
+ }
+ }
+
+ private var statusText: String {
+ switch process.status {
+ case .pending:
+ return "Waiting..."
+ case .inProgress:
+ return "Restarting..."
+ case .success:
+ return "Complete"
+ case .failed(let reason):
+ return reason == .needsAdmin ? "Needs Admin" : "Failed"
+ }
+ }
+
+ private var statusColor: Color {
+ switch process.status {
+ case .pending: return .gray
+ case .inProgress: return .blue
+ case .success: return .green
+ case .failed: return .red
+ }
+ }
+
+ private var backgroundColor: Color {
+ switch process.status {
+ case .pending: return Color.gray.opacity(0.05)
+ case .inProgress: return Color.blue.opacity(0.1)
+ case .success: return Color.green.opacity(0.1)
+ case .failed: return Color.red.opacity(0.1)
+ }
+ }
+}
+
+// MARK: - Preview
+
+#if DEBUG
+struct TouchBarDashboardView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ // Restarting state
+ TouchBarDashboardView(
+ progress: {
+ let p = RestartProgress()
+ p.controlStrip = .success
+ p.touchBarServer = .inProgress
+ p.displayRefresh = .pending
+ p.overallState = .restarting
+ return p
+ }(),
+ flowState: .restarting,
+ onGrantAdmin: {},
+ onRestartComputer: {},
+ onCancel: {},
+ onDone: {},
+ onTryAgain: {},
+ onShare: {}
+ )
+ .previewDisplayName("Restarting")
+
+ // Needs admin
+ TouchBarDashboardView(
+ progress: {
+ let p = RestartProgress()
+ p.controlStrip = .success
+ p.touchBarServer = .failed(reason: .needsAdmin)
+ p.displayRefresh = .success
+ p.overallState = .partialFailure(needsAdmin: true)
+ return p
+ }(),
+ flowState: .partialFailure(needsAdmin: true),
+ onGrantAdmin: {},
+ onRestartComputer: {},
+ onCancel: {},
+ onDone: {},
+ onTryAgain: {},
+ onShare: {}
+ )
+ .previewDisplayName("Needs Admin")
+
+ // Success
+ TouchBarDashboardView(
+ progress: {
+ let p = RestartProgress()
+ p.controlStrip = .success
+ p.touchBarServer = .success
+ p.displayRefresh = .success
+ p.overallState = .success
+ return p
+ }(),
+ flowState: .success(usedAdmin: false),
+ onGrantAdmin: {},
+ onRestartComputer: {},
+ onCancel: {},
+ onDone: {},
+ onTryAgain: {},
+ onShare: {}
+ )
+ .previewDisplayName("Success")
+ }
+ .padding()
+ .background(Color.gray.opacity(0.2))
+ }
+}
+#endif
diff --git a/App/Sources/main.swift b/App/Sources/main.swift
index a59913d..866544e 100644
--- a/App/Sources/main.swift
+++ b/App/Sources/main.swift
@@ -11,12 +11,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.setActivationPolicy(.regular)
// Create and configure window
+ // Start at idle size, will resize for dashboard when needed
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 360),
- styleMask: [.titled, .closable, .miniaturizable],
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
+
+ // Allow window to resize based on content
+ window.contentMinSize = NSSize(width: 420, height: 360)
+ window.contentMaxSize = NSSize(width: 800, height: 600)
window.center()
window.title = "TouchBarFix"
diff --git a/App/build-app.sh b/App/build-app.sh
index 9d913b1..fbff289 100755
--- a/App/build-app.sh
+++ b/App/build-app.sh
@@ -10,7 +10,7 @@ echo "🔨 Building TouchBarFix App Bundle..."
# Configuration
APP_NAME="TouchBarFix"
BUNDLE_ID="com.produktentdecker.touchbarfix"
-VERSION="1.4.1"
+VERSION="1.5.0"
BUILD_DIR=".build"
RELEASE_DIR="Release"
diff --git a/App/create-dmg.sh b/App/create-dmg.sh
index ebc6066..f311f30 100755
--- a/App/create-dmg.sh
+++ b/App/create-dmg.sh
@@ -4,7 +4,7 @@
set -e
APP_NAME="TouchBarFix"
-DMG_NAME="TouchBarFix-1.4.1"
+DMG_NAME="TouchBarFix-1.5.0"
RELEASE_DIR="Release"
APP_BUNDLE="$RELEASE_DIR/$APP_NAME.app"
@@ -35,7 +35,7 @@ ln -s /Applications "$DMG_DIR/Applications"
# Create README file
cat > "$DMG_DIR/README.txt" << EOF
-TouchBarFix v1.4.1
+TouchBarFix v1.5.0
==========================
Installation:
diff --git a/todos/001-pending-p2-continuous-pulsing-animation.md b/todos/001-pending-p2-continuous-pulsing-animation.md
new file mode 100644
index 0000000..3be16aa
--- /dev/null
+++ b/todos/001-pending-p2-continuous-pulsing-animation.md
@@ -0,0 +1,107 @@
+---
+status: pending
+priority: p2
+issue_id: "001"
+tags: [code-review, performance, animation, swift]
+dependencies: []
+---
+
+# Continuous Pulsing Animation Never Stops
+
+## Problem Statement
+
+The pulsing animation in `CompactProcessRow` uses `.repeatForever()` but continues running even after the process transitions away from `.inProgress` state. SwiftUI's animation system does not automatically stop repeating animations when the condition changes.
+
+## Findings
+
+**Source:** Performance Oracle Agent
+
+**Location:** `App/Sources/TouchBarDashboardView.swift:186-194`
+
+```swift
+Circle()
+ .fill(Color.blue)
+ .scaleEffect(isPulsing ? 1.2 : 1.0)
+ .opacity(isPulsing ? 0.7 : 1.0)
+ .animation(
+ .easeInOut(duration: 0.6)
+ .repeatForever(autoreverses: true),
+ value: isPulsing
+ )
+```
+
+**Impact:** The animation continues running in the background, consuming CPU cycles even when not visible. With 3 process rows, this could mean 3 continuous animations running simultaneously.
+
+## Proposed Solutions
+
+### Option A: Extract to Separate View (Recommended)
+
+**Pros:** Clean lifecycle management, animation stops when view disappears
+**Cons:** Slightly more code
+**Effort:** Small (30 min)
+**Risk:** Low
+
+```swift
+struct PulsingCircle: View {
+ @State private var isPulsing = false
+
+ var body: some View {
+ Circle()
+ .fill(Color.blue)
+ .scaleEffect(isPulsing ? 1.2 : 1.0)
+ .opacity(isPulsing ? 0.7 : 1.0)
+ .onAppear {
+ withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
+ isPulsing = true
+ }
+ }
+ }
+}
+```
+
+### Option B: Conditional Animation
+
+**Pros:** Simpler, fewer files
+**Cons:** May not cleanly stop animation
+**Effort:** Small (15 min)
+**Risk:** Medium - may not work as expected
+
+```swift
+.animation(
+ process.status == .inProgress
+ ? .easeInOut(duration: 0.6).repeatForever(autoreverses: true)
+ : .default,
+ value: process.status
+)
+```
+
+## Recommended Action
+
+Option A - Extract to separate `PulsingCircle` view
+
+## Technical Details
+
+**Affected Files:**
+- `App/Sources/TouchBarDashboardView.swift`
+
+**Components:**
+- `CompactProcessRow`
+- `statusIndicator` ViewBuilder
+
+## Acceptance Criteria
+
+- [ ] Pulsing animation only runs when process is `.inProgress`
+- [ ] Animation stops when process transitions to success/failure
+- [ ] CPU usage returns to baseline after restart completes
+- [ ] No visual regression in animation appearance
+
+## Work Log
+
+| Date | Action | Notes |
+|------|--------|-------|
+| 2026-01-01 | Created | From code review of v1.5.0 dashboard changes |
+
+## Resources
+
+- PR: Uncommitted v1.5.0 changes on main
+- SwiftUI animation lifecycle: https://developer.apple.com/documentation/swiftui/animation
diff --git a/todos/002-pending-p2-unused-progress-parameter.md b/todos/002-pending-p2-unused-progress-parameter.md
new file mode 100644
index 0000000..247e4a6
--- /dev/null
+++ b/todos/002-pending-p2-unused-progress-parameter.md
@@ -0,0 +1,77 @@
+---
+status: pending
+priority: p2
+issue_id: "002"
+tags: [code-review, simplicity, swift]
+dependencies: []
+---
+
+# Unused `progress` Parameter in ContextPanelView
+
+## Problem Statement
+
+The `ContextPanelView` receives an `@ObservedObject var progress: RestartProgress` parameter but never uses it. This creates unnecessary coupling and could cause unintended view rebuilds.
+
+## Findings
+
+**Source:** Simplicity Reviewer Agent
+
+**Location:** `App/Sources/ContextPanelView.swift:6`
+
+```swift
+struct ContextPanelView: View {
+ @ObservedObject var progress: RestartProgress // Never used!
+ let flowState: ContentViewFlowState
+ // ...
+}
+```
+
+**Impact:**
+- Unnecessary dependency in view signature
+- Potential for view rebuilds when progress changes (even though nothing uses it)
+- Confusing API for future maintainers
+
+## Proposed Solutions
+
+### Option A: Remove Parameter (Recommended)
+
+**Pros:** Cleaner API, no unnecessary observation
+**Cons:** Breaking change to call sites
+**Effort:** Small (15 min)
+**Risk:** Low
+
+Remove from `ContextPanelView` and update call site in `TouchBarDashboardView`.
+
+### Option B: Keep for Future Use
+
+**Pros:** Available if needed later
+**Cons:** YAGNI violation
+**Effort:** None
+**Risk:** Low
+
+## Recommended Action
+
+Option A - Remove the unused parameter
+
+## Technical Details
+
+**Affected Files:**
+- `App/Sources/ContextPanelView.swift` (line 6)
+- `App/Sources/TouchBarDashboardView.swift` (lines 31-32)
+
+## Acceptance Criteria
+
+- [ ] `progress` parameter removed from ContextPanelView
+- [ ] Call site in TouchBarDashboardView updated
+- [ ] App builds and functions correctly
+- [ ] No visual or behavioral changes
+
+## Work Log
+
+| Date | Action | Notes |
+|------|--------|-------|
+| 2026-01-01 | Created | From code review of v1.5.0 dashboard changes |
+
+## Resources
+
+- PR: Uncommitted v1.5.0 changes on main
diff --git a/todos/003-pending-p2-deprecated-onchange-api.md b/todos/003-pending-p2-deprecated-onchange-api.md
new file mode 100644
index 0000000..0ff1483
--- /dev/null
+++ b/todos/003-pending-p2-deprecated-onchange-api.md
@@ -0,0 +1,71 @@
+---
+status: pending
+priority: p2
+issue_id: "003"
+tags: [code-review, deprecation, swift, pattern]
+dependencies: []
+---
+
+# Deprecated onChange API Usage
+
+## Problem Statement
+
+The code uses the deprecated single-parameter `onChange(of:perform:)` API which is deprecated in iOS 17/macOS 14.
+
+## Findings
+
+**Source:** Pattern Recognition Agent
+
+**Location:** `App/Sources/TouchBarDashboardView.swift:174`
+
+```swift
+.onChange(of: process.status) { newStatus in
+ isPulsing = (newStatus == .inProgress)
+}
+```
+
+**Impact:**
+- Deprecation warning in Xcode
+- May be removed in future Swift/SwiftUI versions
+- Different performance characteristics
+
+## Proposed Solutions
+
+### Option A: Update to New API (Recommended)
+
+**Pros:** Future-proof, no deprecation warning
+**Cons:** Minor code change
+**Effort:** Small (10 min)
+**Risk:** Low
+
+```swift
+.onChange(of: process.status) { oldStatus, newStatus in
+ isPulsing = (newStatus == .inProgress)
+}
+```
+
+## Recommended Action
+
+Option A - Update to new two-parameter API
+
+## Technical Details
+
+**Affected Files:**
+- `App/Sources/TouchBarDashboardView.swift` (line 174)
+- Also check `RestartProgressView.swift` (line 179)
+
+## Acceptance Criteria
+
+- [ ] No deprecation warnings for onChange
+- [ ] Animation behavior unchanged
+- [ ] App builds on macOS 14+
+
+## Work Log
+
+| Date | Action | Notes |
+|------|--------|-------|
+| 2026-01-01 | Created | From code review of v1.5.0 dashboard changes |
+
+## Resources
+
+- Apple docs: https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-8wgp9
diff --git a/todos/004-pending-p3-remove-hover-animation.md b/todos/004-pending-p3-remove-hover-animation.md
new file mode 100644
index 0000000..18b1ecc
--- /dev/null
+++ b/todos/004-pending-p3-remove-hover-animation.md
@@ -0,0 +1,86 @@
+---
+status: pending
+priority: p3
+issue_id: "004"
+tags: [code-review, simplicity, performance, swift]
+dependencies: []
+---
+
+# Remove Barely-Visible Hover Animation System
+
+## Problem Statement
+
+The `ContextPanelView` has a complex hover state management system (`hoveredButton` enum and state, multiple `.onHover` handlers) for a barely-noticeable 2% scale effect (1.02x). The complexity is not worth the minimal visual benefit.
+
+## Findings
+
+**Source:** Simplicity Reviewer Agent, Performance Oracle Agent
+
+**Location:** `App/Sources/ContextPanelView.swift:17-21, 275-276, 322-324, 350-352, 378-379`
+
+```swift
+@State private var hoveredButton: ButtonType?
+
+private enum ButtonType {
+ case primary, secondary, tertiary
+}
+
+// Multiple places:
+.scaleEffect(hoveredButton == .primary ? 1.02 : 1.0)
+.onHover { hoveredButton = $0 ? .primary : nil }
+```
+
+**Impact:**
+- ~15 lines of complexity for imperceptible effect
+- Each hover triggers state change and view rebuild
+- Cognitive load for maintainers
+
+## Proposed Solutions
+
+### Option A: Remove Entirely (Recommended)
+
+**Pros:** Simplest, removes all complexity
+**Cons:** Slightly less polish (but barely visible anyway)
+**Effort:** Small (20 min)
+**Risk:** Low
+
+Delete all hover-related code.
+
+### Option B: Use `.hoverEffect()` Modifier (macOS 13+)
+
+**Pros:** System-managed, less code
+**Cons:** May not work identically
+**Effort:** Small (15 min)
+**Risk:** Low
+
+## Recommended Action
+
+Option A - Remove hover animation system entirely
+
+## Technical Details
+
+**Code to Remove:**
+- Lines 17-21: `@State private var hoveredButton` and `ButtonType` enum
+- Lines 275-276: hover effect on success Done button
+- Lines 322-324: hover effect on Grant Admin button
+- Lines 350-352: hover effect on Restart Mac button
+- Lines 378-379: hover effect on Try Again button
+
+**Estimated LOC reduction:** ~15 lines
+
+## Acceptance Criteria
+
+- [ ] All hover-related code removed
+- [ ] No visual regression (buttons still look correct)
+- [ ] Buttons still respond to clicks
+- [ ] Build succeeds
+
+## Work Log
+
+| Date | Action | Notes |
+|------|--------|-------|
+| 2026-01-01 | Created | From code review of v1.5.0 dashboard changes |
+
+## Resources
+
+- PR: Uncommitted v1.5.0 changes on main