Skip to content

feat(sidebar): show claude agents --json sessions in custom sidebars - #6493

Open
samuelpatro wants to merge 3 commits into
manaflow-ai:mainfrom
samuelpatro:feat/sidebar-claude-agents
Open

samuelpatro wants to merge 3 commits into
manaflow-ai:mainfrom
samuelpatro:feat/sidebar-claude-agents

Conversation

@samuelpatro

@samuelpatro samuelpatro commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

Custom sidebars can only render the data context cmux hands the interpreter (workspaces, clock), so a sidebar file cannot shell out to claude agents --json itself. 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

  • CmuxSidebar package (pure, testable, no pbxproj):
    • CustomSidebarAgentSnapshot — one claude agents --json --all entry.
    • ClaudeAgentsSessionParser — lenient JSON → snapshots (bad/partial output degrades to empty rather than throwing).
    • ClaudeAgentsSessionPoller — @MainActor refresh loop with an injected fetch closure (keeps the subprocess out of the UI package).
    • CustomSidebarContextSnapshot carries agents; the data-context builder emits a top-level agents array plus agentsCount / agentsWorkingCount / agentsBlockedCount and per-agent derived booleans (working, blocked, done, failed, stopped, active, background).
  • App: ContentView resolves the user's claude binary via AgentExecutableResolver, runs claude agents --json --all off 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 no claude process runs otherwise.
  • Docs: docs/custom-sidebars.md documents the new agents data.
  • Example: Examples/CustomSidebars/claude-agents.swift groups sessions by cwd with state dots.

Notes

  • No new app-target source files (all new types live in the SPM package), so no project.pbxproj changes.
  • New unit tests cover the parser (mapping, leniency, non-JSON) and the builder (agents array, counts, per-agent field/boolean projection). swift test for CmuxSidebar passes locally.
  • The app target could not be built locally (unrelated zig 0.15.2 vs Xcode 26.5 SDK toolchain mismatch on this machine); relying on CI to build/verify the app side.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Show Claude Code agent sessions in custom sidebars by exposing claude agents --json --all data, 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.

  • New Features
    • CmuxSidebar: add CustomSidebarAgentSnapshot, a lenient ClaudeAgentsSessionParser, and ClaudeAgentsSessionPoller with an injected fetch.
    • Data context: emit agents plus agentsCount, agentsWorkingCount, agentsBlockedCount, and per-agent booleans (working, blocked, done, failed, stopped, active, background).
    • App: resolve the user’s claude via AgentExecutableResolver, run claude agents --json --all off the main thread, and poll only while a custom sidebar is shown.
    • Docs/Example: document the new agents data and add Examples/CustomSidebars/claude-agents.swift (groups by cwd with state dots).
    • Tests: cover parser leniency/mapping and data-context projection.

Written for commit c12f310. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Custom sidebars can now display Claude Code agent sessions with live, auto-refreshing status updates.
    • Shows working, blocked, and failed agents with aggregate counts.
    • Each agent displays name, working directory, and optional wait information.
  • Documentation

    • Updated custom sidebar documentation to detail the new agents binding and available agent properties.

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).
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

@samuelpatro is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds end-to-end Claude agent session support to the custom sidebar: a new CustomSidebarAgentSnapshot data model, a lenient ClaudeAgentsSessionParser, a ClaudeAgentsSessionPoller with async fetch injection, extensions to CustomSidebarContextSnapshot and CustomSidebarDataContextBuilder, a subprocess-based ClaudeAgentsSessionSource wired into VerticalTabsSidebar, a SwiftUI example, tests, and documentation.

Changes

Claude Agents Sidebar Integration

