Skip to content

Add OSC 9;4 progress reporting (pane bar + tab indicator)#28

Merged
trydis merged 5 commits intomainfrom
progress-reporting
Mar 15, 2026
Merged

Add OSC 9;4 progress reporting (pane bar + tab indicator)#28
trydis merged 5 commits intomainfrom
progress-reporting

Conversation

@trydis
Copy link
Owner

@trydis trydis commented Mar 15, 2026

Receives GHOSTTY_ACTION_PROGRESS_REPORT from libghostty and renders a 2px progress bar at the top of the active terminal surface. Tab dots reflect the progress state (accent/red/orange). State is auto-cleared after 15 seconds of inactivity, matching Ghostty's own behavior.

Summary by CodeRabbit

  • New Features
    • Terminal panes show a 2px progress indicator with color-coded states (error red, pause orange, accent otherwise).
    • Progress bars support determinate values and an indeterminate bouncing animation.
    • Active surface tabs reflect progress; a non-interactive progress bar appears under the active panel.
    • Progress reports propagate to workspace state and auto-clear after a short timeout.
  • Tests
    • Added test verifying progress-report callbacks are forwarded and invoked.

Receives GHOSTTY_ACTION_PROGRESS_REPORT from libghostty and renders a
2px progress bar at the top of the active terminal surface. Tab dots
reflect the progress state (accent/red/orange). State is auto-cleared
after 15 seconds of inactivity, matching Ghostty's own behavior.
@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26d75d66-8207-4bd1-9a67-a29af1686d17

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds per-surface progress reporting: new SurfaceProgressReport model, UI components (SurfaceProgressBar and tab-dot coloring), workspace manager tracking with auto-clear timers, and a progress-report callback threaded from backend (Ghostty) through GhosttyRuntime → LibghosttySurfaceView → GhosttyTerminalView into the UI.

Changes

Cohort / File(s) Summary
Data Model
Sources/Shellraiser/Models/SurfaceProgressReport.swift
Adds SurfaceProgressState and SurfaceProgressReport (state + optional 0–100 progress, tint mapping).
UI: Progress Display
Sources/Shellraiser/Features/WorkspaceDetail/SurfaceProgressBar.swift
New SwiftUI 2px progress bar with determinate/indeterminate modes, color mapping and accessibility; includes BouncingProgressBar animation.
UI: Pane/Tab Integration
Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift, Sources/Shellraiser/Features/WorkspaceDetail/SurfaceTabButton.swift
PaneLeafView forwards progress reports to WorkspaceManager and renders SurfaceProgressBar for active surface; SurfaceTabButton computes tab-dot fill from progress state.
Terminal View Wiring
Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift, Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift
Threads onProgressReport: (SurfaceProgressReport?) -> Void through GhosttyTerminalView, host acquisition/update, and LibghosttySurfaceView APIs; updates call sites and forwarded parameters.
Runtime: Ghostty
Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
Adds SurfaceActionEvent.progressReport, includes onProgressReport in SurfaceCallbacks/register/acquire flows, and dispatches backend progress events to surface callbacks.
Workspace Management
Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift, .../WorkspaceManager+SurfaceOperations.swift
Adds @Published progressBySurfaceId, timers and APIs setProgressReport, focusedSurfaceProgress, clearProgressReport with 15s auto-clear; clears on surface close.
Tests
Tests/ShellraiserTests/GhosttyTerminalViewTests.swift
Extends mocks and sync helpers to accept/forward onProgressReport; adds test verifying forwarding and invocation.
Project Config
Shellraiser.xcodeproj/project.pbxproj
Updates PBX references/groups/build-phase entries to add/rename SurfaceProgressReport.swift, SurfaceProgressBar.swift and reorder source entries.

Sequence Diagram(s)

