sidebar: per-agent status rows (one row per agent pane) - #7559
lawrencecchen wants to merge 53 commits into
Conversation
Several agents in one workspace previously collapsed into a single last-write-wins status pill per agent type (statusEntries is workspace-scoped, keyed by agent type). Store a panel-scoped copy of every structured agent status report and render one sidebar row per live agent pane: panel-scoped report wins, the workspace-level entry is only trusted when a single pane owns the key, otherwise the per-panel lifecycle drives the row. Rows follow pane moves via the existing detached-runtime transfer and clear on pane close; clicking a row focuses that pane.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds per-panel structured agent status storage and sidebar projection, plus new sidebar views, snapshot wiring, lifecycle updates, localization entries, tests, and Xcode project references. ChangesPer-Panel Sidebar Agent Status
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (22 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commented
Jul 8, 2026
|
@codex review |
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17a5e5255a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // slot is last-write-wins per key, so "no change" there can still | ||
| // be a change for this panel's own row. | ||
| if let panelId = panelID { | ||
| tab.recordPanelStatusEntry(entry, panelId: panelId) |
There was a problem hiding this comment.
Skip unchanged panel-scoped status writes
When the same agent pane re-emits an identical status (for example, hooks repeatedly send set_status codex Running --panel=<same>), this unconditional write stores a fresh SidebarStatusEntry(timestamp: Date()) before the duplicate check below runs. Since statusEntriesByPanelId equality includes that timestamp, the setter notifies and the sidebar rows can re-render/re-sort even though nothing visible changed, defeating the existing duplicate suppression; compare against the current panel entry before recording it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 9c95eb5: recordPanelStatusEntry now reuses TerminalController.shouldReplaceStatusEntry, so identical repeats (timestamp-only changes) are dropped before any observation write. Regression test: testIdenticalPanelStatusReportDoesNotReplaceStoredEntry.
— Claude Code
Greptile SummaryThis PR adds per-pane agent status rows to the sidebar. The main changes are:
Confidence Score: 4/5This is close, but the pane-move lifecycle gap should be fixed before merging.
Sources/Workspace+DetachedSurfaceTransfer.swift; Sources/Workspace+PanelLifecycle.swift Important Files Changed
Reviews (39): Last reviewed commit: "Fix covariant Self in allFlags stored-pr..." | Re-trigger Greptile |
| for (panelId, entries) in statusEntriesByPanelId where panels[panelId] != nil { | ||
| for statusKey in entries.keys where Self.structuredAgentHookStatusKeys.contains(statusKey) { | ||
| statusKeysByPanel[panelId, default: []].insert(statusKey) | ||
| } |
There was a problem hiding this comment.
When set_status includes a panel but omits --pid, the upsert path still records a panel-scoped entry. This loop then treats that entry as enough to create an agent row, so a pane can show a live-agent status row with no recorded PID or lifecycle owner to clear it when the process exits.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
Fixed in 9c95eb5: sidebarAgentStatusRows() now seeds rows exclusively from recorded agent PID ownership (agentPIDPanelIdsByKey), so a panel-scoped entry without a PID owner can no longer create a row. Regression test: testPanelEntryWithoutAgentPIDDoesNotCreateRow.
— Claude Code
There was a problem hiding this comment.
Correction to my earlier reply: PID-only seeding was reverted in ba8ab51 because real Claude hooks share one bare PID key across panes (ownership migrates to the last reporter), which collapsed the rows this PR exists to split. Rows now come from the union of PID ownership and live panels holding a panel-scoped structured entry (statusEntriesByPanelId is only populated for structured agent hook status keys, and entries are dropped on clear_status, clearAgentPID(clearStatus: true), and pane close, so unowned rows stay bounded to a live pane's own report). Regression test: testBareSharedPIDKeyHookSequenceKeepsBothPanesRows.
— Claude Code
| nonisolated func controlSidebarScheduleStatusClear(target: ControlSidebarTabTarget, key: String) { | ||
| controlSidebarScheduleMutation(target: target) { _, tab in | ||
| _ = tab.statusEntries.removeValue(forKey: key) | ||
| _ = tab.clearPanelStatusEntries(statusKey: key) |
There was a problem hiding this comment.
clear_status <key> has only a workspace/tab target, but panel-scoped rows are now per pane. If one same-type agent clears claude_code, this call removes that key from every panel in the workspace, so other running panes lose their panel-specific row text even though their agents are still active.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
clear_status is workspace/tab-scoped by protocol: ControlSidebarContext.controlSidebarScheduleStatusClear (CmuxControlSocket) carries no panel id. Pre-PR the same command removed the one shared pill for every pane, so clearing the per-pane copies mirrors the existing workspace-scoped semantics, and rows themselves survive via PID ownership + lifecycle. A pane-scoped clear needs a protocol + CLI verb extension, which is out of scope for this PR; flagged as a follow-up.
— Claude Code
There was a problem hiding this comment.
Intended contract: clear_status is the workspace-scoped user command and by contract removes the key from the whole workspace, including every pane's panel-scoped copy (see the doc comment on controlSidebarScheduleStatusClear). Agent hooks never send it; panel-scoped cleanup routes through clear_agent_pid --panel= --clear-status, which cannot touch sibling panes' rows.
— Claude Code
| guard let runtimeState else { return } | ||
| for (statusKey, statusEntry) in runtimeState.statusEntries { | ||
| statusEntries[statusKey] = statusEntry | ||
| recordPanelStatusEntry(statusEntry, panelId: runtimeState.panelId) |
There was a problem hiding this comment.
Detached runtime adoption stores the panel-scoped status under runtimeState.panelId. When a move creates or uses a different panel UUID in the destination workspace, later lookups use the destination panel ID and miss this entry, so the moved agent can fall back to ambiguous workspace text or lose its per-pane status.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
The entry is stored under the same runtimeState.panelId that agentPIDs/agentPIDKeys ownership adoption already uses (pre-existing contract, unchanged by this PR): panel UUIDs are preserved across transfer. If a destination re-keyed panels, the runtime PIDs would already be orphaned; the panel-scoped entry follows exactly the same identity rule, so no new failure mode is introduced.
— Claude Code
There was a problem hiding this comment.
That's a fair correction — since agentPIDs/agentPIDKeys adoption already relies on runtimeState.panelId being stable across transfer, the panel-scoped status entry is following the same established contract. Any UUID mismatch would have already orphaned PID tracking before this PR. Withdrawing the concern.
There was a problem hiding this comment.
Pane moves carry the panel UUID with the transferred surface, so the adoption keyed by runtimeState.panelId re-binds correctly; if a future path ever re-keys panels mid-move, the entry is dropped at source pane close, so it stays bounded rather than leaking.
— Claude Code
Review fixes: - recordPanelStatusEntry reuses TerminalController.shouldReplaceStatusEntry so identical agent heartbeats (timestamp-only changes) no longer invalidate the sidebar snapshot on every report. - Agent status rows are keyed off recorded agent PID ownership only, so a bare panel-scoped text report cannot create an unowned row or keep a stale one alive after clear_agent_pid without --clear-status. - agentRuntimeState only attributes the workspace-level last-write-wins entry to a panel when no other pane can own the status key, so a transferred pane no longer adopts a sibling agent's status text. - Shared-key rows no longer inherit the last writer's icon/color decorations. - URL-backed agent rows mirror SidebarMetadataEntryRow: underlined button that focuses the pane and opens the URL. workflow-guard-tests (Swift file length budget) fix by splitting: - Sources/Workspace+SidebarAgentStatus.swift: SidebarAgentStatusRow, panel status storage, visibility filtering, and the per-agent row projection. - Sources/SidebarStatusRowViews.swift: SidebarMetadataRows and SidebarAgentStatusRows view families moved out of ContentView.swift. Tests: identical-heartbeat dedupe, ambiguous transfer attribution, and unowned/stale row gating added to PerAgentSidebarStatusRowTests.
| _ = tab.clearPanelStatusEntries(statusKey: key) | ||
| tab.clearAgentPID(key: key) |
There was a problem hiding this comment.
Shared Clear Scope
clear_status is still scoped only to the workspace or tab, but this path now deletes every panel-scoped entry for the key and then clears PID ownership without a panel filter. When two live panes both own claude_code, a clear from one pane removes the other pane's statusEntriesByPanelId value and can drop the ownership mapping that sidebarAgentStatusRows() uses to build that row. The other running pane then loses its agent status row/text even though its agent is still active. The clear needs to use the emitting panel when that context exists, or avoid deleting other panels' panel-scoped row state.
There was a problem hiding this comment.
Rejecting with emitter evidence: no agent lifecycle path sends bare clear_status. Every built-in agent cleanup goes through clear_agent_pid --panel= --clear-status (CLI/cmux.swift:24719, 24782, 30518), which routes to the panel-scoped clearAgentPID: it refuses when another panel owns the key and clears only that panel's entry. Bare clear_status is the workspace-scoped user command; removing the key from every pane is its documented contract (leaving sibling panel copies would make the user command appear to fail). Documented inline at the clear path in b76ad3d.
— Claude Code
There was a problem hiding this comment.
That's a convincing distinction — the two clear paths have clearly different contracts and the routing keeps them isolated. Withdrawing the concern; the documented workspace-scoped semantics for bare clear_status make the all-panel clear correct by design.
… sessions A panel can hold several PID keys for one base status key (two sessions of the same agent type); clearAgentPID(clearStatus: true) now drops the panel- scoped entry only when no remaining key on that panel maps to the status key, so a stale session's cleanup cannot erase a live session's row. Mirrors the existing hasAgentRuntime gate that protects the workspace-level entry. Test: testStaleSessionCleanupKeepsLiveSameKeySessionEntry.
…same-key sessions" This reverts commit 6e19951.
…write recordAgentPID evicts stale structured runtimes on the panel (agent replacement in the same pane), and that cleanup clears their panel-scoped status entries. The upsert previously wrote the new panel-scoped entry first, so the eviction deleted the just-recorded row text. The upsert now updates the workspace slot (guarded), records PID ownership, then writes the panel-scoped copy last. Test: testAgentReplacementOnSamePanelKeepsFreshPanelEntry.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift`:
- Line 37: Add test coverage for `agentStatusRows` retention in
`SidebarWorkspaceSnapshotRefreshPolicyTests`; the current helper always uses an
empty array, so it never verifies `applyingContextMenuImmediateFields` preserves
non-empty `agentStatusRows` from `self`. Update the test setup alongside
`contextMenuPinChangeUpdatesDisplayedFieldsAndDefersNoisyFields` to seed
`agentStatusRows` with data and assert the result keeps `self.agentStatusRows`
unchanged, similar to the existing `metadataEntries` and `listeningPorts`
checks.
In `@Sources/SidebarStatusRowViews.swift`:
- Around line 98-112: Validate the agent-provided URL before calling
NSWorkspace.shared.open in the row action handling, since row.url can point to
unsafe schemes. Update the Button action in SidebarStatusRowViews (and the other
open-url call site noted in the review) to allow only approved schemes such as
http and https, optionally mailto, and fall back to focusing the panel or
showing no open action when the URL is absent or disallowed. Keep the validation
close to the existing row.url / entry.url handling so the unsafe open path is
prevented wherever the URL is used.
- Around line 216-236: The icon parsing in iconView is duplicated across
SidebarAgentStatusEntryRow and SidebarMetadataEntryRow, so extract the shared
emoji:/text:/sf: handling into a common helper to keep behavior consistent. Add
a reusable renderer or parsing helper near these row views and have both
iconView properties delegate to it, preserving the same fallback to
CmuxSystemSymbolImage and the same empty-value guards.
- Around line 216-236: Avoid AnyView type erasure in iconView; it is hurting
SwiftUI identity and row diffing. Refactor the SidebarStatusRowViews.iconView
property to use `@ViewBuilder` and return some View instead of AnyView, preserving
the existing emoji/text/sf symbol branches with conditional view composition.
Update the caller in the row view to consume the builder output directly (no
optional AnyView unwrapping), so empty cases render as EmptyView while keeping
efficient SwiftUI updates.
- Around line 140-149: The displayText property in SidebarStatusRowViews
currently builds the name-and-lifecycle string with plain interpolation, so
replace the "\(name): \(lifecycleText)" path with a localized template that lets
translators control ordering and punctuation. Update displayText to use the new
localization key sidebar.agentStatus.displayNameWithLifecycle when lifecycleText
is present, and add that entry to Localizable.xcstrings with per-locale
translations so the formatting can adapt correctly.
In `@Sources/Workspace`+PanelLifecycle.swift:
- Line 410: Clear the workspace-level status when a panel-only status owner
closes. The cleanup in `discardAgentRuntimeState`/panel teardown currently
removes `statusEntriesByPanelId` but leaves stale `statusEntries` when the
structured status had no tracked PID, so update the panel-close path around
`statusEntriesByPanelId.removeValue(forKey:)` to also clear the corresponding
workspace entry in `tab.statusEntries` (or route it through a shared cleanup
helper used by `controlSidebarScheduleStatusUpsert` and
`discardAgentRuntimeState`) whenever the panel-scoped owner is the sole source
of that status.
In `@Sources/Workspace`+SidebarAgentStatus.swift:
- Around line 187-198: The SidebarAgentStatusRow construction in
Workspace+SidebarAgentStatus is still inheriting workspaceEntry priority and
timestamp when soleOwner is false, which lets ambiguous multi-pane rows use
workspace-level freshness for ordering. Update the priority and timestamp
fallbacks in the row builder so they only use workspaceEntry values when
soleOwner is true, otherwise rely on the row’s own entry or neutral defaults.
Keep the existing entry/soleOwner logic around value, icon, and color consistent
with this change.
- Around line 92-139: The structured status filtering in
Workspace+SidebarAgentStatus is still allowing keys that are already represented
by agent rows to remain in the flat metadata list. Update
sidebarStatusEntriesVisibleForDisplay and, if needed,
visibleStructuredAgentStatusKeysByPanel so that structured keys mapped to a
panel in agentPIDPanelIdsByKey are excluded from the legacy workspace-level
pills, while unscoped/legacy keys without a panel mapping continue to pass
through. Keep the per-panel winner selection in place, but return only the keys
that should not be rendered as agent rows.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4b05b515-60e9-4ef9-928f-334ac31b3b58
📒 Files selected for processing (11)
Resources/Localizable.xcstringsSources/ContentView.swiftSources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swiftSources/SidebarStatusRowViews.swiftSources/TerminalController+ControlSidebarContext.swiftSources/Workspace+PanelLifecycle.swiftSources/Workspace+SidebarAgentStatus.swiftSources/WorkspaceSidebarAgentRuntimeObservationModel.swiftcmux.xcodeproj/project.pbxprojcmuxTests/PerAgentSidebarStatusRowTests.swiftcmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
| if let url = row.url { | ||
| Button { | ||
| onFocusPanel(row.panelId) | ||
| NSWorkspace.shared.open(url) | ||
| } label: { | ||
| rowContent(underlined: true) | ||
| } | ||
| .buttonStyle(.plain) | ||
| .safeHelp(url.absoluteString) | ||
| } else { | ||
| rowContent(underlined: false) | ||
| .contentShape(Rectangle()) | ||
| .onTapGesture { onFocusPanel(row.panelId) } | ||
| .safeHelp(helpText) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate URL scheme before NSWorkspace.shared.open.
Agent-provided URLs (from row.url / entry.url) are opened directly via NSWorkspace.shared.open without scheme validation. A malicious or buggy agent could set file://, ftp://, or other schemes that open arbitrary files or applications. Consider allowlisting http/https (and optionally mailto) before opening.
🛡️ Proposed fix for both call sites
// In SidebarAgentStatusEntryRow.body (line ~98)
if let url = row.url {
Button {
onFocusPanel(row.panelId)
- NSWorkspace.shared.open(url)
+ if let scheme = url.scheme?.lowercased(),
["http", "https", "mailto"].contains(scheme) {
+ NSWorkspace.shared.open(url)
+ }
} label: { // In SidebarMetadataEntryRow.body (line ~248)
if let url = entry.url {
Button {
onFocus()
- NSWorkspace.shared.open(url)
+ if let scheme = url.scheme?.lowercased(),
+ ["http", "https", "mailto"].contains(scheme) {
+ NSWorkspace.shared.open(url)
+ }
} label: {Also applies to: 247-262
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/SidebarStatusRowViews.swift` around lines 98 - 112, Validate the
agent-provided URL before calling NSWorkspace.shared.open in the row action
handling, since row.url can point to unsafe schemes. Update the Button action in
SidebarStatusRowViews (and the other open-url call site noted in the review) to
allow only approved schemes such as http and https, optionally mailto, and fall
back to focusing the panel or showing no open action when the URL is absent or
disallowed. Keep the validation close to the existing row.url / entry.url
handling so the unsafe open path is prevented wherever the URL is used.
| private var iconView: AnyView? { | ||
| guard let iconRaw = effectiveIcon else { return nil } | ||
| if iconRaw.hasPrefix("emoji:") { | ||
| let value = String(iconRaw.dropFirst("emoji:".count)) | ||
| guard !value.isEmpty else { return nil } | ||
| return AnyView(Text(value).cmuxFont(size: 9 * fontScale)) | ||
| } | ||
| if iconRaw.hasPrefix("text:") { | ||
| let value = String(iconRaw.dropFirst("text:".count)) | ||
| guard !value.isEmpty else { return nil } | ||
| return AnyView(Text(value).cmuxFont(size: 8 * fontScale, weight: .semibold)) | ||
| } | ||
| let symbolName: String | ||
| if iconRaw.hasPrefix("sf:") { | ||
| symbolName = String(iconRaw.dropFirst("sf:".count)) | ||
| } else { | ||
| symbolName = iconRaw | ||
| } | ||
| guard !symbolName.isEmpty else { return nil } | ||
| return AnyView(CmuxSystemSymbolImage(magnified: symbolName, pointSize: 8 * fontScale, weight: .medium)) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Extract shared icon-parsing logic to eliminate duplication.
The iconView property is nearly identical in SidebarAgentStatusEntryRow (lines 216–236) and SidebarMetadataEntryRow (lines 293–316) — same emoji:/text:/sf: prefix parsing, same CmuxSystemSymbolImage fallback. Extract a shared helper to keep them from diverging.
♻️ Proposed shared helper
/// Shared icon rendering for sidebar rows.
`@ViewBuilder`
enum SidebarRowIcon {
static func make(iconRaw: String, fontScale: CGFloat) -> some View {
if iconRaw.hasPrefix("emoji:") {
let value = String(iconRaw.dropFirst("emoji:".count))
if !value.isEmpty {
Text(value).cmuxFont(size: 9 * fontScale)
}
} else if iconRaw.hasPrefix("text:") {
let value = String(iconRaw.dropFirst("text:".count))
if !value.isEmpty {
Text(value).cmuxFont(size: 8 * fontScale, weight: .semibold)
}
} else {
let symbolName = iconRaw.hasPrefix("sf:")
? String(iconRaw.dropFirst("sf:".count))
: iconRaw
if !symbolName.isEmpty {
CmuxSystemSymbolImage(magnified: symbolName, pointSize: 8 * fontScale, weight: .medium)
}
}
}
}Then each iconView becomes:
`@ViewBuilder`
private var iconView: some View {
if let iconRaw = effectiveIcon?.trimmingCharacters(in: .whitespacesAndNewlines),
!iconRaw.isEmpty {
SidebarRowIcon.make(iconRaw: iconRaw, fontScale: fontScale)
}
}Also applies to: 293-316
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/SidebarStatusRowViews.swift` around lines 216 - 236, The icon parsing
in iconView is duplicated across SidebarAgentStatusEntryRow and
SidebarMetadataEntryRow, so extract the shared emoji:/text:/sf: handling into a
common helper to keep behavior consistent. Add a reusable renderer or parsing
helper near these row views and have both iconView properties delegate to it,
preserving the same fallback to CmuxSystemSymbolImage and the same empty-value
guards.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Avoid AnyView type erasure in sidebar row views.
AnyView breaks SwiftUI's view identity and prevents diffing optimizations. In a ForEach inside a LazyVStack sidebar, this causes unnecessary re-renders on every row invalidation. Use @ViewBuilder to return some View with _ConditionalContent instead.
⚡ Proposed refactor using `@ViewBuilder`
- private var iconView: AnyView? {
- guard let iconRaw = effectiveIcon else { return nil }
- if iconRaw.hasPrefix("emoji:") {
- let value = String(iconRaw.dropFirst("emoji:".count))
- guard !value.isEmpty else { return nil }
- return AnyView(Text(value).cmuxFont(size: 9 * fontScale))
- }
- if iconRaw.hasPrefix("text:") {
- let value = String(iconRaw.dropFirst("text:".count))
- guard !value.isEmpty else { return nil }
- return AnyView(Text(value).cmuxFont(size: 8 * fontScale, weight: .semibold))
- }
- let symbolName: String
- if iconRaw.hasPrefix("sf:") {
- symbolName = String(iconRaw.dropFirst("sf:".count))
- } else {
- symbolName = iconRaw
- }
- guard !symbolName.isEmpty else { return nil }
- return AnyView(CmuxSystemSymbolImage(magnified: symbolName, pointSize: 8 * fontScale, weight: .medium))
- }
+ `@ViewBuilder`
+ private var iconView: some View {
+ if let iconRaw = effectiveIcon {
+ SidebarRowIcon.make(iconRaw: iconRaw, fontScale: fontScale)
+ }
+ }The caller at line 119 changes from if let icon = iconView { to:
- if let icon = iconView {
- icon
- .foregroundColor(foregroundColor.opacity(0.95))
- }
+ iconView
+ .foregroundColor(foregroundColor.opacity(0.95))@ViewBuilder renders EmptyView when the if let is nil, so no optional unwrapping is needed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/SidebarStatusRowViews.swift` around lines 216 - 236, Avoid AnyView
type erasure in iconView; it is hurting SwiftUI identity and row diffing.
Refactor the SidebarStatusRowViews.iconView property to use `@ViewBuilder` and
return some View instead of AnyView, preserving the existing emoji/text/sf
symbol branches with conditional view composition. Update the caller in the row
view to consume the builder output directly (no optional AnyView unwrapping), so
empty cases render as EmptyView while keeping efficient SwiftUI updates.
…tries Claude Code hooks share ONE bare PID key per agent type across all panes of a workspace (set_agent_pid claude_code <pid>), so bare-key ownership migrates to the last reporting pane. Seeding rows from PID ownership alone made every other pane lose its row, reintroducing the exact collapse this PR fixes (reproduced live with two panes running the real hook sequence). sidebarAgentStatusRows() now derives candidate (panel, statusKey) pairs from the union of recorded PID ownership and live panels holding a panel-scoped structured entry. Stale entries stay bounded: clear_status, clearAgentPID(clearStatus: true), and pane close all drop panel-scoped entries. Regression test: testBareSharedPIDKeyHookSequenceKeepsBothPanesRows simulates the bare-key two-pane hook sequence end-to-end at the Workspace level.
| for (panelId, entries) in statusEntriesByPanelId where panels[panelId] != nil { | ||
| for statusKey in entries.keys where Self.structuredAgentHookStatusKeys.contains(statusKey) { | ||
| statusKeysByPanel[panelId, default: []].insert(statusKey) | ||
| } | ||
| } |
There was a problem hiding this comment.
This still lets set_status with a panel but without --pid create an agent row. The upsert path can store a panel-scoped entry without recording PID ownership, and this loop treats that entry as enough to seed a row while the pane remains open. If that agent exits without sending clear_status, the stale PID sweep cannot clean the row because there is no PID key to sweep, so the sidebar can keep showing a live-looking agent status for a dead process.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
This is the tradeoff consciously taken in ba8ab51: strict PID-only seeding broke the primary feature scenario (real Claude hooks share ONE bare claude_code PID key across panes, ownership migrates to the last reporter), so rows seed from the union of PID ownership and live panel entries. A set_status --panel without --pid can keep a row until the pane closes or a clear arrives; that residue is bounded to the open pane, and as of b76ad3d pane close also sweeps the workspace-level slot when the closed pane was the key's last owner.
— Claude Code
There was a problem hiding this comment.
Understood — the bounded residue (open pane only, swept on close) is an acceptable tradeoff given that strict PID-only seeding breaks the primary Claude hooks use case where a single bare PID key migrates across panes. The docblock on sidebarAgentStatusRows() already captures this reasoning, so the intent is clear. I'll withdraw the concern.
There was a problem hiding this comment.
Deliberate design, documented on sidebarAgentStatusRows(): panel-scoped entries keep a pane's row alive independently of pid ownership because bare shared keys migrate ownership to the last reporter. Staleness is bounded: clear_status, clearAgentPID(clearStatus: true), agent replacement on the pane (as of 2cde0e4), and pane close all drop panel-scoped entries. An agent that reports set_status --panel without --pid and exits without clear_status leaves its final status visible on purpose (same as workspace-level set_status always has); pid-based liveness requires the pid the caller chose not to provide.
— Claude Code
The app-host unit test lane builds the app as "cmux DEV" (module cmux_DEV),
so a bare `@testable import cmux` fails module scanning there ("Unable to
find module dependency: 'cmux'", all four shards red). Use the same
canImport(cmux_DEV) conditional import every other wired cmuxTests file
uses.
| for (panelId, entries) in statusEntriesByPanelId where panels[panelId] != nil { | ||
| for statusKey in entries.keys where Self.structuredAgentHookStatusKeys.contains(statusKey) { | ||
| statusKeysByPanel[panelId, default: []].insert(statusKey) | ||
| } |
There was a problem hiding this comment.
This loop lets a panel-scoped structured status create an agent row even when no PID owner was recorded. A caller can send set_status with a panel and no --pid; the upsert path records statusEntriesByPanelId, and this branch then renders it as a live agent row. Since no PID owner exists, PID-exit cleanup cannot remove that row, so the pane can keep showing stale agent status until the pane closes or an explicit clear arrives.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
Settled in ba8ab51 (see the sibling thread): PID-only seeding was tried in 9c95eb5 and reverted because shared bare PID keys migrate ownership to the last reporter, which erased still-running sibling panes' rows. The panel-entry seed is required for correctness of the primary scenario; the no-PID residue is bounded to the pane's lifetime (pane close, clear_status, and clear_agent_pid --clear-status all drop it, and b76ad3d also sweeps the workspace slot on close).
— Claude Code
There was a problem hiding this comment.
See the reply on the sibling thread: intended, documented, and bounded (clear paths + replacement cleanup + pane close). Liveness sweeping needs a pid; a caller omitting --pid opts into explicit-clear semantics.
— Claude Code
…ing) - Split SidebarStatusRowViews.swift into SidebarMetadataRowViews.swift and SidebarAgentStatusRowViews.swift so each new file owns one major view type (plus its private entry row). - Convert PerAgentSidebarStatusRowTests to Swift Testing (@Test/#expect/ #require, @mainactor struct suite), matching the repo policy for new non-UI tests. Suite remains discovered by the app-host shard planner (cmux_unit_test_shard.py --validate passes, suite listed with weight 20).
| for (panelId, entries) in statusEntriesByPanelId where panels[panelId] != nil { | ||
| for statusKey in entries.keys where Self.structuredAgentHookStatusKeys.contains(statusKey) { | ||
| statusKeysByPanel[panelId, default: []].insert(statusKey) | ||
| } |
There was a problem hiding this comment.
A structured set_status can still create a live-looking agent row without an owning runtime. The upsert path records a panel-scoped entry whenever a panel id is present, even when no --pid was supplied, and this loop then uses that entry as a row seed. If the reporting process exits without sending clear_status, the stale PID sweep cannot clean the row because no PID key was recorded, so the sidebar can keep showing the old agent status until the pane closes.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
Same settled decision as the two sibling threads (ba8ab51): the union seed is deliberate because bare shared PID keys migrate ownership; without the panel-entry seed, live sibling agents lose their rows, which is worse than a bounded stale row in a still-open pane whose agent exited without clear_status.
— Claude Code
There was a problem hiding this comment.
Same as the sibling threads: intended explicit-clear semantics for no-pid reports, staleness bounded by clear paths, same-pane agent replacement (2cde0e4), and pane close.
— Claude Code
| nonisolated func controlSidebarScheduleStatusClear(target: ControlSidebarTabTarget, key: String) { | ||
| controlSidebarScheduleMutation(target: target) { _, tab in | ||
| _ = tab.statusEntries.removeValue(forKey: key) | ||
| _ = tab.clearPanelStatusEntries(statusKey: key) |
There was a problem hiding this comment.
clear_status still removes the panel-scoped copy for every pane that shares this structured key. With two live claude_code panes, one pane's clear_status claude_code removes all statusEntriesByPanelId[*][claude_code]; for the bare-key hook sequence, only the last reporter has PID ownership, so another pane can lose its row entirely. With qualified PID keys, the rows remain but their per-pane text, icon, and color are erased. The clear path needs a panel-scoped target when available, or it should avoid deleting other panes' panel status state.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
Same resolution as the earlier thread on this line: agent exits never send bare clear_status; hooks use clear_agent_pid --panel --clear-status (CLI/cmux.swift:24719, 24782, 30518), which is panel-scoped and cannot touch sibling panes. Bare clear_status is the workspace-wide user command by contract. Invariant now documented inline in b76ad3d.
— Claude Code
There was a problem hiding this comment.
Same as the sibling thread: workspace-scoped clear_status intentionally clears every pane's copy by contract; hooks use the panel-scoped clear_agent_pid --panel path, which never touches sibling panes. The 2cde0e4 round strengthened this: displaced synthesized runtimes and lifecycles are now reaped too, so the whole-workspace clear is complete instead of partial.
— Claude Code
left a comment
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/SidebarAgentStatusRowViews.swift`:
- Around line 93-96: The URL-backed tooltip path in SidebarAgentStatusRowView is
omitting the pane label that helpText includes for non-URL rows, so update the
tooltip logic to preserve that same disambiguating context. In the view’s URL
branch, build the tooltip from the URL string plus the row’s paneLabel when
available, using the same helpText-style formatting already used by the helpText
computed property.
- Around line 158-178: The icon parsing in SidebarAgentStatusEntryRow.iconView
duplicates the same emoji:/text:/sf: handling already used in
SidebarMetadataEntryRow.iconView, so extract that shared logic into a common
helper or small parser type and have both row views call it. Keep the existing
prefix behavior, empty-string guards, and symbol/font sizing in the shared
implementation so the two views stay consistent and do not drift.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 25e5870e-2936-48c4-bb3a-dbbd75385f2c
📒 Files selected for processing (4)
Sources/SidebarAgentStatusRowViews.swiftSources/SidebarMetadataRowViews.swiftcmux.xcodeproj/project.pbxprojcmuxTests/PerAgentSidebarStatusRowTests.swift
…values Structured statuses moved out of the generic metadata renderer lost their format: a set_status value with --format=markdown rendered as raw markdown syntax in the per-agent row. SidebarAgentStatusRow now carries the reported entry's format and the row view renders markdown values through the same AttributedString(markdown:) inline path SidebarMetadataEntryRow uses; lifecycle/name fallback text stays plain. Test: testRowCarriesReportedValueFormat.
With more than one agent pane, the agent-rows block gets a disclosure header showing an aggregate summary (agent count, needs-input count, worst-state accent dot when collapsed). Clicking the header folds or unfolds the per-agent rows. Single-agent workspaces render the row directly with no accordion chrome.
commented
Jul 8, 2026
|
@codex review |
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcf43ddba3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| agentPIDProcessIdentities: agentPIDIdentitiesForPanel, | ||
| agentPIDKeys: pidKeys | ||
| agentPIDKeys: pidKeys, | ||
| dynamicAgentRowKeys: dynamicAgentRowKeys.intersection(statusEntriesForPanel.keys) |
There was a problem hiding this comment.
Keep dynamic row keys when transferring lifecycle-only agents
When a registered third-party agent is tracked with set_agent_pid <key> --panel plus set_agent_lifecycle <key> ... --panel but has not emitted a panel-scoped set_status, it can render in the source workspace from PID + lifecycle state. This line only carries dynamic row eligibility for keys that also have a status entry, so after a detach/move adoptDetachedAgentRuntimeState records the PID without re-registering the dynamic key and the destination treats it as an unregistered metadata key, dropping the row.
Useful? React with 👍 / 👎.
| /// instead of degrading to the ambiguous workspace-level slot. | ||
| func panelScopedAgentStatusSnapshots(panelId: UUID) -> [SessionStatusEntrySnapshot]? { | ||
| let entries = (statusEntriesByPanelId[panelId] ?? [:]).values | ||
| .filter { Self.structuredAgentHookStatusKeys.contains($0.key) } |
There was a problem hiding this comment.
Persist registered dynamic agent row snapshots
For a registered dynamic agent row created by set_status <custom-key> ... --panel, the live entry is stored in statusEntriesByPanelId and included in the autosave fingerprint, but this filter writes only built-in structured keys to the session snapshot. After relaunch those custom rows are missing entirely (and their lifecycle is similarly filtered below), so pane-scoped third-party agent rows do not survive session restore even though built-in rows do.
Useful? React with 👍 / 👎.
Key resolutions: - Workspace lifecycle functions moved to main's new Workspace+AgentLifecycle.swift; ported dynamic-agent-row-key pruning into clearAgentLifecycle, clearAgentLifecycleStates, and clearAllAgentLifecycleStates there. - SidebarWorkspaceSnapshotBuilder moved to its own file on main; added agentStatusRows field to the extracted Snapshot. - FeatureFlags: sidebar-agent-rows-enabled-release appended at index 4 after main's cloud-vm and agent-chat flags. - Kept main's extraction shape for sidebar test files; dropped this branch's duplicate SidebarSelectedWorkspaceScrollPolicyTests.swift (identical suite now lives inline in SidebarWorkspaceSnapshotRefreshPolicyTests.swift). - Localizable.xcstrings merged at JSON level (37 branch keys applied onto main's catalog); budget tsv regenerated; pbxproj unioned + normalized.
# Conflicts: # .github/swift-file-length-budget.tsv # Resources/Localizable.xcstrings # Sources/ContentView.swift # Sources/SidebarWorkspaceSnapshotBuilder.swift # cmux.xcodeproj/project.pbxproj
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | ||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | ||
| let agentPIDKeys: Set<String> | ||
| let dynamicAgentRowKeys: Set<String> |
There was a problem hiding this comment.
This transfer state still does not carry the panel lifecycle values. When a pane with an agent row in needsInput or idle is moved, extraction carries the status entry and PID data, then the source cleanup clears agentLifecycleStatesByPanelId[panelId]. The destination adoption restores the row text, but there is no lifecycle value to restore, so the moved row loses its state dot and summary count until another lifecycle hook arrives.
workflow-guard-tests rejects files that enter the tracked budget at >=500 lines. Pure moves, no behavior change: - SidebarAgentStatusRowViews.swift -> graphite row views split into SidebarAgentGraphiteRowViews.swift - PerAgentSidebarStatusRowTests.swift -> clearing/replacement/summary tests split into PerAgentSidebarStatusRowClearingTests.swift - SidebarWorkspaceSnapshotRefreshPolicyTests.swift -> scroll-policy and row-interaction suites split into their own test files (matching the pre-revert extraction shape)
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | ||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | ||
| let agentPIDKeys: Set<String> | ||
| let dynamicAgentRowKeys: Set<String> |
There was a problem hiding this comment.
Lifecycle still drops This transfer state still does not carry the panel lifecycle map. When a pane with an agent row in
needsInput or idle is moved, the source cleanup removes agentLifecycleStatesByPanelId[panelId], while adoption restores the status text and row key without restoring the lifecycle value. The moved row can keep its text but lose the state dot and summary count until another lifecycle hook arrives. Please carry the panel lifecycle entries in the detached runtime state and adopt them with the panel-scoped status entries.
| let lifecycle = rem2[1] | ||
| let allowed = ["running", "idle", "needsInput", "unknown"] | ||
| guard allowed.contains(lifecycle) else { | ||
| throw CLIError(message: "set-agent-lifecycle: invalid lifecycle '\(lifecycle)' (expected running, idle, needsInput, or unknown)") |
There was a problem hiding this comment.
CLI lifecycle token mismatch
Medium Severity
The new cmux set-agent-lifecycle path rejects lifecycle tokens that the control socket already accepts. forwardSetAgentLifecycleCommand checks an exact allowlist (needsInput, etc.) before forwarding, while set_agent_lifecycle normalizes input (case, underscores, and forms like needs-input). Scripts using common spellings get a CLI error even though the same command would succeed on the socket.
Reviewed by Cursor Bugbot for commit c258544. Configure here.
# Conflicts: # Sources/ContentView.swift
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | ||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | ||
| let agentPIDKeys: Set<String> | ||
| let dynamicAgentRowKeys: Set<String> |
There was a problem hiding this comment.
This transfer state still has no field for the moved pane's agent lifecycle values. A pane move can carry the panel-scoped status entry into the destination workspace, but the source cleanup removes agentLifecycleStatesByPanelId[panelId] and adoptDetachedAgentRuntimeState only restores entries, dynamic keys, and PID ownership. When a pane showing needsInput or idle is moved, the row keeps its text but loses its state dot and summary count until another lifecycle hook arrives. Carry the panel lifecycle entries in this runtime state and restore them with the status entries.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
# Conflicts: # .github/swift-file-length-budget.tsv
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | ||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | ||
| let agentPIDKeys: Set<String> | ||
| let dynamicAgentRowKeys: Set<String> |
There was a problem hiding this comment.
Lifecycle Still Drops
The detached runtime payload now carries dynamic row keys, but it still has no field for the pane's lifecycle values. When a pane in needsInput or idle moves to another workspace or through the dock, the transfer can restore the row text, PID state, and dynamic row eligibility, while the source-side close clears agentLifecycleStatesByPanelId[panelId]. The destination then builds the row with a nil lifecycle, so the state dot and summary count disappear until another lifecycle hook arrives. Carry the panel lifecycle map in this transfer state and restore it with the panel-scoped status entries.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Main nested the struct inside Workspace (Workspace+DetachedSurfaceTransfer.swift); the bare reference broke the test-target build on every app-host shard.
# Conflicts: # Resources/Localizable.xcstrings # Sources/FeatureFlags.swift # Sources/Workspace+PanelLifecycle.swift
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | ||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | ||
| let agentPIDKeys: Set<String> | ||
| let dynamicAgentRowKeys: Set<String> |
There was a problem hiding this comment.
The detached runtime payload carries status entries, PIDs, and dynamic row keys, but it still does not carry the pane's lifecycle values. During a pane move, agentRuntimeState(forPanelId:) captures this payload before discardClosedPanelLifecycleState clears agentLifecycleStatesByPanelId[panelId]; the destination then restores the row text in adoptDetachedAgentRuntimeState without restoring whether the row was needsInput or idle. A moved agent pane can keep its status text while losing its state dot and summary count until another hook fires. Please carry the panel lifecycle map with this transfer state and restore it with the panel-scoped status entries.
Merge left the new flag entry referencing Self.sidebarAgentRowsDefault inside the allFlags closure initializer; spell the class name like the sibling entries.
| @@ -14,6 +14,7 @@ extension Workspace { | |||
| /// an unrelated process" (same contract as `isRecordedAgentPIDLive`). | |||
| let agentPIDProcessIdentities: [String: AgentPIDProcessIdentity] | |||
| let agentPIDKeys: Set<String> | |||
There was a problem hiding this comment.
This transfer state still omits the pane's agent lifecycle values. During a pane move, the runtime transfer can restore the row text, PID state, and dynamic row eligibility, but the source cleanup clears agentLifecycleStatesByPanelId[panelId] and the destination never writes it back. A moved row that was needsInput or idle can keep its status text while losing the state dot and summary count until another lifecycle hook arrives. Carry the panel lifecycle map in this payload and restore it with the panel-scoped status entries.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
left a comment
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 32fae4c. Configure here.
| if let panelId = panelID, !tab.panels.keys.contains(panelId) { | ||
| return | ||
| } | ||
| guard Self.shouldReplaceStatusEntry( |
There was a problem hiding this comment.
Stale panel drops workspace status
Medium Severity
When set_status includes a --panel id that is not in the target workspace, the scheduled upsert returns before updating anything, yet the socket path still replies OK. Workspace-level statusEntries are skipped along with the panel-scoped copy, so hooks with a stale or wrong surface id lose the update silently.
Reviewed by Cursor Bugbot for commit 32fae4c. Configure here.
* test: cover agent row surface names * Show surface names in agent rows
Add a regression test proving that claiming an unchanged auto-generated surface name emits the sidebar observation update and renders the combined row label.
Include custom-title provenance in the sidebar observation snapshot so a same-text auto-to-user claim refreshes the visible agent row immediately.


Summary
statusEntriesis workspace-scoped and keyed by agent type (last write wins), so N agents of the same type in one workspace collapsed into a single ambiguous pill. This PR stores a panel-scoped copy of every structured agent status report (statusEntriesByPanelIdon the sidebar agent runtime observation model, written in the same socket upsert path) and renders one row per live agent pane.set_statuskeys and the CLIlist-statusoutput are unchanged.DetachedAgentRuntimeStatetransfer (extract now prefers the panel-scoped entry; adopt seeds it in the destination) and clear on agent PID clear,clear_status, and pane close.Testing
cmuxTests/PerAgentSidebarStatusRowTests.swift(wired into pbxproj, lint-pbxproj-test-wiring ok): two same-type agents keep separate rows; sole owner falls back to the workspace entry; shared key without panel entries uses per-panel lifecycle and never the ambiguous shared text; PID clear removes only that pane's row; detached-runtime transfer carries the panel-scoped entry; pane close drops panel-scoped entries.agrows.Issues
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches sidebar agent attribution, persistence, and control-socket mutation paths where incorrect panel/workspace targeting could show stale or wrong rows; changes are feature-flagged but span CLI and UI behavior.
Overview
Adds one sidebar status row per agent pane instead of collapsing multiple agents of the same type into a single workspace pill. Status is stored and resolved per panel (with safe fallbacks when a shared workspace key is ambiguous), rows can focus their pane, and structured keys shown as rows are hidden from the flat metadata list.
CLI / control socket:
cmux set-statusgains--paneland--pid; new commandsset-agent-lifecycleandclear-agent-pidforward to the socket. Sidebar metadata forwarding resolves workspace/tab and panel IDs more consistently.set_statuswith a panel can markdynamicAgentRowKeywhen the key passes the vault-agent allowlist (not enabled forreport_meta).Also adds localization for agent row lifecycle copy, a
sidebar-agent-rowsfeature flag, debug Agent Rows Style strings, and routine Swift file-length budget updates.Reviewed by Cursor Bugbot for commit 32fae4c. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Show one sidebar status row per agent pane and store status per panel. Previously we collapsed same‑type agents into a workspace pill; now each pane keeps its own row that persists, focuses its pane on click, and safely falls back when a shared key is ambiguous.
clear-agent-pidclears only that pane and refreshes ports; workspaceclear_statusreaps displaced runtimes. Pane close and agent replacement drop stale rows and workspace slots.set-statusadds--panel/--pid; newset-agent-lifecycleandclear-agent-pid; registry‑validated dynamic keys can create rows;--workspaceresolves both--taband panel scope; validation runs off‑main. Brand icons with state dots; spinner for running, idle hides; accordion summary when multiple; independent below‑card click targets; graphite prototype and a debug “Agent Rows Style” lab. Gated behindsidebar-agent-rows-enabled-release(DEBUG on, release off).Written for commit 403717e. Summary will update on new commits.
Summary by CodeRabbit