Layer / File(s) Summary
Agent snapshot data model
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarAgentSnapshot.swift
Defines CustomSidebarAgentSnapshot, the Sendable/Equatable struct holding all agent session fields with a public memberwise initializer.
Lenient JSON parser
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift
Adds ClaudeAgentsSessionParser.parse(_:) decoding claude agents --json output, silently dropping entries missing cwd and returning [] on top-level decode failure.
Background session poller
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swift
Adds ClaudeAgentsSessionPoller, a @MainActor class with a non-overlapping async refresh loop, configurable 3-second default interval, injected fetch closure, and explicit start()/stop() lifecycle.
Context snapshot and data context builder extensions
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarContextSnapshot.swift, Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swift
Extends CustomSidebarContextSnapshot with an agents property; extends dataContext(for:) to emit agents, agentsCount, agentsWorkingCount, and agentsBlockedCount; adds agentValue(_:) mapping a snapshot into a SwiftValue with required fields, derived boolean flags, and conditionally-present optional fields.
Subprocess fetch source and VerticalTabsSidebar wiring
Sources/ContentView.swift
Adds ClaudeAgentsSessionSource.fetch() running claude agents --json --all as a subprocess and parsing stdout. Wires claudeAgentsSessionPoller as @State in VerticalTabsSidebar, injects agents into the sidebar context, and starts/stops the poller on custom sidebar appear/disappear.
Parser and context builder tests
Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/ClaudeAgentsSessionParserTests.swift, Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/CustomSidebarDataContextBuilderTests.swift
Adds ClaudeAgentsSessionParserTests (full-payload parse, missing-cwd drop, non-JSON fallback) and new CustomSidebarDataContextBuilderTests cases for top-level agent fields, empty default, and agentValue field mapping with derived boolean flags.
SwiftUI example and documentation
Examples/CustomSidebars/claude-agents.swift, docs/custom-sidebars.md
Adds a claude-agents.swift UI block showing grouped sorted sessions with aggregate counters and per-session status indicators. Documents the agents binding shape, derived status flags, conditionally-present fields, and polling behavior in custom-sidebars.md.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • manaflow-ai/cmux#5275: The claude-agents.swift SwiftUI example relies on nested-view composability and modifier primitives expanded in this PR.

Poem

🐇 Hop, hop, I watch the agents spin,
Their cwd sorted, their statuses in—
Working or blocked, the dot glows bright,
The poller ticks every three seconds at night.
JSON parsed leniently, no crash to fear,
The sidebar reflects what Claude holds dear! 🌟


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces Task.sleep for polling in ClaudeAgentsSessionPoller (line 59) and blocking subprocess waits (readDataToEndOfFile, waitUntilExit) in ContentView without proper cancellation/timeo... Replace Task.sleep with real cancellation-aware signal; add timeout and cancellation termination of the subprocess process before blocking I/O calls return.
Cmux Algorithmic Complexity ❌ Error CustomSidebarDataContextBuilder.swift lines 49-51 performs three passes (one map + two filters) over snapshot.agents in a hot path (CustomSidebarDataContext called on every ~1Hz TimelineView tick),... Combine lines 49-51 into a single pass that accumulates workingCount and blockedCount while mapping agents, avoiding the redundant filter operations on every sidebar tick.
Cmux Full Internationalization ❌ Error The example file Examples/CustomSidebars/claude-agents.swift contains user-facing hardcoded strings ("Claude Agents", "working", "blocked", "total", "session") without localization APIs or matching... Wrap user-facing strings in the example with String(localized:defaultValue:) and add corresponding entries to Resources/Localizable.xcstrings with translations for all supported locales (en, ja).
Cmux Architecture Rethink ❌ Error PR introduces polling with unbounded subprocess blocking without cancellation. ContentView.ClaudeAgentsSessionSource calls process.waitUntilExit() with no timeout; stop() can't terminate the proces... Add process termination on task cancellation using withTaskCancellationHandler (as SessionIndexStore does) with a timeout. ContentView.ClaudeAgentsSessionSource.fetch() must terminate the process if it doesn't respond within a deadline.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature: exposing claude agents --json sessions in custom sidebars.
Description check ✅ Passed The description covers why (sidebar data limitation), what (component breakdown), and testing/documentation notes. Testing coverage is explained but no demo video provided.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR correctly implements Swift 6 actor isolation: pure value types (CustomSidebarAgentSnapshot, CustomSidebarContextSnapshot, CustomSidebarDataContextBuilder, ClaudeAgentsSessionParser) follow exist...
Cmux Expensive Synchronous Load ✅ Passed The PR does not add expensive synchronous loaders to the main actor or interactive paths. All subprocess I/O (readDataToEndOfFile, waitUntilExit) runs on a utility queue via DispatchQueue.global(qo...
Cmux Cache Substitution Correctness ✅ Passed Agent data cache is used only for transient sidebar UI rendering (not persisted to disk/UserDefaults/undo paths), with cold cache initialized empty and stale cache prevented by stopping polls when...
Cmux No Hacky Sleeps ✅ Passed This check applies only to TypeScript, JavaScript, shell, or build/runtime scripts. The PR contains only Swift code and documentation; no changes to non-Swift runtime code.
Cmux Swift Concurrency ✅ Passed PR uses modern Swift concurrency: ClaudeAgentsSessionPoller stores Tasks with explicit lifecycle (start/stop, allowed by guidelines), ClaudeAgentsSessionSource uses DispatchQueue.global() to bridge...
Cmux Swift @Concurrent ✅ Passed ClaudeAgentsSessionSource.fetch() performs heavy I/O from @MainActor context but includes an explicit actor hop via DispatchQueue.global(qos: .utility), satisfying the rule's requirement for either...
Cmux Swift File And Package Boundaries ✅ Passed All new production Swift files comply with boundary rules: no oversized files (largest is 69 lines), no large additions to ContentView (only +65 lines, well under 250-line threshold), package conta...
Cmux Swiftpm Lockfiles ✅ Passed CmuxSidebar Package.resolved is committed with the PR; no cmux-owned .gitignore ignores Package.resolved; root Xcode Package.resolved included; only path-based dependencies (no external pins added).
Cmux Swift Logging ✅ Passed No Swift logging violations found. PR adds no print/debugPrint/dump/NSLog calls, no ad hoc file logging, no unsanitized Logger, and no secrets/personal data in logs.
Cmux User-Facing Error Privacy ✅ Passed PR adds agent polling to custom sidebars with no user-facing error messages, credential exposure, or sensitive data disclosure. Failures (missing executable, non-zero exit, parse errors) return nil...
Cmux Swiftui State Layout ✅ Passed PR passes SwiftUI state layout rules: uses @State for non-observable poller lifecycle management, passes only value snapshots to interpreters, no store references in ForEach rows, and maintains cle...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no new NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup declarations; all new types are data structures and parsers in the CmuxSidebar package or view content examples.
Cmux Source Artifacts ✅ Passed All changed files are hand-written source code, tests, documentation, or examples—no artifacts, build output, logs, or generated files detected.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seams found in production source files. All new public APIs (ClaudeAgentsSessionParser.parse, ClaudeAgentsSessionPoller, agentValue) have legitimate production usage, and newly added...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between ae0c71e and c12f310.

📒 Files selected for processing (10)
  • Examples/CustomSidebars/claude-agents.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarAgentSnapshot.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarContextSnapshot.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swift
  • Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/ClaudeAgentsSessionParserTests.swift
  • Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/CustomSidebarDataContextBuilderTests.swift
  • Sources/ContentView.swift
  • docs/custom-sidebars.md

return CustomSidebarAgentSnapshot(
id: id,
cwd: cwd,
kind: kind ?? "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 -70

Repository: 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.swift

Repository: 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 2

Repository: 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.

Comment on lines +55 to +59
if let result = await fetch() {
self?.sessions = result
}
if Task.isCancelled { break }
try? await Task.sleep(for: interval)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +49 to +51
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
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

Comment thread Sources/ContentView.swift
Comment on lines +16056 to +16099
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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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

Comment thread Sources/ContentView.swift
Comment on lines +16062 to +16067
static func fetch() async -> [CustomSidebarAgentSnapshot]? {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .utility).async {
continuation.resume(returning: runOnce())
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a live claude agents --json --all bridge into the custom-sidebar interpreter context, exposing agent sessions as a top-level agents array (plus aggregate counts and per-agent derived booleans) while polling only when a custom sidebar is on screen.

  • New CmuxSidebar types (CustomSidebarAgentSnapshot, ClaudeAgentsSessionParser, ClaudeAgentsSessionPoller) are cleanly layered in the SPM package with an injected fetch closure keeping subprocess logic in the app layer; parser is lenient and degrades to an empty array on any failure.
  • ClaudeAgentsSessionSource.fetch() in ContentView.swift bridges the subprocess call back to async using withCheckedContinuation + DispatchQueue.global(qos: .utility) — the legacy dispatch bridging idiom that cmux-swift-concurrency-modernization flags; a Task.detached(priority: .utility) is the Swift-native replacement.
  • ClaudeAgentsSessionPoller uses try? await Task.sleep which silently swallows CancellationError; the loop exits correctly anyway via the while !Task.isCancelled check, but an explicit catch { break } makes the cancellation path unambiguous.

Confidence Score: 4/5

Safe to merge after swapping the DispatchQueue.global bridge in ClaudeAgentsSessionSource.fetch() for a Task.detached; all other changes are well-structured and well-tested.

The new ClaudeAgentsSessionSource.fetch() wraps a synchronous subprocess call using withCheckedContinuation + DispatchQueue.global(qos: .utility) — the legacy dispatch bridging idiom that the cmux Swift concurrency rule explicitly flags. The rest of the PR (lenient parser, injected-fetch poller, data-context projection, docs, and tests) is clean and correctly layered.

The bottom of Sources/ContentView.swift where ClaudeAgentsSessionSource lives needs the DispatchQueue.global → Task.detached swap. All CmuxSidebar package files look fine.

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds ClaudeAgentsSessionSource (new enum at bottom of file) and wires ClaudeAgentsSessionPoller into VerticalTabsSidebar via @State. The new fetch() method uses DispatchQueue.global + withCheckedContinuation, a legacy pattern that should be Task.detached(priority: .utility) per cmux's Swift concurrency modernization rule.
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionPoller.swift New @MainActor poller with injected fetch closure. Lifecycle is clean (start/stop, no-op on double start). try? await Task.sleep silently drops CancellationError; the loop exits correctly anyway but an explicit catch { break } would be cleaner.
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/ClaudeAgentsSessionParser.swift Lenient JSON decoder — all fields optional, cwd-less entries silently dropped, non-JSON output degrades to empty array. Well-structured and matches the documented contract.
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarAgentSnapshot.swift Plain Sendable, Equatable value type mirroring one claude agents --json entry. No issues.
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Layout/CustomSidebarDataContextBuilder.swift New agentValue projection and aggregate count fields added; two separate filter passes are fine at the expected agent counts. agentValue is public but has a genuine production caller in dataContext(for:), so it is not a test seam.
Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/ClaudeAgentsSessionParserTests.swift Good coverage: representative payload mapping, dropped-entry leniency, and non-JSON fallback. No issues.
Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/CustomSidebarDataContextBuilderTests.swift New tests cover agents array, aggregate counts, optional field omission, and derived boolean projection. Solid coverage.
Examples/CustomSidebars/claude-agents.swift Illustrative example grouping agents by cwd with status-colour dots. Example-only code, not shipped. No issues.
docs/custom-sidebars.md Documents the new agents binding, all per-entry fields, aggregate count scalars, and the empty-when-claude-absent behavior. Accurate and complete.

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]
Loading
%%{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]
Loading

Reviews (1): Last reviewed commit: "docs(sidebar): document agents data and ..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines +16062 to +16068
static func fetch() async -> [CustomSidebarAgentSnapshot]? {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .utility).async {
continuation.resume(returning: runOnce())
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Suggested change
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
try? await Task.sleep(for: interval)
do { try await Task.sleep(for: interval) } catch { break }

@teamleaderleo teamleaderleo added enhancement New feature or request area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status area: sidebar The workspace sidebar: list, groups, status, reordering S3: minor Wrong behavior with a workaround labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status area: sidebar The workspace sidebar: list, groups, status, reordering enhancement New feature or request S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants