Repository navigation
feat(sidebar): show claude agents --json sessions in custom sidebars - #6493
samuelpatro wants to merge 3 commits into
Conversation
Add CustomSidebarAgentSnapshot, a lenient ClaudeAgentsSessionParser for 'claude agents --json' output, and a ClaudeAgentsSessionPoller refresh loop. CustomSidebarContextSnapshot carries an agents array and the data context builder emits a top-level 'agents' array plus agentsCount / agentsWorkingCount / agentsBlockedCount and per-agent derived state booleans.
Resolve the user's claude binary via AgentExecutableResolver, run 'claude agents --json --all' off the main thread, and feed the parsed sessions into the custom-sidebar data context. Polling runs only while a custom sidebar is on screen (started/stopped with its appear/disappear).
|
@samuelpatro is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds end-to-end Claude agent session support to the custom sidebar: a new ChangesClaude Agents Sidebar Integration
Sequence DiagramsequenceDiagram
participant VerticalTabsSidebar
participant ClaudeAgentsSessionPoller
participant ClaudeAgentsSessionSource
participant ClaudeProcess as claude agents --json --all
participant ClaudeAgentsSessionParser
VerticalTabsSidebar->>ClaudeAgentsSessionPoller: start() on appear
loop every 3 seconds
ClaudeAgentsSessionPoller->>ClaudeAgentsSessionSource: await fetch()
ClaudeAgentsSessionSource->>ClaudeProcess: launch subprocess, read stdout to EOF
ClaudeProcess-->>ClaudeAgentsSessionSource: JSON bytes
ClaudeAgentsSessionSource->>ClaudeAgentsSessionParser: parse(data)
ClaudeAgentsSessionParser-->>ClaudeAgentsSessionSource: [CustomSidebarAgentSnapshot] or []
ClaudeAgentsSessionSource-->>ClaudeAgentsSessionPoller: snapshots or nil on failure
ClaudeAgentsSessionPoller->>ClaudeAgentsSessionPoller: update sessions if non-nil
end
VerticalTabsSidebar->>VerticalTabsSidebar: read sessions → dataContext(agents, agentsCount, ...)
VerticalTabsSidebar->>ClaudeAgentsSessionPoller: stop() on disappear
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (4 errors, 1 warning)
✅ Passed checks (18 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift`:
- Line 42: The ClaudeAgentsSessionParser is inconsistently validating the `kind`
field compared to the `cwd` field. Add a check for empty `kind` values to the
guard condition on line 38 (where `!cwd.isEmpty` is already being checked) by
adding `!kind.isEmpty` to filter out entries with empty kind values, ensuring
consistent validation behavior between the two fields as documented in the
header comment that states entries with missing required fields are dropped.
In
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swift`:
- Around line 55-59: The race condition occurs because the code updates
self?.sessions with the fetched result before checking if the task has been
cancelled. When stop() cancels the task while fetch() is in flight, the result
is still assigned after the await completes, potentially overwriting the current
cache with stale data after a restart. Move the Task.isCancelled check to
immediately after the await fetch() returns and guard the session assignment so
it only happens if the task has not been cancelled. This ensures cancellation is
observed before any state mutation occurs.
In
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swift`:
- Around line 49-51: The code currently performs two separate filter operations
on snapshot.agents to count "working" and "blocked" states, which is inefficient
in this hot path that runs frequently. Replace the two filter operations (lines
counting workingCount and blockedCount) with a single-pass reduce operation that
iterates through snapshot.agents once and accumulates both counts
simultaneously. Use a tuple or dictionary to track both the working and blocked
counts in one pass, then assign the results to the workingCount and blockedCount
variables.
In `@Sources/ContentView.swift`:
- Around line 16056-16099: The ClaudeAgentsSessionSource enum contains process
I/O and executable-resolution logic that is independent of SwiftUI view state
and should not be embedded in ContentView.swift. Create a new dedicated source
file (e.g., ClaudeAgentsSessionSource.swift) in the app-layer Sources directory,
move the entire ClaudeAgentsSessionSource enum including both the fetch() and
runOnce() static methods to this new file, remove ClaudeAgentsSessionSource from
ContentView.swift, and update the imports in ContentView.swift to reference the
moved source if needed.
- Around line 16062-16067: The fetch() method dispatches runOnce() to GCD with
no timeout or cancellation mechanism, causing hung processes to persist
indefinitely when stop() is called (which only cancels the Swift task, not the
dispatched work). Add a bounded deadline using async/await timeout pattern to
the fetch() function that terminates the underlying Process object if the
timeout is exceeded, and ensure the cancellation handler explicitly stops the
process before returning nil. This prevents orphaned processes from accumulating
when the sidebar disappears and reappears.
🪄 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: 502155fd-6673-4d49-9e8c-3ecc9f800177
📒 Files selected for processing (10)
Examples/CustomSidebars/claude-agents.swiftPackages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swiftPackages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swiftPackages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarAgentSnapshot.swiftPackages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarContextSnapshot.swiftPackages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swiftPackages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/ClaudeAgentsSessionParserTests.swiftPackages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/CustomSidebarDataContextBuilderTests.swiftSources/ContentView.swiftdocs/custom-sidebars.md
| return CustomSidebarAgentSnapshot( | ||
| id: id, | ||
| cwd: cwd, | ||
| kind: kind ?? "", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if `kind` is a required field in the claude agents JSON schema
# Search for documentation or schema definitions related to claude agents
rg -i "kind.*background.*interactive" --type md
# Check the test payload to see if kind is always present
cat Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/ClaudeAgentsSessionParserTests.swift | grep -A5 '"kind"'Repository: manaflow-ai/cmux
Length of output: 793
🏁 Script executed:
cat -n Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift | head -70Repository: manaflow-ai/cmux
Length of output: 2502
🏁 Script executed:
# Check the header comment and filtering logic
sed -n '1,50p' Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swiftRepository: manaflow-ai/cmux
Length of output: 2113
🏁 Script executed:
# Look for the CustomSidebarAgentSnapshot definition
rg "CustomSidebarAgentSnapshot" -A 5 Packages/macOS/CmuxSidebar/Sources/Repository: manaflow-ai/cmux
Length of output: 7669
🏁 Script executed:
# Check for any kind-based filtering
rg "kind" Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift -B 2 -A 2Repository: manaflow-ai/cmux
Length of output: 345
Inconsistent field validation: kind defaults to empty string while cwd is filtered for emptiness.
Line 38 filters out entries missing a non-empty cwd, but line 42 silently defaults missing kind to "" without filtering. The header comment (lines 7-11) documents that "entries missing a cwd are dropped"—implying a stated leniency for malformed data—yet applies no equivalent filtering to kind. Since CustomSidebarAgentSnapshot.kind is non-optional and downstream code in CustomSidebarDataContextBuilder.agentValue() uses agent.kind directly in context ("kind": .string(agent.kind)) and comparisons (agent.kind == "background"), entries with empty-string kind will be included and may cause unexpected behavior in sidebar rendering or queries.
Either apply the same filtering to kind entries (add !kind.isEmpty to the guard on line 38), or update the header comment and document that empty-string kind is intentional graceful degradation.
🤖 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
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift`
at line 42, The ClaudeAgentsSessionParser is inconsistently validating the
`kind` field compared to the `cwd` field. Add a check for empty `kind` values to
the guard condition on line 38 (where `!cwd.isEmpty` is already being checked)
by adding `!kind.isEmpty` to filter out entries with empty kind values, ensuring
consistent validation behavior between the two fields as documented in the
header comment that states entries with missing required fields are dropped.
| if let result = await fetch() { | ||
| self?.sessions = result | ||
| } | ||
| if Task.isCancelled { break } | ||
| try? await Task.sleep(for: interval) |
There was a problem hiding this comment.
Check cancellation before publishing fetched sessions.
stop() can cancel while fetch() is still in flight; when that await returns, this writes sessions before observing cancellation, so an old stopped task can overwrite the current cache after a restart.
Proposed fix
- if let result = await fetch() {
- self?.sessions = result
- }
- if Task.isCancelled { break }
+ let result = await fetch()
+ guard !Task.isCancelled else { break }
+ guard let self = self else { break }
+ if let result = result {
+ self.sessions = result
+ }🤖 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
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swift`
around lines 55 - 59, The race condition occurs because the code updates
self?.sessions with the fetched result before checking if the task has been
cancelled. When stop() cancels the task while fetch() is in flight, the result
is still assigned after the await completes, potentially overwriting the current
cache with stale data after a restart. Move the Task.isCancelled check to
immediately after the await fetch() returns and guard the session assignment so
it only happens if the task has not been cancelled. This ensures cancellation is
observed before any state mutation occurs.
| let agents: [SwiftValue] = snapshot.agents.map(agentValue(_:)) | ||
| let workingCount = snapshot.agents.filter { $0.state == "working" }.count | ||
| let blockedCount = snapshot.agents.filter { $0.state == "blocked" }.count |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Prefer single-pass count aggregation over repeated filters.
Lines 50-51 scan snapshot.agents twice (once for working, once for blocked). Since dataContext(for:) runs on every sidebar tick (~1Hz per the TimelineView in ContentView), a single-pass reduce would be more efficient and avoid redundant work.
♻️ Single-pass aggregation
-let agents: [SwiftValue] = snapshot.agents.map(agentValue(_:))
-let workingCount = snapshot.agents.filter { $0.state == "working" }.count
-let blockedCount = snapshot.agents.filter { $0.state == "blocked" }.count
+var workingCount = 0
+var blockedCount = 0
+let agents: [SwiftValue] = snapshot.agents.map { agent in
+ if agent.state == "working" { workingCount += 1 }
+ if agent.state == "blocked" { blockedCount += 1 }
+ return agentValue(agent)
+}As per coding guidelines, the algorithmic-complexity.md rule flags "repeated sort/filter/map work in hot paths" and requires "one-pass reducers" for "any UI/process path that can be hit often (e.g., poll interval or SwiftUI row/body updates)."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let agents: [SwiftValue] = snapshot.agents.map(agentValue(_:)) | |
| let workingCount = snapshot.agents.filter { $0.state == "working" }.count | |
| let blockedCount = snapshot.agents.filter { $0.state == "blocked" }.count | |
| var workingCount = 0 | |
| var blockedCount = 0 | |
| let agents: [SwiftValue] = snapshot.agents.map { agent in | |
| if agent.state == "working" { workingCount += 1 } | |
| if agent.state == "blocked" { blockedCount += 1 } | |
| return agentValue(agent) | |
| } |
🤖 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
`@Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swift`
around lines 49 - 51, The code currently performs two separate filter operations
on snapshot.agents to count "working" and "blocked" states, which is inefficient
in this hot path that runs frequently. Replace the two filter operations (lines
counting workingCount and blockedCount) with a single-pass reduce operation that
iterates through snapshot.agents once and accumulates both counts
simultaneously. Use a tuple or dictionary to track both the working and blocked
counts in one pass, then assign the results to the workingCount and blockedCount
variables.
Source: Coding guidelines
| enum ClaudeAgentsSessionSource { | ||
| /// Resolves the user's `claude` binary, runs `claude agents --json --all`, | ||
| /// and returns the parsed sessions. Returns `nil` on any failure (claude not | ||
| /// found, non-zero exit, unreadable output) so the poller keeps its last | ||
| /// good value. The subprocess runs on a utility queue, never the main | ||
| /// thread. | ||
| static func fetch() async -> [CustomSidebarAgentSnapshot]? { | ||
| await withCheckedContinuation { continuation in | ||
| DispatchQueue.global(qos: .utility).async { | ||
| continuation.resume(returning: runOnce()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static func runOnce() -> [CustomSidebarAgentSnapshot]? { | ||
| let resolver = AgentExecutableResolver( | ||
| configuredExecutablePaths: AgentExecutableResolver.cmuxConfiguredExecutablePaths() | ||
| ) | ||
| guard let plan = try? resolver.resolve(.claude) else { return nil } | ||
|
|
||
| let process = Process() | ||
| process.executableURL = plan.executableURL | ||
| process.arguments = ["agents", "--json", "--all"] | ||
| process.environment = plan.environment | ||
|
|
||
| let stdout = Pipe() | ||
| process.standardOutput = stdout | ||
| // Discard stderr so a chatty diagnostic can't fill an undrained pipe | ||
| // buffer and wedge the process before it exits. | ||
| process.standardError = FileHandle.nullDevice | ||
|
|
||
| do { | ||
| try process.run() | ||
| } catch { | ||
| return nil | ||
| } | ||
|
|
||
| // Read to EOF before waiting so a large `--all` payload can't deadlock | ||
| // against a full stdout pipe buffer. | ||
| let data = stdout.fileHandleForReading.readDataToEndOfFile() | ||
| process.waitUntilExit() | ||
| guard process.terminationStatus == 0 else { return nil } | ||
| return ClaudeAgentsSessionParser.parse(data) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Move the subprocess source out of ContentView.swift.
ClaudeAgentsSessionSource is process I/O and executable-resolution logic with no SwiftUI view state. Keeping it inside this already-large view file makes the polling/timeout behavior harder to test and evolve; move it to a dedicated app-layer source file and leave ContentView with only the sidebar wiring.
As per coding guidelines, “Flag features implemented directly in the app target/module's root Sources path when core logic is independent of cmux app lifecycle and can compile/test without AppKit, SwiftUI view state, Ghostty globals, or process-wide singletons.”
🧰 Tools
🪛 SwiftLint (0.63.3)
[Warning] 16062-16062: Prefer empty collection over optional collection
(discouraged_optional_collection)
[Warning] 16070-16070: Prefer empty collection over optional collection
(discouraged_optional_collection)
🤖 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/ContentView.swift` around lines 16056 - 16099, The
ClaudeAgentsSessionSource enum contains process I/O and executable-resolution
logic that is independent of SwiftUI view state and should not be embedded in
ContentView.swift. Create a new dedicated source file (e.g.,
ClaudeAgentsSessionSource.swift) in the app-layer Sources directory, move the
entire ClaudeAgentsSessionSource enum including both the fetch() and runOnce()
static methods to this new file, remove ClaudeAgentsSessionSource from
ContentView.swift, and update the imports in ContentView.swift to reference the
moved source if needed.
Source: Coding guidelines
| static func fetch() async -> [CustomSidebarAgentSnapshot]? { | ||
| await withCheckedContinuation { continuation in | ||
| DispatchQueue.global(qos: .utility).async { | ||
| continuation.resume(returning: runOnce()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Add a timeout and cancellation path for the polled subprocess.
stop() cancels only the Swift task; fetch() has already dispatched runOnce() to GCD, and nothing terminates the Process or unblocks readDataToEndOfFile() / waitUntilExit() if claude agents hangs. A sidebar disappear/reappear can then leave stuck claude processes behind and start more workers. Add a bounded deadline and cancellation handler that terminates the process before returning nil.
Also applies to: 16087-16097
🧰 Tools
🪛 SwiftLint (0.63.3)
[Warning] 16062-16062: Prefer empty collection over optional collection
(discouraged_optional_collection)
🤖 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/ContentView.swift` around lines 16062 - 16067, The fetch() method
dispatches runOnce() to GCD with no timeout or cancellation mechanism, causing
hung processes to persist indefinitely when stop() is called (which only cancels
the Swift task, not the dispatched work). Add a bounded deadline using
async/await timeout pattern to the fetch() function that terminates the
underlying Process object if the timeout is exceeded, and ensure the
cancellation handler explicitly stops the process before returning nil. This
prevents orphaned processes from accumulating when the sidebar disappears and
reappears.
Greptile SummaryThis PR adds a live
Confidence Score: 4/5Safe to merge after swapping the The new The bottom of Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant SW as VerticalTabsSidebar (SwiftUI)
participant P as ClaudeAgentsSessionPoller (@MainActor)
participant S as ClaudeAgentsSessionSource
participant DQ as DispatchQueue.global(.utility)
participant CLI as claude agents --json --all
SW->>P: start() [onAppear]
loop every 3 s
P->>S: await fetch()
S->>DQ: "DispatchQueue.global.async { runOnce() }"
DQ->>CLI: Process.run()
CLI-->>DQ: stdout + exitCode
DQ-->>S: continuation.resume(returning:)
S-->>P: [CustomSidebarAgentSnapshot]
P->>P: "sessions = result"
end
SW->>P: sessions (read on ~1s sidebar tick)
P-->>SW: [CustomSidebarAgentSnapshot]
SW->>SW: dataContext(for: snapshot) → agents array
SW->>P: stop() [onDisappear]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant SW as VerticalTabsSidebar (SwiftUI)
participant P as ClaudeAgentsSessionPoller (@MainActor)
participant S as ClaudeAgentsSessionSource
participant DQ as DispatchQueue.global(.utility)
participant CLI as claude agents --json --all
SW->>P: start() [onAppear]
loop every 3 s
P->>S: await fetch()
S->>DQ: "DispatchQueue.global.async { runOnce() }"
DQ->>CLI: Process.run()
CLI-->>DQ: stdout + exitCode
DQ-->>S: continuation.resume(returning:)
S-->>P: [CustomSidebarAgentSnapshot]
P->>P: "sessions = result"
end
SW->>P: sessions (read on ~1s sidebar tick)
P-->>SW: [CustomSidebarAgentSnapshot]
SW->>SW: dataContext(for: snapshot) → agents array
SW->>P: stop() [onDisappear]
Reviews (1): Last reviewed commit: "docs(sidebar): document agents data and ..." | Re-trigger Greptile |
| static func fetch() async -> [CustomSidebarAgentSnapshot]? { | ||
| await withCheckedContinuation { continuation in | ||
| DispatchQueue.global(qos: .utility).async { | ||
| continuation.resume(returning: runOnce()) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
DispatchQueue.global for ordinary async work is a legacy concurrency pattern that the cmux-swift-concurrency-modernization rule flags. withCheckedContinuation + DispatchQueue.global is the old bridging idiom; Task.detached(priority: .utility) is the correct Swift-native way to hop off the caller's actor and run blocking/subprocess work on a non-main executor.
| static func fetch() async -> [CustomSidebarAgentSnapshot]? { | |
| await withCheckedContinuation { continuation in | |
| DispatchQueue.global(qos: .utility).async { | |
| continuation.resume(returning: runOnce()) | |
| } | |
| } | |
| } | |
| static func fetch() async -> [CustomSidebarAgentSnapshot]? { | |
| await Task.detached(priority: .utility) { | |
| runOnce() | |
| }.value | |
| } |
Rule Used: Flag new legacy async patterns in cmux-owned Swift... (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!
| self?.sessions = result | ||
| } | ||
| if Task.isCancelled { break } | ||
| try? await Task.sleep(for: interval) |
There was a problem hiding this comment.
Using
try? here silently swallows the CancellationError that Task.sleep throws when the task is cancelled. The loop still exits on the next while !Task.isCancelled check, so the current behavior is correct — but an explicit catch { break } makes the cancellation path unambiguous and avoids a redundant loop-head evaluation after a stop() call.
| try? await Task.sleep(for: interval) | |
| do { try await Task.sleep(for: interval) } catch { break } |
Why
Custom sidebars can only render the data context cmux hands the interpreter (
workspaces,clock), so a sidebar file cannot shell out toclaude agents --jsonitself. This adds an app-side bridge so a custom sidebar can show Claude Code background agent sessions (the worktree-agent sessions you spawn), grouped by project, like a native sidebar.What
CustomSidebarAgentSnapshot— oneclaude agents --json --allentry.ClaudeAgentsSessionParser— lenient JSON → snapshots (bad/partial output degrades to empty rather than throwing).ClaudeAgentsSessionPoller—@MainActorrefresh loop with an injected fetch closure (keeps the subprocess out of the UI package).CustomSidebarContextSnapshotcarriesagents; the data-context builder emits a top-levelagentsarray plusagentsCount/agentsWorkingCount/agentsBlockedCountand per-agent derived booleans (working,blocked,done,failed,stopped,active,background).ContentViewresolves the user'sclaudebinary viaAgentExecutableResolver, runsclaude agents --json --alloff the main thread, and feeds the parsed sessions into the custom-sidebar context. Polling runs only while a custom sidebar is on screen (started/stopped with its appear/disappear), so noclaudeprocess runs otherwise.docs/custom-sidebars.mddocuments the newagentsdata.Examples/CustomSidebars/claude-agents.swiftgroups sessions bycwdwith state dots.Notes
project.pbxprojchanges.swift testfor CmuxSidebar passes locally.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Show Claude Code agent sessions in custom sidebars by exposing
claude agents --json --alldata, with live polling only while a custom sidebar is visible. This lets sidebar files group and display agent activity by project with simple state flags.CmuxSidebar: addCustomSidebarAgentSnapshot, a lenientClaudeAgentsSessionParser, andClaudeAgentsSessionPollerwith an injected fetch.agentsplusagentsCount,agentsWorkingCount,agentsBlockedCount, and per-agent booleans (working,blocked,done,failed,stopped,active,background).claudeviaAgentExecutableResolver, runclaude agents --json --alloff the main thread, and poll only while a custom sidebar is shown.agentsdata and addExamples/CustomSidebars/claude-agents.swift(groups bycwdwith state dots).Written for commit c12f310. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Documentation
agentsbinding and available agent properties.