sequenceDiagram
    participant Backend as Backend (Ghostty)
    participant Runtime as GhosttyRuntime
    participant Surface as LibghosttySurfaceView
    participant Manager as WorkspaceManager
    participant UI as PaneLeafView / SurfaceProgressBar

    Backend->>Runtime: GHOSTTY_ACTION_PROGRESS_REPORT payload
    Runtime->>Runtime: Convert payload -> SurfaceProgressReport?
    Runtime->>Surface: callbacks.onProgressReport(report)
    Surface->>Manager: setProgressReport(workspaceId, surfaceId, report)
    Manager->>Manager: Store progressBySurfaceId[surfaceId], start 15s timer
    Manager->>UI: Publish progressBySurfaceId update
    UI->>UI: Render/refresh SurfaceProgressBar and tab-dot

    Note over Manager: After 15s timer triggers
    Manager->>Manager: clearProgressReport(surfaceId)
    Manager->>UI: Publish removal → UI hides progress indicator
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding OSC 9;4 progress reporting with visual indicators (pane bar and tab indicator).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift (1)

148-154: Consider extracting the auto-clear delay into a named constant.

This keeps behavior easier to tune and avoids scattering timing literals.

♻️ Proposed refactor
+private enum ProgressReporting {
+    static let autoClearDelay: TimeInterval = 15
+}
+
 /// Stores or removes an OSC 9;4 progress report for a surface, resetting the 15-second auto-clear timer.
 func setProgressReport(workspaceId: UUID, surfaceId: UUID, report: SurfaceProgressReport?) {
     if let report {
         progressBySurfaceId[surfaceId] = report
         progressClearTimers[surfaceId]?.invalidate()
-        progressClearTimers[surfaceId] = Timer.scheduledTimer(withTimeInterval: 15, repeats: false) { [weak self] _ in
+        progressClearTimers[surfaceId] = Timer.scheduledTimer(withTimeInterval: ProgressReporting.autoClearDelay, repeats: false) { [weak self] _ in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift
around lines 148 - 154, Extract the hard-coded 15-second auto-clear delay in
setProgressReport into a named constant (e.g., progressAutoClearInterval or
kProgressAutoClearInterval) and use that constant when scheduling
Timer.scheduledTimer; update any other occurrences of the same literal (such as
other Timer.scheduledTimer uses related to progress clearing) to reference the
constant and add it near WorkspaceManager (as a static let or file-level private
constant) so the timing is easy to tune and annotated.
Tests/ShellraiserTests/GhosttyTerminalViewTests.swift (1)

203-224: Consider adding one assertion that onProgressReport is actually propagated.

Current updates only ensure the parameter is accepted. A small callback-forwarding assertion would lock in the new behavior and prevent silent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ShellraiserTests/GhosttyTerminalViewTests.swift` around lines 203 -
224, Add an assertion in the update(...) test helper to verify that the
onProgressReport closure is forwarded: in the update function (the one that
currently assigns workingDirectoryChangeHandler and appends to
updatedSurfaceIds) accept and store a local variable or call site-provided stub
for onProgressReport and trigger it with a sample SurfaceProgressReport, then
assert the receiver/handler (or a test-recorded variable) gets that same value;
reference the update(...) function and the onProgressReport parameter and
updatedSurfaceIds to locate where to add the forwarding/assertion.
Sources/Shellraiser/Features/WorkspaceDetail/SurfaceProgressBar.swift (1)

11-17: Centralize progress-state color mapping to avoid drift across views.

SurfaceProgressBar and SurfaceTabButton both encode the same state→color logic. A shared helper keeps them synchronized.

♻️ Proposed refactor
+extension SurfaceProgressState {
+    var tintColor: Color {
+        switch self {
+        case .error: .red
+        case .pause: .orange
+        default: .accentColor
+        }
+    }
+}
+
 struct SurfaceProgressBar: View {
     let report: SurfaceProgressReport
 
     private var color: Color {
-        switch report.state {
-        case .error: return .red
-        case .pause: return .orange
-        default: return .accentColor
-        }
+        report.state.tintColor
     }

Also apply report.state.tintColor in Sources/Shellraiser/Features/WorkspaceDetail/SurfaceTabButton.swift Line 32-36.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Shellraiser/Features/WorkspaceDetail/SurfaceProgressBar.swift` around
lines 11 - 17, Add a centralized color mapping by adding a computed property
named tintColor on the shared state type (e.g., Report.State or whatever type
report.state is) that returns .red for .error, .orange for .pause, and
.accentColor otherwise, then replace the duplicated logic inside
SurfaceProgressBar (the private var color) and SurfaceTabButton (lines where
state→color is implemented) to use report.state.tintColor instead so both views
reference the single source of truth.
Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift (1)

138-150: Collapse host updates into one write path.

acquireHostView(...) mutates existing hosts here, and GhosttyTerminalView.syncHostView(...) in Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift, Lines 236-247 immediately calls host.update(...) again. Every new callback now has to be threaded through both mutation paths, which is getting easy to drift. Consider making acquisition pure and keeping host mutation in one place.

As per coding guidelines, "Prefer small, composable methods and keep methods focused and readable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift` around lines
138 - 150, The host acquisition currently mutates host state in acquireHostView
(via existing.update(...) on hostViewsBySurfaceId) while
GhosttyTerminalView.syncHostView also calls host.update(...), causing duplicated
mutation paths; change acquireHostView to be pure/side-effect-free (return the
existing host or create a new host without invoking update callbacks) and
consolidate all state mutations to a single place (e.g.,
GhosttyTerminalView.syncHostView), so only syncHostView calls host.update(...)
(or a single canonical update method) and all callback wiring (onActivate,
onInput, onTitleChange, etc.) is applied there; update references to
existing.update and host.update usages accordingly and remove the duplicate
mutation in acquireHostView to ensure callbacks are only threaded through the
single write path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift`:
- Around line 27-28: The enum case progressReport(SurfaceProgressReport?)
conflates an explicit "remove" (nil) with unsupported/unknown states because the
switch's default branch also returns nil; change the model to distinguish
explicit remove from ignored/unsupported states (e.g., add a dedicated case like
progressRemoved or make the associated value non-optional:
progressReport(SurfaceProgressReport) plus a separate progressRemoved case) and
update all switch handling (the branches that currently return nil in the
default path) to leave progress unchanged for unsupported states while only
treating the explicit remove case as a removal; update any code that constructs
or matches progressReport to use the new explicit remove/non-optional pattern so
unknown states do not clear progress.

In `@Sources/Shellraiser/Models/SurfaceProgressReport.swift`:
- Around line 10-15: SurfaceProgressReport currently allows progress values
101...255 because it's a bare UInt8; add a validating initializer on
SurfaceProgressReport (e.g., init(state: SurfaceProgressState, progress:
UInt8?)) that checks if progress is nil or within 0...100 and otherwise
rejects/handles the value (use a failable init returning nil or
assert/precondition depending on desired behavior), and make callers use this
initializer so the invariant is enforced; reference SurfaceProgressReport, its
progress property, and SurfaceProgressState when implementing the validation.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift:
- Around line 149-156: In setProgressReport, avoid writing progress/timer
entries for surfaces that no longer exist and prevent the timer closure from
re-inserting them: before assigning progressBySurfaceId[surfaceId] and creating
progressClearTimers[surfaceId], verify the surface is still present in the
workspace (e.g., check workspace.surfaces contains surfaceId or call a
surfaceExists(workspaceId:surfaceId) helper); and inside the Timer scheduled
closure, re-check that the surface still exists before removing or modifying
progressBySurfaceId/progressClearTimers (and early-return if the surface was
removed) so stale callbacks cannot reinsert entries for closed surfaces. Ensure
you reference setProgressReport, progressBySurfaceId, progressClearTimers and
the workspace surface presence check when implementing the guards.

---

Nitpick comments:
In `@Sources/Shellraiser/Features/WorkspaceDetail/SurfaceProgressBar.swift`:
- Around line 11-17: Add a centralized color mapping by adding a computed
property named tintColor on the shared state type (e.g., Report.State or
whatever type report.state is) that returns .red for .error, .orange for .pause,
and .accentColor otherwise, then replace the duplicated logic inside
SurfaceProgressBar (the private var color) and SurfaceTabButton (lines where
state→color is implemented) to use report.state.tintColor instead so both views
reference the single source of truth.

In `@Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift`:
- Around line 138-150: The host acquisition currently mutates host state in
acquireHostView (via existing.update(...) on hostViewsBySurfaceId) while
GhosttyTerminalView.syncHostView also calls host.update(...), causing duplicated
mutation paths; change acquireHostView to be pure/side-effect-free (return the
existing host or create a new host without invoking update callbacks) and
consolidate all state mutations to a single place (e.g.,
GhosttyTerminalView.syncHostView), so only syncHostView calls host.update(...)
(or a single canonical update method) and all callback wiring (onActivate,
onInput, onTitleChange, etc.) is applied there; update references to
existing.update and host.update usages accordingly and remove the duplicate
mutation in acquireHostView to ensure callbacks are only threaded through the
single write path.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift:
- Around line 148-154: Extract the hard-coded 15-second auto-clear delay in
setProgressReport into a named constant (e.g., progressAutoClearInterval or
kProgressAutoClearInterval) and use that constant when scheduling
Timer.scheduledTimer; update any other occurrences of the same literal (such as
other Timer.scheduledTimer uses related to progress clearing) to reference the
constant and add it near WorkspaceManager (as a static let or file-level private
constant) so the timing is easy to tune and annotated.

In `@Tests/ShellraiserTests/GhosttyTerminalViewTests.swift`:
- Around line 203-224: Add an assertion in the update(...) test helper to verify
that the onProgressReport closure is forwarded: in the update function (the one
that currently assigns workingDirectoryChangeHandler and appends to
updatedSurfaceIds) accept and store a local variable or call site-provided stub
for onProgressReport and trigger it with a sample SurfaceProgressReport, then
assert the receiver/handler (or a test-recorded variable) gets that same value;
reference the update(...) function and the onProgressReport parameter and
updatedSurfaceIds to locate where to add the forwarding/assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e63da255-4ba1-484b-ba32-1562f0975d35

📥 Commits

Reviewing files that changed from the base of the PR and between dba8f15 and 28b1ab6.

📒 Files selected for processing (11)
  • Shellraiser.xcodeproj/project.pbxproj
  • Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/SurfaceProgressBar.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/SurfaceTabButton.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift
  • Sources/Shellraiser/Models/SurfaceProgressReport.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift
  • Tests/ShellraiserTests/GhosttyTerminalViewTests.swift

Comment on lines +10 to +15
struct SurfaceProgressReport: Equatable {
/// Current progress state.
let state: SurfaceProgressState
/// Percentage completion (0–100), or nil when not quantified.
let progress: UInt8?
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Enforce the documented 0–100 invariant for progress.

UInt8 permits 101...255, but the model contract says 0–100. That allows invalid state into UI/accessibility paths.

🔧 Proposed fix
 struct SurfaceProgressReport: Equatable {
     /// Current progress state.
     let state: SurfaceProgressState
     /// Percentage completion (0–100), or nil when not quantified.
     let progress: UInt8?
+
+    init(state: SurfaceProgressState, progress: UInt8?) {
+        self.state = state
+        self.progress = progress.map { min($0, 100) }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Shellraiser/Models/SurfaceProgressReport.swift` around lines 10 - 15,
SurfaceProgressReport currently allows progress values 101...255 because it's a
bare UInt8; add a validating initializer on SurfaceProgressReport (e.g.,
init(state: SurfaceProgressState, progress: UInt8?)) that checks if progress is
nil or within 0...100 and otherwise rejects/handles the value (use a failable
init returning nil or assert/precondition depending on desired behavior), and
make callers use this initializer so the invariant is enforced; reference
SurfaceProgressReport, its progress property, and SurfaceProgressState when
implementing the validation.

trydis added 2 commits March 15, 2026 20:33
- Skip dispatch for unknown progress states instead of clearing progress
- Clamp progress values to 100 at parse boundary and in model init
- Guard surface existence in setProgressReport before writing state
- Extract tintColor onto SurfaceProgressState to remove duplicate color mapping
- Remove redundant host.update() call from acquireHostView
- Extract progressAutoClearInterval constant for magic number 15
- Store onProgressReport in test mock; add closure-forwarding test
Combines OSC 9;4 progress reporting with in-terminal search overlay.
Both features coexist: SurfaceCallbacks carries both onProgressReport
and onSearchStateChange; GhosttyRuntime handles both PROGRESS_REPORT
and START/END/TOTAL/SELECTED_SEARCH actions; PaneLeafView renders both
the progress bar and search overlay in the terminal ZStack.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift (1)

159-164: Consider using RunLoop.main for timer scheduling to ensure firing during UI tracking.

Timer.scheduledTimer schedules on the current run loop's default mode. If the user is actively scrolling or dragging (tracking mode), the timer won't fire. This is likely acceptable for a 15-second auto-clear, but worth noting.

Optional: Schedule in common modes for guaranteed firing
-            progressClearTimers[surfaceId] = Timer.scheduledTimer(withTimeInterval: Self.progressAutoClearInterval, repeats: false) { [weak self] _ in
+            let timer = Timer(timeInterval: Self.progressAutoClearInterval, repeats: false) { [weak self] _ in
                 Task { `@MainActor` [weak self] in
                     self?.progressBySurfaceId.removeValue(forKey: surfaceId)
                     self?.progressClearTimers.removeValue(forKey: surfaceId)
                 }
             }
+            RunLoop.main.add(timer, forMode: .common)
+            progressClearTimers[surfaceId] = timer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift
around lines 159 - 164, The current Timer created in the progressClearTimers
assignment uses Timer.scheduledTimer which attaches to the current run loop mode
and may not fire during UI tracking; change to create a Timer with the same time
interval and closure but explicitly add it to RunLoop.main in common modes so it
fires during scrolling/dragging (use the same Self.progressAutoClearInterval,
the same closure that removes entries from progressBySurfaceId and
progressClearTimers, and store the resulting Timer back into
progressClearTimers[surfaceId]).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift:
- Around line 159-164: The current Timer created in the progressClearTimers
assignment uses Timer.scheduledTimer which attaches to the current run loop mode
and may not fire during UI tracking; change to create a Timer with the same time
interval and closure but explicitly add it to RunLoop.main in common modes so it
fires during scrolling/dragging (use the same Self.progressAutoClearInterval,
the same closure that removes entries from progressBySurfaceId and
progressClearTimers, and store the resulting Timer back into
progressClearTimers[surfaceId]).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 316d6fdd-bb10-4073-9300-90574c906234

📥 Commits

Reviewing files that changed from the base of the PR and between c4e4066 and 0078952.

📒 Files selected for processing (5)
  • Shellraiser.xcodeproj/project.pbxproj
  • Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift

Use .common run loop mode so the timer fires during scroll/drag events,
not just in .default mode which pauses when the run loop switches to
.tracking mode.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+SurfaceOperations.swift:
- Around line 173-178: focusedSurfaceProgress currently returns nil when
workspace.focusedSurfaceId is unset; change it to mirror other flows by falling
back to workspace.rootPane.firstActiveSurfaceId() before looking up
progressBySurfaceId. In other words, inside focusedSurfaceProgress use
workspace.focusedSurfaceId ?? workspace.rootPane.firstActiveSurfaceId() (or
equivalent) to compute the surface id, then return
progressBySurfaceId[surfaceId]; reference function focusedSurfaceProgress,
property workspace.focusedSurfaceId, method
workspace.rootPane.firstActiveSurfaceId(), and dictionary progressBySurfaceId
when making the change.
- Around line 159-166: The timer callback can race with a newer timer and clear
a freshly set report; when creating the Timer in setProgressReport, capture the
created Timer instance in the closure and, inside the `@MainActor` Task, compare
that captured timer to the current progressClearTimers[surfaceId] before
removing entries from progressBySurfaceId and progressClearTimers; only clear
state if they are identical to avoid deleting a newer report/timer (refer to
Timer, Self.progressAutoClearInterval, progressBySurfaceId, progressClearTimers
and the setProgressReport path).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7b5ce9cf-3b63-4524-afee-9a934886531f

📥 Commits

Reviewing files that changed from the base of the PR and between 0078952 and f891015.

📒 Files selected for processing (1)
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift

- focusedSurfaceProgress now falls back to firstActiveSurfaceId()
  when focusedSurfaceId is nil, matching the pattern used by other
  surface lookup functions
- Replace timer identity check (unsound due to Timer not being
  Sendable and forward-reference restrictions) with a generation
  counter; stale timer callbacks are now ignored if a newer timer
  has been registered for the same surface
@trydis trydis merged commit 9d13df9 into main Mar 15, 2026
2 checks passed
@trydis trydis deleted the progress-reporting branch March 15, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant