Skip to content

Vibe-codable custom sidebars (runtime Swift interpreter), behind a beta flag - #5254

Merged
azooz2003-bit merged 13 commits into
mainfrom
feat-dsl-sidebar
Jun 3, 2026
Merged

azooz2003-bit merged 13 commits into
mainfrom
feat-dsl-sidebar

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 2, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adds user/agent-authored custom sidebars: a single SwiftUI-style view, interpreted at runtime (no toolchain, no signing, no compile step), discovered from ~/.config/cmux/sidebars/<name>.{swift,json} and selectable in the sidebar toggle button's right-click provider picker. It renders in the real sidebar area, hot-reloads on save, binds to a live workspace data context, and runs cmux commands on tap.

Everything is gated by betaFeatures.customSidebars (default off); while off, nothing changes in the app.

How it's structured (mirrors CmuxSettings / CmuxSettingsUI)

  • Packages/CmuxSwiftRender (logic, pure, no SwiftUI, unit-tested): swift-syntax parser + tree-walking interpreter for a growing Swift subset lowering to a RenderNode IR. Covers Text/VStack/HStack/ZStack/HSplitView/Button (string + label forms) /Image/shapes/Spacer/Divider; for/if/let/ForEach/ternary; member access, subscript, array + string methods; data binding and cmux() action capture.
  • Packages/CmuxSwiftRenderUI (UI): RenderNode to native SwiftUI (RenderNodeView, resizable HSplitView, JSON DSL renderer, CustomSidebarModel/CustomSidebarView). The cmux-coupled executor is injected via SidebarActionDispatch through the SwiftUI environment, so the package has zero TerminalController dependency.
  • App target (thin composition root): ContentView adds the provider, the live data context (workspaces/tabs/clock), and a TimelineView auto-refresh; makeCmuxSidebarActionDispatch() runs actions via TerminalController; a small runV2CommandLine seam exposes the v2 dispatcher in-process.

Notes for review

  • This is an early, opt-in feature spike. It builds clean (app + both packages); the interpreter has 24 passing unit tests in CmuxSwiftRender.
  • docs/custom-sidebar-gap-report.md is a multi-agent survey of 14 opinionated sidebar concepts plus the prioritized interpreter-feature gaps that drive the remaining roadmap (richer data provider, more actions, @State/inputs, popover for per-workspace notes).
  • The app-target file is still named Sources/DSLSidebarPlayground.swift (legacy from the JSON-DSL spike); worth renaming.
  • Not for merge before dogfood.

🤖 Generated with Claude Code


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


Summary by cubic

Adds runtime‑interpreted custom sidebars (SwiftUI‑style or JSON) behind a beta flag. They hot‑reload, bind to rich workspace data, support actions and drag‑and‑drop reordering, respect titlebar insets, and now cache the parsed Swift program for smoother updates while JSON actions dispatch through the same sink.

  • New Features

    • Beta flag customSidebars (default off). Sidebars are discovered in ~/.config/cmux/sidebars/<name>.{swift,json}, selectable in the provider picker, and hot‑reload on save. cmux docs sidebars opens the authoring guide.
    • Data context Wave A: full read‑surface for workspaces and surfaces (e.g., pinned, index, directory, ports/portCount, unread, tabs/tabCount, description, color, git {branch,dirty}, pullRequest {number,label,url,status,stale,branch}, progress {value,label}, latest agent message/prompt/timestamp, remote {target,state,connected}), plus selectedId, unreadTotal, and clock.weekday; absent fields are omitted.
    • CmuxSwiftRender: swift-syntax interpreter to RenderNode IR; supports user-defined funcs (value and view, including explicit return), flatMap/reduce, .formatted(.currency/.notation/...) with real currency codes, Color(...), ScrollView, .strikethrough, .font(.system(...)), and openURL(...).
    • CmuxSwiftRenderUI: renders IR to native SwiftUI (resizable HSplit, JSON DSL, reorderable rows via SidebarActionDispatch); host‑injected content insets keep content below the titlebar accessory and fade into the top mask.
    • Interpreter reliability: short‑circuits &&/||, guards integer / and % by zero (tests assert soft‑fail output), and honors sorted { ... } comparators via a stable insertion sort. Tests updated (34 passing).
    • Performance and fixes: parsed‑AST caching (parse → ParsedProgram) avoids per‑tick re‑parsing; JSON DSL actions now route through SidebarActionDispatch (log/openURL/cmux method) instead of being dropped.
    • Docs and tests: cmux docs sidebars authoring guide; corpus of 14 persona sidebars with a coverage harness. Minor cleanup: renamed the app‑side action sink to CmuxSidebarActionDispatch, removed a stale gap report, and split RenderNode.swift into dedicated files (no behavior change).
  • Migration

    • Enable in Settings: Beta → Custom sidebars.
    • Create ~/.config/cmux/sidebars/<name>.swift or .json, then pick it from the sidebar button’s provider picker (right‑click). Use cmux docs sidebars for the authoring guide.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Experimental "custom sidebars" (beta) with live, hot-reloaded Swift/.json sidebars, integrated into the sidebar picker and guarded by a beta toggle.
    • Runtime SwiftUI-like interpreter producing renderable nodes, JSON-DSL renderer, resizable two-column split, reorderable lists, and tappable actions routed to the host app.
  • Documentation

    • Full authoring guide and design/roadmap docs for custom sidebars and the interpreter.
  • Tests

    • Extensive interpreter and corpus test coverage added.

Custom sidebars authored as a single SwiftUI-style view, interpreted at
runtime (no toolchain/signing), discovered from ~/.config/cmux/sidebars/
and selectable in the sidebar button's provider picker. Renders in the real
sidebar area, hot-reloads on save, binds to a live workspace data context,
and runs cmux commands on tap.

Packages (mirroring CmuxSettings/CmuxSettingsUI):
- CmuxSwiftRender (logic): swift-syntax parser + tree-walking interpreter for
  a growing Swift subset -> RenderNode IR. Pure, no SwiftUI. Unit tested.
  Covers Text/VStack/HStack/ZStack/HSplitView/Button(both forms)/Image/
  shapes/Spacer/Divider; for/if/let/ForEach/ternary; member access, subscript,
  array+string methods; data binding + cmux() action capture.
- CmuxSwiftRenderUI (UI): RenderNode -> native SwiftUI (RenderNodeView,
  resizable HSplitView, JSON DSL renderer, CustomSidebarModel/View). The
  cmux-coupled action executor is injected via SidebarActionDispatch
  (Environment), so the package has no TerminalController dependency.

App target: thin composition root. ContentView adds the provider, the live
data context (workspaces/tabs/clock), and a TimelineView auto-refresh;
makeCmuxSidebarActionDispatch() runs actions via TerminalController; a
runV2CommandLine seam exposes the dispatcher in-process.

Gated by betaFeatures.customSidebars (default off). docs/custom-sidebar-gap-report.md
captures a multi-agent survey of opinionated sidebar ideas and the prioritized
interpreter gaps driving further work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 3, 2026 3:40pm
cmux-staging Building Building Preview, Comment Jun 3, 2026 3:40pm

@coderabbitai

coderabbitai Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a runtime-interpreted sidebar feature that allows authors to write custom sidebars in SwiftUI-like source or a declarative JSON DSL. It adds two new packages (CmuxSwiftRender, CmuxSwiftRenderUI), implements expression and view interpreters producing a RenderNode IR, JSON rendering and SwiftUI lowering, host action dispatch, project wiring, tests, corpus examples, and docs.

Changes

Custom Sidebars Feature

Layer / File(s) Summary
Beta Feature Flag
Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
Adds customSidebars boolean toggle to the beta feature catalog with false default value.
Packages & Project Wiring
Packages/CmuxSwiftRender/Package.swift, Packages/CmuxSwiftRender/Package.resolved, Packages/CmuxSwiftRenderUI/Package.swift, Packages/CmuxSwiftRenderUI/Package.resolved, cmux.xcodeproj/project.pbxproj, cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Creates two new local Swift packages with swift-syntax dependency, updates SPM lockfiles, and wires both packages and CmuxSidebarActionDispatch.swift into the Xcode project.
Runtime Contracts
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftValue.swift, Environment.swift, ActionCommand.swift, ButtonAction.swift, ModifierArg.swift, RenderModifier.swift, RenderNode.swift, ReorderSpec.swift
Defines the runtime value representation (SwiftValue), lexical scope (Environment), action/command types (ActionCommand, ButtonAction), modifier argument types (ModifierArg, RenderModifier), RenderNode IR and reorder spec.
Expression Evaluator
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
Interprets Swift expression syntax into SwiftValue for literals, interpolation, operators, subscripts, array/string methods, closures, user functions, number formatting, and Color resolution.
Swift View Interpreter
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
Parses Swift source, normalizes operators, and evaluates supported SwiftUI-like constructors, modifier chains, ViewBuilder control flow (let, for, if/else, ForEach), action extraction, and reorderable expansions into RenderNode.
JSON DSL & RenderNode Rendering
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift, DSLDocument.swift, DSLAction.swift, DSLNodeKind.swift, DSLSidebarRenderer.swift, Rendering/RenderStyle.swift, RenderNodeView.swift, OptionalStyleModifiers.swift, CustomSidebarContentInsets.swift, ResizableHSplit.swift, ReorderableList.swift
Adds JSON sidebar schema and JSON-to-SwiftUI renderer, RenderNode-to-SwiftUI lowering with style/token resolution, optional modifiers, split/resizable HSplit, content insets, and drag-and-drop reorder support.
Sidebar Model, View & Dispatch
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Action/SidebarActionDispatch.swift, Sidebar/CustomSidebarModel.swift, Sidebar/CustomSidebarView.swift, Sources/CmuxSidebarActionDispatch.swift
Introduces SidebarActionDispatch env key, file-backed CustomSidebarModel with hot-reload, CustomSidebarView that renders JSON or interprets Swift against a live data context, and a host dispatch that converts actions to v2 JSON commands or opens URLs.
Provider Integration & Host Dispatch
Sources/ContentView.swift, Sources/TerminalController.swift
Discovers custom sidebar files under ~/.config/cmux/sidebars, gates them by the beta flag, builds a per-render data context, mounts CustomSidebarView (driven by TimelineView), and adds runV2CommandLine() seam for in-process v2 command invocation.
Interpreter Tests
Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
Adds extensive tests covering parser output, builder control flow, data binding, action capture, modifier/frame handling, higher-order and string/array helpers, user-defined functions, range iteration, and robustness.
Corpus Coverage & Examples
Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/*.swift, CorpusCoverageTests.swift
Adds many persona-based example sidebars and a coverage test that evaluates the corpus and reports unsupported constructs.
Docs & CLI References
docs/custom-sidebars.md, docs/data-driven-sidebar-plan.md, docs/swiftui-interpreter-surface.md, CLI/CMUXCLI+DocsSettings.swift, CLI/cmux.swift
Adds an authoring guide, architectural/roadmap docs for the data-driven sidebar and interpreter surface, and CLI/docs index/help updates for the sidebars topic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5182: Changes provider-descriptor plumbing in Sources/ContentView.swift; overlaps with provider discovery/selection logic here.
  • manaflow-ai/cmux#5127: Related edits to effectiveExtensionSidebarProviderId routing in Sources/ContentView.swift.
  • manaflow-ai/cmux#4975: Adds beta toggles to BetaFeaturesCatalogSection; related because this PR adds the customSidebars toggle.

"🐰 I nibble tokens, hop through code with cheer,
Custom sidebars blooming, live and near.
Hot-reload whiskers twitch, actions scurry fast,
RenderNodes and JSON make a playful cast.
Cheers from a rabbit—may your UI hold fast!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-dsl-sidebar

@greptile-apps

greptile-apps Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces an opt-in beta feature for runtime-interpreted custom sidebars: users write a SwiftUI-style .swift or .json file in ~/.config/cmux/sidebars/, the interpreter walks the swift-syntax AST into a RenderNode IR, and the UI package renders it as native SwiftUI with hot-reload, live workspace data, and in-app command dispatch. The feature is gated by betaFeatures.customSidebars (default off) and is organized across two new SwiftPM packages (CmuxSwiftRender for logic, CmuxSwiftRenderUI for rendering).

  • New packages CmuxSwiftRender and CmuxSwiftRenderUI add the interpreter, IR, renderer, file-watcher model, and JSON DSL; the app target contributes only the cmux-coupled action dispatch and the live workspace data projection.
  • if let optional-binding conditions in view code silently evaluate to false — the documented if let b = w.branch { ... } pattern never renders its body; the condition handling in evalClosure/evalClosure2 also does not support multi-statement closures with return.
  • The sorted { comparator } path uses an O(n²) insertion sort run on the main actor every second via the enclosing TimelineView, which becomes significant for workspace lists beyond a few dozen entries.

Confidence Score: 3/5

The feature is correctly gated behind a default-off beta flag and the packages are well-structured, but the interpreter has multiple silent correctness gaps that make it unreliable for the use cases it documents.

Two new correctness issues compound the ones already on the thread: if let optional-binding conditions always evaluate to false — silently breaking the documented authoring pattern for optional workspace fields — and sorted { comparator } runs an O(n²) interpreted loop on the main actor every second, creating sustained CPU pressure for any sidebar that sorts a non-trivial workspace list. Together with the previously-flagged full-parse-per-tick, blocking file I/O on the main actor, range overflow traps, and missing locale catalog entries, the interpreter is not yet reliable enough for the use cases its own documentation promises.

SwiftViewInterpreter.swift (evalIf needs optional-binding support), ExpressionEvaluator.swift (sorted needs an element cap or cached result), CustomSidebarModel.swift (blocking reload), CustomSidebarView.swift (parse-per-tick), and ContentView.swift (dispatch allocation and filesystem scan).

Important Files Changed

Filename Overview
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift Core view interpreter: if let optional-binding conditions silently evaluate to false, breaking the documented authoring pattern for optional workspace fields.
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift Expression evaluator: sorted { } uses O(n²) insertion sort called on the main actor every 1-second tick; evalClosure/evalClosure2 still drop ReturnStmtSyntax (previously flagged).
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift File-watching model: blocking synchronous file I/O in reload() runs on the main actor (previously flagged); debugDescription surfaces internal decoder details to users (previously flagged).
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift View: full swift-syntax parse + AST walk on every TimelineView tick (previously flagged); localization keys sidebar.custom.* missing from all locale catalogs (previously flagged).
Sources/ContentView.swift Composition root: makeCmuxSidebarActionDispatch() called inside TimelineView body (previously flagged); synchronous directory scan in customSidebarDescriptors on main actor (previously flagged); dual-source-of-truth beta flag read (previously flagged).
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftValue.swift Value type: Int.max + 1 overflow trap in inclusive ranges and unbounded range materialization (both previously flagged).
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift Renderer: .subheadline incorrectly resolves to .headline due to substring matching order (previously flagged); modifier application is otherwise well-structured.
Sources/CmuxSidebarActionDispatch.swift Thin composition root: maps interpreted button actions onto TerminalController.runV2CommandLine; correctly coerces integer-looking params to numbers.
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Action/SidebarActionDispatch.swift Clean environment-key dispatch; noop default and @MainActor @Sendable run closure are correct.

Sequence Diagram

sequenceDiagram
    participant TV as TimelineView (1s tick)
    participant CV as CustomSidebarView.body
    participant SVI as SwiftViewInterpreter
    participant EE as ExpressionEvaluator
    participant SVM as CustomSidebarModel
    participant FW as FileWatcher (kqueue)

    FW-->>SVM: file change event
    SVM->>SVM: reload() [main actor, blocking I/O]
    SVM->>SVM: "state = .swiftSource(newSource)"

    loop Every 1 second
        TV->>CV: timeline.date tick
        CV->>CV: customSidebarDataContext(now:)
        CV->>SVI: evaluate(source, state: dataContext)
        SVI->>SVI: Parser.parse(source:) [full parse]
        SVI->>EE: evalItems / evalFor / evalIf
        EE->>EE: sorted closure O(n²) insertion sort
        SVI-->>CV: RenderNode tree
        CV-->>TV: rendered SwiftUI view
    end

    note over CV,SVI: if let conditions silently false
    note over EE: ReturnStmtSyntax in closures dropped
Loading

Reviews (10): Last reviewed commit: "Strengthen div-by-zero test to assert th..." | Re-trigger Greptile

Comment on lines +123 to +124
case "/": return bothInt ? .int(Int(l) / Int(r)) : .double(l / r)
case "%": return bothInt ? .int(Int(l) % Int(r)) : nil

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.

P0 Integer division and modulo by zero are unguarded Swift runtime traps here. When user-authored sidebar code contains an expression like x / 0 or count % 0, Int(l) / Int(r) (and %) traps and crashes the host app. Guard both cases before the switch, or return nil for zero denominators, so the interpreter silently drops the result rather than taking the process down.

Suggested change
case "/": return bothInt ? .int(Int(l) / Int(r)) : .double(l / r)
case "%": return bothInt ? .int(Int(l) % Int(r)) : nil
case "/":
if bothInt {
guard Int(r) != 0 else { return nil }
return .int(Int(l) / Int(r))
}
return .double(l / r)
case "%":
guard Int(r) != 0 else { return nil }
return bothInt ? .int(Int(l) % Int(r)) : nil

Comment on lines +56 to +68
do {
state = .swiftSource(try String(contentsOf: fileURL, encoding: .utf8))
} catch {
state = .failed(Self.describe(error))
}
return
}
do {
let data = try Data(contentsOf: fileURL)
let document = try JSONDecoder().decode(DSLDocument.self, from: data)
state = .json(document)
} catch {
state = .failed(Self.describe(error))

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 Blocking file I/O on the main actor

Both String(contentsOf:encoding:) (line 57) and Data(contentsOf:) (line 64) are synchronous, blocking file reads. Because reload() is @MainActor (inherited from the class), every load and hot-reload blocks the main thread. A slow filesystem or large sidebar file will jank or freeze the UI. Move the file read into a Task { @concurrent in ... } and deliver the result back to main with await MainActor.run { ... }.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

case let .swiftSource(source):
// Interpret here (not in the model) so the view re-evaluates against
// `dataContext` whenever live workspace state changes.
if let node = SwiftViewInterpreter().evaluate(source, state: dataContext) {

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 Full Swift parse in SwiftUI body

SwiftViewInterpreter().evaluate(source, state: dataContext) calls Parser.parse(source:) (a full swift-syntax parse) and tree-walks the result on every SwiftUI render. Combined with the enclosing 1-second TimelineView, this runs an expensive CPU pass on the main thread every tick. The interpreter result should be cached in the model keyed on source, so re-evaluation only happens when the source changes, not on every clock tick.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

Comment thread Sources/ContentView.swift
Comment on lines +11038 to +11047
// Periodic tick so the custom sidebar re-renders live (clock,
// countdowns, and refreshed workspace/data context), mirroring the
// default sidebar's TimelineView. No banned timers involved.
TimelineView(.periodic(from: .now, by: 1)) { timeline in
CustomSidebarView(
fileURL: customSidebarURL,
dataContext: customSidebarDataContext(now: timeline.date),
dispatch: makeCmuxSidebarActionDispatch()
)
.id(customSidebarURL)

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 New SidebarActionDispatch closure created on every TimelineView tick

makeCmuxSidebarActionDispatch() is called inside the TimelineView body, allocating a new SidebarActionDispatch struct each second. SidebarActionDispatch has no Equatable conformance, so SwiftUI cannot elide the environment propagation — every button and tappable node in the entire RenderNodeView tree will be re-evaluated every second. Move the dispatch creation to a @State or stored property stable across ticks.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

Comment thread Sources/ContentView.swift Outdated
Comment on lines +10455 to +10468
// persisted selection via an observer) routes correctly on the first render
// pass and restores the user's choice if extensions are re-enabled.
private var effectiveExtensionSidebarProviderId: String {
extensionsExperimentalEnabled
? selectedExtensionSidebarProviderId
: CmuxExtensionSidebarSelection.defaultProviderId
let selected = selectedExtensionSidebarProviderId
if selected.hasPrefix(CmuxExtensionSidebarSelection.customSidebarProviderPrefix) {
// Touch the @LiveSetting so toggling the flag in Settings still
// re-renders, but decide with the synchronous UserDefaults read:
// on a sidebar remount @LiveSetting's initial value lags one tick,
// which would otherwise flash the default sidebar for a frame
// before swapping to the custom one.
_ = customSidebarsExperimentalEnabled
return CmuxExtensionSidebarSelection.customSidebarsEnabled
? selected
: CmuxExtensionSidebarSelection.defaultProviderId

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 Dual-read timing repair for the beta flag

effectiveExtensionSidebarProviderId touches customSidebarsExperimentalEnabled (the @LiveSetting subscription) for change-notification but reads the actual decision from CmuxExtensionSidebarSelection.customSidebarsEnabled (a direct UserDefaults read) to avoid a one-tick flash. The comment names the problem: @LiveSetting lags on sidebar remount. Using two sources of truth for the same boolean is a timing patch that leaves the state representation ambiguous.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (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!

Comment on lines +72 to +88
private static func describe(_ error: Error) -> String {
if let decoding = error as? DecodingError {
switch decoding {
case let .keyNotFound(key, ctx):
return "Missing key '\(key.stringValue)' at \(path(ctx))"
case let .typeMismatch(_, ctx):
return "Type mismatch at \(path(ctx)): \(ctx.debugDescription)"
case let .valueNotFound(_, ctx):
return "Missing value at \(path(ctx))"
case let .dataCorrupted(ctx):
return "Invalid JSON at \(path(ctx)): \(ctx.debugDescription)"
@unknown default:
return decoding.localizedDescription
}
}
return (error as NSError).localizedDescription
}

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 User-facing error messages expose internal debugDescription

ctx.debugDescription in the DecodingError cases is an internal diagnostic string from the Foundation decoder that can contain type names, property-wrapper internals, and key paths not meaningful to a sidebar author. The failed state surfaces this directly via errorView(message). Replace ctx.debugDescription with a human-readable summary derived only from ctx.codingPath and the already-extracted typed information.

Comment on lines +45 to +88
scrollWrap(
Text(String(localized: "sidebar.custom.missing", defaultValue: "Sidebar file is empty or missing."))
.font(.caption)
.foregroundStyle(.secondary)
)
case let .json(document):
scrollWrap(DSLSidebarRenderer(node: document.root) { _ in })
case let .swiftSource(source):
// Interpret here (not in the model) so the view re-evaluates against
// `dataContext` whenever live workspace state changes.
if let node = SwiftViewInterpreter().evaluate(source, state: dataContext) {
// A split root owns its own per-column scrolling and fills the
// sidebar height, so it is not wrapped in the outer ScrollView.
if node.kind == .hsplit {
RenderNodeView(node: node)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
scrollWrap(RenderNodeView(node: node))
}
} else {
scrollWrap(errorView(String(localized: "sidebar.custom.noView", defaultValue: "No supported SwiftUI view found.")))
}
case let .failed(message):
scrollWrap(errorView(message))
}
}

/// Wraps non-split content in the scrolling container with host-owned
/// outer insets (authors control inner spacing).
private func scrollWrap(_ view: some View) -> some View {
ScrollView {
view
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.horizontal, 12)
.padding(.top, 8)
.padding(.bottom, 16)
}
}

private func errorView(_ message: String) -> some View {
VStack(alignment: .leading, spacing: 6) {
Label(
String(localized: "sidebar.custom.error", defaultValue: "Sidebar error"),
systemImage: "exclamationmark.triangle.fill"

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 New user-facing strings missing from Localizable.xcstrings

CustomSidebarView introduces sidebar.custom.missing, sidebar.custom.noView, and sidebar.custom.error; ContentView adds sidebar.provider.custom.subtitle. The existing Localizable.xcstrings ships translations for English, Japanese, and Korean; none of these keys appear in the diff. All four keys need entries for every supported locale before ship.

Rule Used: Flag production user-facing text that is not fully... (source)

Comment thread Sources/ContentView.swift
Comment on lines +10050 to +10072
/// One provider descriptor per `<name>.swift`/`<name>.json` file in the
/// sidebars directory (`.swift` preferred when both exist), titled by the
/// file's base name.
static var customSidebarDescriptors: [CmuxExtensionSidebarProviderDescriptor] {
guard let entries = try? FileManager.default.contentsOfDirectory(
at: customSidebarsDirectory,
includingPropertiesForKeys: nil
) else { return [] }
var extensionByName: [String: String] = [:]
for url in entries {
let ext = url.pathExtension.lowercased()
guard ext == "swift" || ext == "json" else { continue }
let name = url.deletingPathExtension().lastPathComponent
if extensionByName[name] == "swift" { continue }
extensionByName[name] = ext
}
return extensionByName.keys.sorted().map { name in
CmuxExtensionSidebarProviderDescriptor(
id: customSidebarProviderPrefix + name,
title: CmuxExtensionLocalizedText(key: "sidebar.provider.custom.\(name)", defaultValue: name),
subtitle: CmuxExtensionLocalizedText(
key: "sidebar.provider.custom.subtitle",
defaultValue: String(localized: "sidebar.provider.custom.subtitle", defaultValue: "Custom sidebar")

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 Synchronous filesystem scan on the main actor in customSidebarDescriptors

FileManager.default.contentsOfDirectory(at:includingPropertiesForKeys:) is a blocking syscall called from the @MainActor descriptors property on every picker refresh. On a slow filesystem (remote home, encrypted volume) this stalls the sidebar toggle menu. The directory listing should be cached or fetched off-main.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

@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: 13

🤖 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 `@docs/custom-sidebar-gap-report.md`:
- Around line 50-52: Update the report text to correct inaccurate claims:
explicitly state that clock is already provided in the evaluation context and
that the host already performs periodic re-renders via TimelineView; then
clarify the remaining gaps are event-driven context subscriptions and
interpreter lifecycle hooks (e.g. .onAppear) and the larger work needed to add a
host-side provider protocol for named context objects (the evaluation
environment, typed read-only bridge, and per-source refresh wiring). Replace the
blanket “missing” statements at the affected sections (lines referencing
workspaces/workspaceCount/selectedTitle, clock, and TimelineView) with wording
that distinguishes “exists today” (clock + periodic tick via TimelineView)
versus “remaining work” (event-driven subscriptions and interpreter-level
lifecycle hooks and the host-side provider protocol).

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift`:
- Around line 119-130: The integer-division and modulo branches in
ExpressionEvaluator.swift (the switch handling op in the evaluator) currently
use Int(l) / Int(r) and Int(l) % Int(r) which will trap on a zero divisor;
update the "/" and "%" cases (when bothInt is true) to guard the divisor before
converting/operating (e.g., check Int(r) or r != 0) and return nil when the
divisor is zero so the evaluator safely fails instead of crashing; keep the
existing .double path unchanged and optionally note overflow risks for Int
conversions but do not change other operators.
- Around line 181-182: The "sorted" branch currently ignores a detected
comparator closure and always calls sortedScalars(values), producing incorrect
ascending order when a by: closure is supplied; update the case "sorted"
handling to detect the presence of a two-argument comparator closure (the same
closure detected earlier), and if present evaluate it as a two-arg predicate by
calling the closure with pairwise scalar values and using its truthiness to
drive the sort order, otherwise fallback to sortedScalars(values);
alternatively, if implementing the comparator is complex, explicitly return nil
when a by: closure is provided so the caller can skip rendering rather than
silently honoring ascending sort—update the return at case "sorted" (currently
return .array(sortedScalars(values))) accordingly.
- Around line 98-109: The current code eagerly evaluates both operands with
guard let lhs = eval(node.leftOperand, env), let rhs = eval(node.rightOperand,
env) which breaks short-circuiting for "&&" and "||"; change eval flow in
ExpressionEvaluator (the eval call handling BinaryExpression
node.leftOperand/node.rightOperand) to evaluate lhs first, then for "&&" return
.bool(false) if lhs.isFalsy (or truthiness check) without evaluating rhs, and
for "||" return .bool(true) if lhs.isTruthy without evaluating rhs; only call
eval(node.rightOperand, env) when the operator requires the right-hand side,
while preserving existing behavior for range ("..", "..."), equality ("==","!=")
and other operators (evaluate rhs and handle nils as before).

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift`:
- Around line 6-123: This file contains multiple public API types and should be
split so each public type lives in its own file: create ModifierArg.swift
(containing ModifierArg), RenderModifier.swift (RenderModifier),
ActionCommand.swift (ActionCommand), ButtonAction.swift (ButtonAction) and keep
RenderNode.swift only for RenderNode (and its nested Kind). Move the
corresponding declarations and their initializers/derived members into those
files, update imports/module visibility if needed, and ensure references to
symbols like ModifierArg, RenderModifier, ActionCommand, ButtonAction and
RenderNode.Kind continue to compile.

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`:
- Around line 37-48: The evaluator lacks a recursion-depth guard; thread a depth
counter (e.g., currentDepth:Int) from evaluate(_:) into evalView(_:env:depth:),
then into evalItems(_:env:depth:), and decrement/propagate into
evalFor(_:env:depth:), evalIf(_:env:depth:), and evalForEach(_:env:depth:);
enforce a configurable MAX_DEPTH constant and when exceeded return nil (or a
controlled failure) so evaluation stops gracefully; update evaluate to start
depth at 0 and ensure all recursive calls pass depth+1, and add a unit test that
builds deeply nested if/else-if chains to assert it does not crash and returns
the expected graceful failure.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift`:
- Around line 33-35: The current code always renders Button(node.title ?? "")
which produces an unlabeled, interactive control when node.title is nil/blank;
update DSLSidebarRenderer to only render the Button when the title is non-empty
(e.g., guard let title = node.title?.trimmingCharacters(in:
.whitespacesAndNewlines), !title.isEmpty { Button(title) { if let action =
node.action { onAction(action) } } }) or alternatively supply a deterministic
fallback label (e.g., "Untitled Action") instead of an empty string; ensure you
reference Button, node.title, node.action, and onAction when making the change.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift`:
- Around line 56-69: The cursor can remain as NSCursor.resizeLeftRight if the
drag ends outside hover bounds; update the DragGesture .onEnded handler in
ResizableHSplit (the DragGesture attached to the divider) to explicitly reset
the cursor by calling NSCursor.arrow.set() and clear dragStartFraction there
(and consider doing the same in any cancellation path), ensuring the cursor is
always restored after a drag ends; reference the DragGesture's .onChanged,
.onEnded, dragStartFraction and fraction to place the change.
- Line 13: The AppStorage key used for the shared split fraction
("cmux.customSidebar.splitFraction") makes the stored value global across all
sidebars; update the storage key in ResizableHSplit (the `@AppStorage-backed`
private var fraction: Double) to be scoped per-sidebar (for example by
incorporating a sidebar identifier like sidebarName or providerID into the key,
e.g. "cmux.customSidebar.\(sidebarID).splitFraction") so each sidebar instance
persists its own splitFraction; ensure you pass or compute the sidebarID into
ResizableHSplit (constructor/initializer or environment) and use that when
building the AppStorage key.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift`:
- Around line 72-88: The describe(_:) function returns hardcoded English error
messages; update each user-facing branch (cases .keyNotFound, .typeMismatch,
.valueNotFound, .dataCorrupted and the default decoding.localizedDescription
fallback) to use localized strings via String(localized:defaultValue:) (or the
app’s existing localization helper) and include clear translation keys/strings
in the string catalog for each message and supported locale so
CustomSidebarView.errorView renders localized text.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift`:
- Around line 50-51: The .json branch in CustomSidebarView uses
DSLSidebarRenderer(node: document.root) { _ in } which discards DSLAction
events; change the closure to forward DSLAction events through the same
dispatcher used by the .swiftSource path by mapping DSLAction → the
corresponding cmux action and calling sidebarActionDispatch with that mapped
action (or reuse the existing mapping helper if one exists); update the
DSLSidebarRenderer initializer call in the .json case to provide a closure like
{ action in let cmuxAction = mapDSLActionToCmux(action);
sidebarActionDispatch(cmuxAction) } so JSON-authored interactive nodes trigger
the shared dispatcher, preserving DSLNode.action handling.
- Around line 52-66: CustomSidebarView is re-parsing Swift source on every body
render because it calls SwiftViewInterpreter().evaluate(source:state:) directly;
change the flow to parse once when the file is reloaded and reuse the parsed AST
during rendering. Update CustomSidebarModel.reload() to store a parsed
representation (result of Parser.parse(source:) and
OperatorTable.standardOperators.foldAll(...)) instead of only the raw String, or
add new SwiftViewInterpreter APIs parse(source:) -> ParsedTree and
evaluate(parsedTree:state:) and use those in CustomSidebarView (replace
evaluate(source:state:) with evaluate(parsedTree:state:) or pass the cached
parsed tree from CustomSidebarModel). Ensure CustomSidebarView checks the cached
parsed tree for nil and falls back to errorView when absent.

In `@Sources/ContentView.swift`:
- Around line 10478-10517: customSidebarDataContext is rebuilding a full
workspaces→panes→tabs SwiftValue snapshot every second (via TimelineView) which
will be expensive at scale; change it to return a cached snapshot and only
rebuild when workspace/tab structure changes or on a throttled interval: add a
stored cache (e.g., cachedSidebarSnapshot and cachedSnapshotVersion/timestamp)
used by customSidebarDataContext, compute and store the heavy workspaces array
(the map over tabManager.tabs and the nested bonsplitController.tabs loop) into
that cache when you observe mutations (subscribe to tabManager tabs changes,
workspace additions/removals, bonsplitController pane/tab changes or expose a
workspaceChanged() hook) or when a configurable throttle timer elapses (e.g.,
5s), while keeping the clock object rebuilt every second; locate and modify
customSidebarDataContext and the call sites interacting with
tabManager/TimelineView to read from the cache and trigger invalidation on the
identified mutation points (tabManager.tabs, Workspace.bonsplitController
tabs/panes, Workspace.customTitle/currentDirectory updates).
🪄 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: c78bfbb5-a332-4f3a-9a4a-6abb69945c72

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1c6d3 and a85403d.

📒 Files selected for processing (29)
  • Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
  • Packages/CmuxSwiftRender/Package.resolved
  • Packages/CmuxSwiftRender/Package.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftValue.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
  • Packages/CmuxSwiftRenderUI/Package.resolved
  • Packages/CmuxSwiftRenderUI/Package.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Action/SidebarActionDispatch.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLAction.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLDocument.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderStyle.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift
  • Sources/ContentView.swift
  • Sources/DSLSidebarPlayground.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • docs/custom-sidebar-gap-report.md

Comment thread docs/custom-sidebar-gap-report.md Outdated
Comment on lines +50 to +52
- Rationale: Every single persona invented read-only context arrays beyond the documented workspaces/tabs. The interpreter only exposes workspaces/workspaceCount/selectedTitle, so almost every sidebar's primary content (alerts, PRs, services, GPUs, runs, notes, ports, git state) is fictional today. This is the universal blocker and the highest-leverage investment: most other gaps are cosmetic on top of data that does not exist.
- Example: `services = [{ id, name, port, up, healthy }]; git = { branch, dirty, ahead }; pulls = [{ number, ciState, needsMyReview }]; gpus = [{ index, utilPct, vramUsedGB, tempC }]; ports = [{ port, owner }]; clock.time`
- Effort: large — define a host-side provider protocol that populates named context objects (shelling out to git/lsof/gh/nvidia-smi/health probes) and inject them into the evaluation environment exactly like workspaces; needs a typed read-only value bridge and per-source refresh wiring.

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 | ⚡ Quick win

Correct roadmap claims that contradict current implementation.

This report currently describes two already-shipped capabilities as missing:

  1. data context excludes clock (it is already provided), and
  2. sidebar refresh is a static snapshot (the host already re-renders periodically via TimelineView).

Please update these sections to distinguish what exists today (periodic tick + clock in context) from the remaining gap (e.g., event-driven context subscriptions and interpreter-level lifecycle hooks like .onAppear).

Also applies to: 69-73, 151-154

🤖 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 `@docs/custom-sidebar-gap-report.md` around lines 50 - 52, Update the report
text to correct inaccurate claims: explicitly state that clock is already
provided in the evaluation context and that the host already performs periodic
re-renders via TimelineView; then clarify the remaining gaps are event-driven
context subscriptions and interpreter lifecycle hooks (e.g. .onAppear) and the
larger work needed to add a host-side provider protocol for named context
objects (the evaluation environment, typed read-only bridge, and per-source
refresh wiring). Replace the blanket “missing” statements at the affected
sections (lines referencing workspaces/workspaceCount/selectedTitle, clock, and
TimelineView) with wording that distinguishes “exists today” (clock + periodic
tick via TimelineView) versus “remaining work” (event-driven subscriptions and
interpreter-level lifecycle hooks and the host-side provider protocol).

Comment thread Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift Outdated
Comment thread Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift Outdated
Comment thread Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift Outdated
Comment on lines +56 to +69
.onHover { inside in
if inside { NSCursor.resizeLeftRight.set() } else { NSCursor.arrow.set() }
}
.gesture(
DragGesture()
.onChanged { value in
let start = dragStartFraction ?? fraction
if dragStartFraction == nil { dragStartFraction = start }
let newLeading = CGFloat(start) * total + value.translation.width
let lower = Double(minColumnWidth / total)
fraction = min(max(Double(newLeading / total), lower), 1 - lower)
}
.onEnded { _ in dragStartFraction = nil }
)

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

Cursor may not reset if hover ends during drag.

The onHover handler sets NSCursor.resizeLeftRight on enter and NSCursor.arrow on exit, but if the user drags outside the divider bounds, onHover won't fire an exit event during the active gesture. The cursor can remain resizeLeftRight after the drag ends. Consider resetting the cursor in the .onEnded handler:

Proposed fix
             .onEnded { _ in
+                NSCursor.arrow.set()
                 dragStartFraction = nil
             }
📝 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
.onHover { inside in
if inside { NSCursor.resizeLeftRight.set() } else { NSCursor.arrow.set() }
}
.gesture(
DragGesture()
.onChanged { value in
let start = dragStartFraction ?? fraction
if dragStartFraction == nil { dragStartFraction = start }
let newLeading = CGFloat(start) * total + value.translation.width
let lower = Double(minColumnWidth / total)
fraction = min(max(Double(newLeading / total), lower), 1 - lower)
}
.onEnded { _ in dragStartFraction = nil }
)
.onHover { inside in
if inside { NSCursor.resizeLeftRight.set() } else { NSCursor.arrow.set() }
}
.gesture(
DragGesture()
.onChanged { value in
let start = dragStartFraction ?? fraction
if dragStartFraction == nil { dragStartFraction = start }
let newLeading = CGFloat(start) * total + value.translation.width
let lower = Double(minColumnWidth / total)
fraction = min(max(Double(newLeading / total), lower), 1 - lower)
}
.onEnded { _ in
NSCursor.arrow.set()
dragStartFraction = nil
}
)
🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift`
around lines 56 - 69, The cursor can remain as NSCursor.resizeLeftRight if the
drag ends outside hover bounds; update the DragGesture .onEnded handler in
ResizableHSplit (the DragGesture attached to the divider) to explicitly reset
the cursor by calling NSCursor.arrow.set() and clear dragStartFraction there
(and consider doing the same in any cancellation path), ensuring the cursor is
always restored after a drag ends; reference the DragGesture's .onChanged,
.onEnded, dragStartFraction and fraction to place the change.

Comment on lines +72 to +88
private static func describe(_ error: Error) -> String {
if let decoding = error as? DecodingError {
switch decoding {
case let .keyNotFound(key, ctx):
return "Missing key '\(key.stringValue)' at \(path(ctx))"
case let .typeMismatch(_, ctx):
return "Type mismatch at \(path(ctx)): \(ctx.debugDescription)"
case let .valueNotFound(_, ctx):
return "Missing value at \(path(ctx))"
case let .dataCorrupted(ctx):
return "Invalid JSON at \(path(ctx)): \(ctx.debugDescription)"
@unknown default:
return decoding.localizedDescription
}
}
return (error as NSError).localizedDescription
}

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

Localize the user-facing decoding-error messages.

describe(_:) returns hardcoded English ("Missing key …", "Type mismatch at …", "Invalid JSON at …") that flows into state.failed and is rendered verbatim by CustomSidebarView.errorView. The rest of the sidebar UI already uses String(localized:), so this is partial localization.

As per coding guidelines: "Swift text must use localized APIs with matching translated string-catalog entries."

🌐 Suggested direction
-            case let .keyNotFound(key, ctx):
-                return "Missing key '\(key.stringValue)' at \(path(ctx))"
+            case let .keyNotFound(key, ctx):
+                return String(
+                    localized: "sidebar.custom.error.missingKey",
+                    defaultValue: "Missing key '\(key.stringValue)' at \(path(ctx))"
+                )

Apply the same String(localized:defaultValue:) treatment to the remaining cases and add matching string-catalog entries for every supported locale.

🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift`
around lines 72 - 88, The describe(_:) function returns hardcoded English error
messages; update each user-facing branch (cases .keyNotFound, .typeMismatch,
.valueNotFound, .dataCorrupted and the default decoding.localizedDescription
fallback) to use localized strings via String(localized:defaultValue:) (or the
app’s existing localization helper) and include clear translation keys/strings
in the string catalog for each message and supported locale so
CustomSidebarView.errorView renders localized text.

Comment thread Sources/ContentView.swift
Comment on lines +10478 to 10517
private func customSidebarDataContext(now: Date) -> [String: SwiftValue] {
let selectedId = tabManager.selectedTabId
let workspaces: [SwiftValue] = tabManager.tabs.map { workspace in
let focusedPanelId = workspace.focusedPanelId
var tabs: [SwiftValue] = []
for paneId in workspace.bonsplitController.allPaneIds {
for tab in workspace.bonsplitController.tabs(inPane: paneId) {
guard let panelId = workspace.panelIdFromSurfaceId(tab.id) else { continue }
tabs.append(.object([
"id": .string(panelId.uuidString),
"title": .string(tab.title),
"focused": .bool(panelId == focusedPanelId),
]))
}
}
return .object([
"id": .string(workspace.id.uuidString),
"title": .string(workspace.customTitle ?? workspace.title),
"selected": .bool(workspace.id == selectedId),
"directory": .string(workspace.currentDirectory),
"tabs": .array(tabs),
])
}
let selectedWorkspace = tabManager.tabs.first { $0.id == selectedId }
let c = Calendar.current.dateComponents([.hour, .minute, .second, .weekday], from: now)
let hour = c.hour ?? 0, minute = c.minute ?? 0, second = c.second ?? 0
let clock: SwiftValue = .object([
"time": .string(String(format: "%02d:%02d:%02d", hour, minute, second)),
"hour": .int(hour),
"minute": .int(minute),
"second": .int(second),
"epoch": .int(Int(now.timeIntervalSince1970)),
])
return [
"workspaces": .array(workspaces),
"workspaceCount": .int(tabManager.tabs.count),
"selectedTitle": .string(selectedWorkspace?.customTitle ?? selectedWorkspace?.title ?? ""),
"clock": clock,
]
}

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 | 💤 Low value

Consider caching or throttling the workspace-to-SwiftValue transformation.

The customSidebarDataContext performs triple-nested iteration (workspaces → panes → tabs) and runs every second via TimelineView. While this is acceptable for typical usage behind a beta flag, at scale (~1000 workspaces) this could become expensive.

For the beta phase this is fine, but if custom sidebars graduate to GA, consider:

  • Caching the computed snapshot and invalidating only on workspace/tab mutations
  • Throttling the data context rebuild (e.g., 5-second interval) separately from clock updates

The snapshot boundary is correctly respected—SwiftValue objects are built rather than passing store references.

🤖 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 10478 - 10517,
customSidebarDataContext is rebuilding a full workspaces→panes→tabs SwiftValue
snapshot every second (via TimelineView) which will be expensive at scale;
change it to return a cached snapshot and only rebuild when workspace/tab
structure changes or on a throttled interval: add a stored cache (e.g.,
cachedSidebarSnapshot and cachedSnapshotVersion/timestamp) used by
customSidebarDataContext, compute and store the heavy workspaces array (the map
over tabManager.tabs and the nested bonsplitController.tabs loop) into that
cache when you observe mutations (subscribe to tabManager tabs changes,
workspace additions/removals, bonsplitController pane/tab changes or expose a
workspaceChanged() hook) or when a configurable throttle timer elapses (e.g.,
5s), while keeping the clock object rebuilt every second; locate and modify
customSidebarDataContext and the call sites interacting with
tabManager/TimelineView to read from the cache and trigger invalidation on the
identified mutation points (tabManager.tabs, Workspace.bonsplitController
tabs/panes, Workspace.customTitle/currentDirectory updates).

@cubic-dev-ai cubic-dev-ai 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.

11 issues found

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift:4">
P3: Three ViewModifier types in one file violates the repo's one-major-type-per-file convention. Split `OptionalForeground`, `OptionalPadding`, and `OptionalBackground` into separate files for consistency with the codebase convention.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift:4">
P1: Missing fallback for unknown raw values during JSON decoding — user-authored sidebar files with an unrecognized node kind (e.g., a typo like `"vstak"` or a future plugin authoring a new kind before this code adds it) will throw a hard `DecodingError.dataCorrupted` and fail to load the entire sidebar. Add a custom `Decodable` that maps unknown strings to a fallback `.unknown` case.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift:66">
P2: Drag gesture writes to `@AppStorage` on every `.onChanged` event (~60 writes/sec), causing unnecessary `UserDefaults` synchronization and potential UI jank. Use a local `@State` for the transient drag value and persist only on `.onEnded`.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:123">
P0: Integer division and modulo by zero crash the interpreter with a fatal runtime error. User-authored sidebar expressions like `5 / 0` or `x % 0` (where x is an integer variable resolving to 0) cause an unhandled Swift division-by-zero trap, crashing the host process.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift:9">
P2: Auto-synthesized `Equatable` includes `id` (`UUID()`) in the comparison, making equality checks always return `false` for distinct instances even when semantically identical. The doc comment explicitly says `id` is "Not decoded; a stable identity per decoded node" — it is a runtime identity marker, not part of the value. Including it in `Equatable` means `==` can never be true for two separately-initialized or decoded nodes with identical content, breaking any code that relies on value equality (SwiftUI `EquatableView`, test assertions, collection diffing).</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift:51">
P2: Uses `FileManager.default` directly instead of an injected dependency, making the model untestable without real file I/O. The repository guidance (`CLAUDE.md`) requires package APIs to be testable without launching the app or relying on `FileManager.default`. The `reload()` method also reads files via `Data(contentsOf:)` and `String(contentsOf:encoding:)` which bypass any controllable FileManager.</violation>
</file>

<file name="cmux.xcodeproj/project.pbxproj">

<violation number="1" location="cmux.xcodeproj/project.pbxproj:1932">
P2: CmuxSwiftRenderUI (a SwiftUI+AppKit UI-rendering package) is added as a dependency of the cmux-cli command-line tool target. Every source file in this package imports SwiftUI, and ResizableHSplit.swift imports AppKit. The cmux-cli target (product-type tool) has no UI runtime and cannot meaningfully use SwiftUI View types, which will unnecessarily link SwiftUI/AppKit frameworks into the CLI binary and create a maintenance burden.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift:51">
P2: JSON sidebar action handler silently discards all button actions. The `onAction:` closure is `{ _ in }`, so any interactive element (button, tap) in a `.json` sidebar produces zero side effects — the buttons render but do nothing when tapped. Meanwhile, interpreted Swift sidebars correctly dispatch actions through the environment's `sidebarActionDispatch`.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift:33">
P2: Button node does not apply `resolvedFont`, unlike `.text` and `.image` cases. When a JSON DSL button specifies `font` or `size`, those properties are silently ignored, while the same properties work correctly on text and image nodes. This means the button's title text will always appear at the system default font regardless of the authored style.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:75">
P1: `Button("title", action: { … })` drops the title string because the "label form" branch (triggered by finding an `action:` labeled closure argument) returns a node with no text and an empty children array, ignoring the unlabeled first argument.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P3: `closureParameterName(_:)` is duplicated verbatim in both `SwiftViewInterpreter` and `ExpressionEvaluator`, each as a private method doing the same closure-signature parameter-name extraction.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift Outdated
import Foundation

/// The kind of a ``DSLNode`` in the declarative JSON sidebar format.
enum DSLNodeKind: String, Codable, Sendable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Missing fallback for unknown raw values during JSON decoding — user-authored sidebar files with an unrecognized node kind (e.g., a typo like "vstak" or a future plugin authoring a new kind before this code adds it) will throw a hard DecodingError.dataCorrupted and fail to load the entire sidebar. Add a custom Decodable that maps unknown strings to a fallback .unknown case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift, line 4:

<comment>Missing fallback for unknown raw values during JSON decoding — user-authored sidebar files with an unrecognized node kind (e.g., a typo like `"vstak"` or a future plugin authoring a new kind before this code adds it) will throw a hard `DecodingError.dataCorrupted` and fail to load the entire sidebar. Add a custom `Decodable` that maps unknown strings to a fallback `.unknown` case.</comment>

<file context>
@@ -0,0 +1,13 @@
+import Foundation
+
+/// The kind of a ``DSLNode`` in the declarative JSON sidebar format.
+enum DSLNodeKind: String, Codable, Sendable {
+    case vstack
+    case hstack
</file context>

switch ref.baseName.text {
case "Text":
return RenderNode(kind: .text, text: stringArgument(call.arguments, env) ?? "")
case "Button":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Button("title", action: { … }) drops the title string because the "label form" branch (triggered by finding an action: labeled closure argument) returns a node with no text and an empty children array, ignoring the unlabeled first argument.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift, line 75:

<comment>`Button("title", action: { … })` drops the title string because the "label form" branch (triggered by finding an `action:` labeled closure argument) returns a node with no text and an empty children array, ignoring the unlabeled first argument.</comment>

<file context>
@@ -0,0 +1,308 @@
+        switch ref.baseName.text {
+        case "Text":
+            return RenderNode(kind: .text, text: stringArgument(call.arguments, env) ?? "")
+        case "Button":
+            // Label form: `Button(action: { … }) { labelView }` — the action
+            // is the `action:` closure and the trailing closure is a rich
</file context>

if dragStartFraction == nil { dragStartFraction = start }
let newLeading = CGFloat(start) * total + value.translation.width
let lower = Double(minColumnWidth / total)
fraction = min(max(Double(newLeading / total), lower), 1 - lower)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Drag gesture writes to @AppStorage on every .onChanged event (~60 writes/sec), causing unnecessary UserDefaults synchronization and potential UI jank. Use a local @State for the transient drag value and persist only on .onEnded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift, line 66:

<comment>Drag gesture writes to `@AppStorage` on every `.onChanged` event (~60 writes/sec), causing unnecessary `UserDefaults` synchronization and potential UI jank. Use a local `@State` for the transient drag value and persist only on `.onEnded`.</comment>

<file context>
@@ -0,0 +1,71 @@
+                    if dragStartFraction == nil { dragStartFraction = start }
+                    let newLeading = CGFloat(start) * total + value.translation.width
+                    let lower = Double(minColumnWidth / total)
+                    fraction = min(max(Double(newLeading / total), lower), 1 - lower)
+                }
+                .onEnded { _ in dragStartFraction = nil }
</file context>

/// JSON trivial to author and the renderer a single recursive `switch`.
struct DSLNode: Codable, Equatable, Sendable, Identifiable {
/// Not decoded; a stable identity per decoded node for `ForEach`.
let id = UUID()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Auto-synthesized Equatable includes id (UUID()) in the comparison, making equality checks always return false for distinct instances even when semantically identical. The doc comment explicitly says id is "Not decoded; a stable identity per decoded node" — it is a runtime identity marker, not part of the value. Including it in Equatable means == can never be true for two separately-initialized or decoded nodes with identical content, breaking any code that relies on value equality (SwiftUI EquatableView, test assertions, collection diffing).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift, line 9:

<comment>Auto-synthesized `Equatable` includes `id` (`UUID()`) in the comparison, making equality checks always return `false` for distinct instances even when semantically identical. The doc comment explicitly says `id` is "Not decoded; a stable identity per decoded node" — it is a runtime identity marker, not part of the value. Including it in `Equatable` means `==` can never be true for two separately-initialized or decoded nodes with identical content, breaking any code that relies on value equality (SwiftUI `EquatableView`, test assertions, collection diffing).</comment>

<file context>
@@ -0,0 +1,33 @@
+/// JSON trivial to author and the renderer a single recursive `switch`.
+struct DSLNode: Codable, Equatable, Sendable, Identifiable {
+    /// Not decoded; a stable identity per decoded node for `ForEach`.
+    let id = UUID()
+    var type: DSLNodeKind
+    var children: [DSLNode]?
</file context>

A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */,
A5354305A5354305A5354305 /* CmuxSocketControl */,
C5A1FED000000000000000C3 /* CmuxSwiftRender */,
C5A1FED100000000000000D3 /* CmuxSwiftRenderUI */,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: CmuxSwiftRenderUI (a SwiftUI+AppKit UI-rendering package) is added as a dependency of the cmux-cli command-line tool target. Every source file in this package imports SwiftUI, and ResizableHSplit.swift imports AppKit. The cmux-cli target (product-type tool) has no UI runtime and cannot meaningfully use SwiftUI View types, which will unnecessarily link SwiftUI/AppKit frameworks into the CLI binary and create a maintenance burden.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmux.xcodeproj/project.pbxproj, line 1932:

<comment>CmuxSwiftRenderUI (a SwiftUI+AppKit UI-rendering package) is added as a dependency of the cmux-cli command-line tool target. Every source file in this package imports SwiftUI, and ResizableHSplit.swift imports AppKit. The cmux-cli target (product-type tool) has no UI runtime and cannot meaningfully use SwiftUI View types, which will unnecessarily link SwiftUI/AppKit frameworks into the CLI binary and create a maintenance burden.</comment>

<file context>
@@ -1917,6 +1928,8 @@
 				A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */,
 				A5354305A5354305A5354305 /* CmuxSocketControl */,
+				C5A1FED000000000000000C3 /* CmuxSwiftRender */,
+				C5A1FED100000000000000D3 /* CmuxSwiftRenderUI */,
 				C8000302C8000302C8000302 /* CmuxProcess */,
 				A500D013A1B2C3D4E5F60718 /* CMUXDebugLog */,
</file context>

.font(resolvedFont)
.fontWeight(dslFontWeight(node.weight))
case .button:
Button(node.title ?? "") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Button node does not apply resolvedFont, unlike .text and .image cases. When a JSON DSL button specifies font or size, those properties are silently ignored, while the same properties work correctly on text and image nodes. This means the button's title text will always appear at the system default font regardless of the authored style.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift, line 33:

<comment>Button node does not apply `resolvedFont`, unlike `.text` and `.image` cases. When a JSON DSL button specifies `font` or `size`, those properties are silently ignored, while the same properties work correctly on text and image nodes. This means the button's title text will always appear at the system default font regardless of the authored style.</comment>

<file context>
@@ -0,0 +1,64 @@
+                .font(resolvedFont)
+                .fontWeight(dslFontWeight(node.weight))
+        case .button:
+            Button(node.title ?? "") {
+                if let action = node.action { onAction(action) }
+            }
</file context>

import SwiftUI

/// Applies a foreground color only when one is resolved.
struct OptionalForeground: ViewModifier {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Three ViewModifier types in one file violates the repo's one-major-type-per-file convention. Split OptionalForeground, OptionalPadding, and OptionalBackground into separate files for consistency with the codebase convention.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift, line 4:

<comment>Three ViewModifier types in one file violates the repo's one-major-type-per-file convention. Split `OptionalForeground`, `OptionalPadding`, and `OptionalBackground` into separate files for consistency with the codebase convention.</comment>

<file context>
@@ -0,0 +1,29 @@
+import SwiftUI
+
+/// Applies a foreground color only when one is resolved.
+struct OptionalForeground: ViewModifier {
+    let color: Color?
+    func body(content: Content) -> some View {
</file context>

}

private func closureParameterName(_ closure: ClosureExprSyntax) -> String? {
guard let parameterClause = closure.signature?.parameterClause else { return nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: closureParameterName(_:) is duplicated verbatim in both SwiftViewInterpreter and ExpressionEvaluator, each as a private method doing the same closure-signature parameter-name extraction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift, line 178:

<comment>`closureParameterName(_:)` is duplicated verbatim in both `SwiftViewInterpreter` and `ExpressionEvaluator`, each as a private method doing the same closure-signature parameter-name extraction.</comment>

<file context>
@@ -0,0 +1,308 @@
+    }
+
+    private func closureParameterName(_ closure: ClosureExprSyntax) -> String? {
+        guard let parameterClause = closure.signature?.parameterClause else { return nil }
+        if case let .simpleInput(list) = parameterClause {
+            return list.first?.name.text
</file context>

New `Reorderable(data, move: "method") { item in row }` interpreter primitive
lowers to a `.reorderable` node carrying a ReorderSpec (method + id/index
params + ordered item ids). The renderer makes each row `.draggable` with the
item id and a `.dropDestination`; dropping dispatches the reorder command
(e.g. `workspace.reorder` with workspace_id + index), so for workspaces cmux
both reorders and persists, and the next auto-refresh tick reflects it. Numeric
params (the index) are coerced to numbers in the dispatch. demo.swift uses it
for the workspace list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +59 to +62
case let .range(lower, upper, inclusive):
let end = inclusive ? upper + 1 : upper
guard end >= lower else { return [] }
return (lower..<end).map(SwiftValue.int)

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 Integer overflow crash on inclusive ranges whose upper bound is Int.max. Swift traps on Int.max + 1 before the guard end >= lower check can run. A user-authored sidebar containing a large closed range (e.g. for i in 0...2147483647) will unconditionally crash the host process on every 1-second TimelineView tick.

Suggested change
case let .range(lower, upper, inclusive):
let end = inclusive ? upper + 1 : upper
guard end >= lower else { return [] }
return (lower..<end).map(SwiftValue.int)
case let .range(lower, upper, inclusive):
let end: Int
if inclusive {
guard upper < Int.max else { return [] }
end = upper + 1
} else {
end = upper
}
guard end >= lower else { return [] }
return (lower..<end).map(SwiftValue.int)

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

♻️ Duplicate comments (2)
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift (1)

6-150: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Split public types into per-type files.

This file declares six public API types (ModifierArg, RenderModifier, ActionCommand, ButtonAction, ReorderSpec, RenderNode). The repo convention requires each public type in its own file named after the type; only small closely-bound helpers may stay with a parent.

As per coding guidelines: "Extract one major type per file; each struct, class, enum, actor, or protocol that is part of a public API lives in its own file named after the type; small closely-bound helpers can stay with the parent".

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift` around
lines 6 - 150, The file currently declares multiple public API types; split each
public type into its own file named after the type (ModifierArg, RenderModifier,
ActionCommand, ButtonAction, ReorderSpec, RenderNode). For each type, create a
new source file containing the type declaration and its public init/props
(preserve Sendable/Equatable conformance and documentation comments), and remove
the duplicate declarations from the original file so only the intended parent
type remains; ensure imports and access levels remain unchanged and update any
module tests or references to use the moved types (search for ModifierArg,
RenderModifier, ActionCommand, ButtonAction, ReorderSpec, RenderNode to verify).
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift (1)

37-48: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add recursion-depth guard to prevent stack overflow.

The evaluator lacks a recursion-depth limit. evalItems recursively calls evalFor/evalIf/evalForEach, which call back into evalItems, and evalIf recursively handles else if—deeply nested user input can overflow the stack. Thread a depth counter through evaluate→evalView→evalItems→evalFor/evalIf/evalForEach and fail gracefully past a threshold.

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`
around lines 37 - 48, Add a recursion-depth guard by introducing a depth
parameter (or using an internal counter) that is threaded from evaluate into
evalView, evalItems and down into evalFor, evalIf and evalForEach, and enforce a
defined MAX_DEPTH constant; on each recursive entry increment the counter and
immediately fail gracefully (return nil or a distinct error RenderNode) when the
counter exceeds MAX_DEPTH. Update the signatures of evaluate, evalView,
evalItems, evalFor, evalIf and evalForEach to accept an optional depth:Int
(defaulting to 0 in the public evaluate), increment depth on recursive calls,
and perform the guard check at the start of each function so deeply nested user
input cannot overflow the stack. Ensure callers in the file use the new
parameter and keep the public API behavior by defaulting depth to 0.
🤖 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.

Duplicate comments:
In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift`:
- Around line 6-150: The file currently declares multiple public API types;
split each public type into its own file named after the type (ModifierArg,
RenderModifier, ActionCommand, ButtonAction, ReorderSpec, RenderNode). For each
type, create a new source file containing the type declaration and its public
init/props (preserve Sendable/Equatable conformance and documentation comments),
and remove the duplicate declarations from the original file so only the
intended parent type remains; ensure imports and access levels remain unchanged
and update any module tests or references to use the moved types (search for
ModifierArg, RenderModifier, ActionCommand, ButtonAction, ReorderSpec,
RenderNode to verify).

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`:
- Around line 37-48: Add a recursion-depth guard by introducing a depth
parameter (or using an internal counter) that is threaded from evaluate into
evalView, evalItems and down into evalFor, evalIf and evalForEach, and enforce a
defined MAX_DEPTH constant; on each recursive entry increment the counter and
immediately fail gracefully (return nil or a distinct error RenderNode) when the
counter exceeds MAX_DEPTH. Update the signatures of evaluate, evalView,
evalItems, evalFor, evalIf and evalForEach to accept an optional depth:Int
(defaulting to 0 in the public evaluate), increment depth on recursive calls,
and perform the guard check at the start of each function so deeply nested user
input cannot overflow the stack. Ensure callers in the file use the new
parameter and keep the public API behavior by defaulting depth to 0.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98e3ad16-153c-4094-83c9-a98603f246f0

📥 Commits

Reviewing files that changed from the base of the PR and between a85403d and e679921.

📒 Files selected for processing (6)
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift
  • Sources/DSLSidebarPlayground.swift

@cubic-dev-ai cubic-dev-ai 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.

4 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:18">
P1: ForEach uses index-based identity (`id: \.offset`) in a reorderable list, even though stable per-item identifiers are available via `spec.itemIds`. When a row is dragged to a new position and `rows` updates, SwiftUI identifies each row by its index (0, 1, 2...) rather than by its stable id. This prevents SwiftUI from tracking which view corresponds to which item across the reorder, losing view state, breaking transition animations, and making the reorder feel unresponsive or glitchy.

Use stable IDs from `spec.itemIds` as the ForEach identity, paired with each row, so SwiftUI correctly preserves view identity when items move.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:37">
P2: Validate `ReorderSpec` command/key strings (and `draggedId`) before dispatching `.cmux`; currently malformed or empty dynamic keys can produce invalid reorder commands.

(Based on your team's feedback about avoiding sentinel/missing-key patterns in cmux param construction.) [FEEDBACK_USED].</violation>
</file>

<file name="Sources/DSLSidebarPlayground.swift">

<violation number="1" location="Sources/DSLSidebarPlayground.swift:31">
P2: Unconditionally coercing all numeric-looking string params to Int can break commands that require string-typed values.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P1: Empty-string sentinel for missing item IDs in Reorderable: `item.member(idField)?.displayString ?? ""` inserts `""` into `itemIds` when the id field is absent, causing invalid reorder commands (e.g., `workspace_id: ""`) instead of a clear failure or graceful skip.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


var body: some View {
VStack(spacing: 0) {
ForEach(Array(rows.enumerated()), id: \.offset) { index, row in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: ForEach uses index-based identity (id: \.offset) in a reorderable list, even though stable per-item identifiers are available via spec.itemIds. When a row is dragged to a new position and rows updates, SwiftUI identifies each row by its index (0, 1, 2...) rather than by its stable id. This prevents SwiftUI from tracking which view corresponds to which item across the reorder, losing view state, breaking transition animations, and making the reorder feel unresponsive or glitchy.

Use stable IDs from spec.itemIds as the ForEach identity, paired with each row, so SwiftUI correctly preserves view identity when items move.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift, line 18:

<comment>ForEach uses index-based identity (`id: \.offset`) in a reorderable list, even though stable per-item identifiers are available via `spec.itemIds`. When a row is dragged to a new position and `rows` updates, SwiftUI identifies each row by its index (0, 1, 2...) rather than by its stable id. This prevents SwiftUI from tracking which view corresponds to which item across the reorder, losing view state, breaking transition animations, and making the reorder feel unresponsive or glitchy.

Use stable IDs from `spec.itemIds` as the ForEach identity, paired with each row, so SwiftUI correctly preserves view identity when items move.</comment>

<file context>
@@ -0,0 +1,41 @@
+
+    var body: some View {
+        VStack(spacing: 0) {
+            ForEach(Array(rows.enumerated()), id: \.offset) { index, row in
+                RenderNodeView(node: row)
+                    .draggable(itemId(index))
</file context>

scope.define("$0", item)
let rowNodes = evalItems(closure.statements, scope)
rows.append(rowNodes.count == 1 ? rowNodes[0] : RenderNode(kind: .vstack, children: rowNodes))
ids.append(item.member(idField)?.displayString ?? "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Empty-string sentinel for missing item IDs in Reorderable: item.member(idField)?.displayString ?? "" inserts "" into itemIds when the id field is absent, causing invalid reorder commands (e.g., workspace_id: "") instead of a clear failure or graceful skip.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift, line 178:

<comment>Empty-string sentinel for missing item IDs in Reorderable: `item.member(idField)?.displayString ?? ""` inserts `""` into `itemIds` when the id field is absent, causing invalid reorder commands (e.g., `workspace_id: ""`) instead of a clear failure or graceful skip.</comment>

<file context>
@@ -152,6 +154,41 @@ public struct SwiftViewInterpreter: Sendable {
+            scope.define("$0", item)
+            let rowNodes = evalItems(closure.statements, scope)
+            rows.append(rowNodes.count == 1 ? rowNodes[0] : RenderNode(kind: .vstack, children: rowNodes))
+            ids.append(item.member(idField)?.displayString ?? "")
+        }
+        return RenderNode(
</file context>

// like v2Int decode them.
var typed: [String: Any] = [:]
for (key, value) in params {
if let intValue = Int(value) { typed[key] = intValue } else { typed[key] = value }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Unconditionally coercing all numeric-looking string params to Int can break commands that require string-typed values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/DSLSidebarPlayground.swift, line 31:

<comment>Unconditionally coercing all numeric-looking string params to Int can break commands that require string-typed values.</comment>

<file context>
@@ -22,7 +22,16 @@ func makeCmuxSidebarActionDispatch() -> SidebarActionDispatch {
+                    // like v2Int decode them.
+                    var typed: [String: Any] = [:]
+                    for (key, value) in params {
+                        if let intValue = Int(value) { typed[key] = intValue } else { typed[key] = value }
+                    }
+                    payload["params"] = typed
</file context>
Suggested change
if let intValue = Int(value) { typed[key] = intValue } else { typed[key] = value }
if key == "index", let intValue = Int(value) {
typed[key] = intValue
} else {
typed[key] = value
}


private func reorder(_ draggedId: String, to index: Int) {
guard let spec else { return }
dispatch.run(ButtonAction(commands: [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Validate ReorderSpec command/key strings (and draggedId) before dispatching .cmux; currently malformed or empty dynamic keys can produce invalid reorder commands.

(Based on your team's feedback about avoiding sentinel/missing-key patterns in cmux param construction.) .

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift, line 37:

<comment>Validate `ReorderSpec` command/key strings (and `draggedId`) before dispatching `.cmux`; currently malformed or empty dynamic keys can produce invalid reorder commands.

(Based on your team's feedback about avoiding sentinel/missing-key patterns in cmux param construction.) .</comment>

<file context>
@@ -0,0 +1,41 @@
+
+    private func reorder(_ draggedId: String, to index: Int) {
+        guard let spec else { return }
+        dispatch.run(ButtonAction(commands: [
+            .cmux(method: spec.method, params: [spec.idParam: draggedId, spec.indexParam: String(index)]),
+        ]))
</file context>

…ents

New docs/custom-sidebars.md is the authoring contract for vibe-coding a custom
sidebar: file location + picker, the full supported interpreter subset (views,
modifiers, language, live data context, cmux() actions, Reorderable drag-and-
drop, auto-refresh), worked examples, and an agent-facing section on building a
clean, interactive, native result from a non-technical "make me a sidebar"
request (default to live data, make rows tappable, prefer Reorderable for
orderable lists, cap/lazy-load long lists). Surfaced via a new `cmux docs
sidebars` topic so an in-pane coding agent can discover and fetch it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLI/CMUXCLI+DocsSettings.swift (1)

154-155: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update docs command usage text to include sidebars.

sidebars is now a valid topic in docsReferences, but both the thrown usage string and docsUsage() still show the old topic set. This makes help/error output incorrect for users.

Proposed fix
-            throw CLIError(message: "Usage: cmux docs [settings|shortcuts|api|browser|agents|dock]")
+            throw CLIError(message: "Usage: cmux docs [settings|shortcuts|api|browser|agents|dock|sidebars]")
-        Usage: cmux docs [settings|shortcuts|api|browser|agents|dock]
+        Usage: cmux docs [settings|shortcuts|api|browser|agents|dock|sidebars]

Also applies to: 177-180

🤖 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 `@CLI/CMUXCLI`+DocsSettings.swift around lines 154 - 155, The thrown usage
string and the docsUsage() output are missing the new "sidebars" topic; update
the usage text in the docs command (the CLIError thrown in the docs handler) to
include "sidebars" and also add "sidebars" to the list returned/printed by
docsUsage() so both error/help output reflect the new valid topic; locate the
throw CLIError(message: "...") in the docs command handler and the docsUsage()
function and insert "sidebars" into their topic lists.
🤖 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.

Outside diff comments:
In `@CLI/CMUXCLI`+DocsSettings.swift:
- Around line 154-155: The thrown usage string and the docsUsage() output are
missing the new "sidebars" topic; update the usage text in the docs command (the
CLIError thrown in the docs handler) to include "sidebars" and also add
"sidebars" to the list returned/printed by docsUsage() so both error/help output
reflect the new valid topic; locate the throw CLIError(message: "...") in the
docs command handler and the docsUsage() function and insert "sidebars" into
their topic lists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e1c24437-3846-4733-a1c5-890bf309429b

📥 Commits

Reviewing files that changed from the base of the PR and between e679921 and 1c90f23.

📒 Files selected for processing (3)
  • CLI/CMUXCLI+DocsSettings.swift
  • CLI/cmux.swift
  • docs/custom-sidebars.md

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="CLI/cmux.swift">

<violation number="1" location="CLI/cmux.swift:31283">
P2: Inconsistent docs topic list: main CLI help advertises `sidebars` but the `docs` command's own usage string and error message still show the old topic list without it. Users running `cmux docs --help` or triggering the extra-args error path will see `docs [settings|shortcuts|api|browser|agents|dock]` — no mention of sidebars.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread CLI/cmux.swift
Commands:
welcome
docs [settings|shortcuts|api|browser|agents|dock]
docs [settings|shortcuts|api|browser|agents|dock|sidebars]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Inconsistent docs topic list: main CLI help advertises sidebars but the docs command's own usage string and error message still show the old topic list without it. Users running cmux docs --help or triggering the extra-args error path will see docs [settings|shortcuts|api|browser|agents|dock] — no mention of sidebars.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 31283:

<comment>Inconsistent docs topic list: main CLI help advertises `sidebars` but the `docs` command's own usage string and error message still show the old topic list without it. Users running `cmux docs --help` or triggering the extra-args error path will see `docs [settings|shortcuts|api|browser|agents|dock]` — no mention of sidebars.</comment>

<file context>
@@ -31280,7 +31280,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
         Commands:
           welcome
-          docs [settings|shortcuts|api|browser|agents|dock]
+          docs [settings|shortcuts|api|browser|agents|dock|sidebars]
           settings [open [target]|path|docs|<target>]
           config <doctor|check|validate|path|paths|docs|documentation|reload>
</file context>

…harness

Generated 14 opinionated sidebars (one per workflow persona) as a test corpus
and built a coverage harness that runs each through the interpreter against a
rich stub data context and statically lists unsupported symbols. Iterated the
interpreter until every corpus sidebar renders (57-255 nodes each):

- User-defined functions: `func` value helpers (let / if-else with return /
  trailing return) and view helpers (`func row(x) -> some View { ... }`),
  registered in the environment and callable in value and view position.
- Array higher-order: flatMap, reduce (two-arg closure).
- Number/currency/compact formatting via `.formatted(.currency/.notation/...)`.
- `Color(...)` values (hex string or red/green/blue) usable by color modifiers.
- `ScrollView { ... }` (passthrough), `.strikethrough`, and the
  `.font(.system(size:design:))` / `.system(.style, design:)` forms.
- `openURL(...)` button action (opens via NSWorkspace).

Remaining flagged symbols are undefined helpers the personas referenced but
never declared (authoring gaps, not interpreter limits) plus graceful-degrade
cosmetics (contextMenu/overlay/stroke/AsyncImage). 29 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift (1)

136-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix RenderNodeView.applyFrame alignment + resolveFont .system(size:weight:design:) weight parsing

  • applyFrame sets alignment on the min/max .frame(...), but then wraps with a second .frame(width:height:) that defaults to center—so the requested frame(...alignment:) is lost whenever width/height are provided.
  • resolveFont documents .system(size:weight:design:), but the implementation only parses size: (plus monospaced/named text styles) and never handles weight:—so authored font weight is silently dropped; implement weight: parsing or update the doc/comment to match supported tokens.
🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift`
around lines 136 - 145, In applyFrame (RenderNodeView.applyFrame), ensure the
requested alignment is applied to the outermost frame so it isn't overridden by
the width/height wrapper: pass alignment into the second .frame call (i.e.,
.frame(width: dim("width"), height: dim("height"), alignment: alignment)) or
otherwise apply the same alignment to both frames instead of only the min/max
frame. In resolveFont (resolveFont function), actually parse and honor the font
weight token referenced in the docs by reading the weight modifier (e.g.,
modifier.value("weight") or corresponding token), map common names
("ultraLight","thin","light","regular","medium","semibold","bold","heavy","black")
to Font.Weight, and supply that mapped weight into Font.system(size: weight:
design:) when creating system fonts (while preserving existing
size/monospaced/named-style handling).
Sources/DSLSidebarPlayground.swift (1)

27-34: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Support non-integer JSON literals in sidebar cmux params.

Only coercing Int narrows the v2 surface for custom sidebars: commands that need Double or Bool params still go through as strings and fail downstream validation/dispatch. A sidebar-authored ratio: "0.5" for split sizing is one concrete breakage.

Proposed fix
-                    var typed: [String: Any] = [:]
-                    for (key, value) in params {
-                        if let intValue = Int(value) { typed[key] = intValue } else { typed[key] = value }
-                    }
+                    var typed: [String: Any] = [:]
+                    for (key, value) in params {
+                        switch value.lowercased() {
+                        case "true":
+                            typed[key] = true
+                        case "false":
+                            typed[key] = false
+                        default:
+                            if let intValue = Int(value) {
+                                typed[key] = intValue
+                            } else if let doubleValue = Double(value) {
+                                typed[key] = doubleValue
+                            } else {
+                                typed[key] = value
+                            }
+                        }
+                    }

Based on learnings: in Sources/TerminalController.swift, v2 param parsing relies on typed JSON numerics (NSNumber/Double), and present-but-invalid numeric params such as ratio must error rather than fall back.

🤖 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/DSLSidebarPlayground.swift` around lines 27 - 34, The current
coercion only tries Int; update the params-to-typed conversion in
DSLSidebarPlayground.swift (the loop that builds typed from params and assigns
payload["params"]) to: for each value, attempt Int(value), then Double(value),
then Bool(value) and set the first successful typed value; if the string clearly
represents a numeric/boolean literal (e.g., contains digits, a decimal point,
exponent, or "true"/"false") but all parses fail, treat that as an invalid param
and return/error rather than leaving it as a string so downstream v2
validation/dispatch fails fast.
🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift`:
- Around line 14-15: The functions dictionary currently maps names to
FunctionDeclSyntax only, causing helpers to be dynamically scoped because
call-time code binds parameters into the caller environment; update
defineFunction/Environment to capture and store the defining Environment
alongside the FunctionDeclSyntax (e.g., store a tuple or small struct {decl:
FunctionDeclSyntax, definingEnv: Environment?} in functions) and, when invoking
a function, create the call scope as a child of that captured definingEnv
instead of the call-site parent so free variables resolve from the definition
site; apply the same change to the related code paths referenced around the
existing functions and parent usage (also lines ~33-40).

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift`:
- Around line 318-323: The numberMethod(_:_:_:) implementation currently detects
"currency" by string search and returns a hardcoded "$%.2f"; change it to use a
NumberFormatter configured for .currency and set its currencyCode from the call
arguments (parse the currency code from call.arguments/trimmedDescription or
evaluate the first argument expression when name == "formatted" and arg contains
"currency(code:") so it supports non-USD codes (e.g., "EUR"); format the Double
value with that formatter and return .string of the formatted result instead of
the hardcoded USD string.

In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`:
- Around line 139-145: The view-helper branch that handles user-defined
functions (env.lookupFunction + bindParameters) calls evalItems(body.statements,
scope), but evalItems ignores ReturnStmtSyntax so helpers that return a single
view produce no nodes; modify this path to either (A) reuse the value-function
block handling used elsewhere: evaluate the function body as a value-expression
block and extract its returned expression before passing to evalItems, or (B)
extend evalItems to detect and unwrap ReturnStmtSyntax (extract the returned
expression from ReturnStmtSyntax and evaluate it into nodes) so that functions
like func row() -> some View { return HStack { ... } } yield the HStack node;
update the same logic for the similar block at the other mentioned location
(lines 153-172) and ensure you still wrap multiple nodes with RenderNode(kind:
.vstack, children: nodes).

In
`@Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/a-multi-repo-maintainer-juggling-the-cmu.swift`:
- Around line 27-40: The test uses hard-coded roots in notesByDir that don't
match the seeded workspace from corpusStubContext(), so the intended
note/grouping branches aren't exercised; update notesByDir (and any roots array)
to use the same paths produced by corpusStubContext()—e.g., /Users/me/proj0,
/Users/me/proj1, /Users/me/proj2, /Users/me/proj3—or derive roots directly from
corpusStubContext() when constructing notesByDir so the tests hit the
non-empty-note branches and not the OTHER fallback (adjust the variables named
notesByDir and roots accordingly).

In
`@Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/backend-devops-engineer-running-a-fleet-.swift`:
- Around line 84-159: corpusStubContext() currently seeds ci and builds but not
the single-item view-model fields used by the pipeline pane (build, deploys,
deployLog), so the UI under CorpusCoverageTests renders empty; update
corpusStubContext() to also populate a representative Build object (with runId,
status, sha, durationSec, ahead/behind, branch, dirty, changedFiles) and assign
it to the key/variable named build, add an array of Deploy objects (with env,
state, version, relativeTime) for deploys, and add an array of LogLine entries
for deployLog; this ensures the view code (which references build, deploys,
deployLog) has concrete data to exercise the CI/deploy interpreter paths.

In
`@Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/data-scientist-running-ml-training-jobs-.swift`:
- Around line 88-142: The runs and datasets returned by the shared corpus stub
are missing fields used by the UI (in the ForEach rendering of runs and datasets
in this test file), so update the corpus stub used by CorpusCoverageTests to
seed runs with status, epoch, totalEpochs, lr, etaMin, totalSteps, surfaceId and
datasets with workspaceId, surfaceId, mountCommand, mounted, sizeGB, rows (in
addition to existing id/step/loss/eta/name) so the rendering paths in the run
card (Text checks, progress bar using totalSteps, cmux surface.focus using
surfaceId) and dataset buttons (workspace.select, surface.run, mounted state,
size/rows display) exercise the intended interpreter logic.
- Around line 12-72: The test stub is missing fields used by the telemetry pane
(gpu.name, gpu.vramTotalGB, gpu.ownerSurfaceId, host.diskFreeGB), so update the
corpusStubContext() test fixture (and any GPU/Host test object builders it uses)
to populate those fields with realistic sample values; specifically add a name
string, vramTotalGB Int, and ownerSurfaceId (string/int as used by
cmux("surface.focus")) to each GPU entry and add diskFreeGB to the host object
so the ForEach GPU rendering and host Disk label/usage paths exercise the
intended UI branches. Ensure any helpers that construct GPU/Host samples (e.g.,
the GPU struct or factory used in corpusStubContext()) are adjusted accordingly
so existing tests compile and exercise the new labels/actions.

In
`@Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/CorpusCoverageTests.swift`:
- Around line 14-65: The test currently only prints diagnostics; change
reportCorpusCoverage() to assert failures when rendering regresses by (1)
asserting node is non-nil for each file (use XCTAssertNotNil(node, "...
\(file.lastPathComponent)")) and (2) asserting the parsed tree size is
meaningful (e.g., XCTAssertTrue(count > 1, "trivial tree for
\(file.lastPathComponent)")) using the existing nodeCount helper, and (3) fail
the test if unsupportedCalls or unsupportedMembers are non-empty (XCTFail or
XCTAssertTrue(unsupportedCalls.isEmpty, ...) and similarly for
unsupportedMembers) so CI fails on regressions; keep references to
SwiftViewInterpreter, nodeCount, collector.declaredFunctions, unsupportedCalls
and unsupportedMembers to locate where to add the assertions.

---

Outside diff comments:
In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift`:
- Around line 136-145: In applyFrame (RenderNodeView.applyFrame), ensure the
requested alignment is applied to the outermost frame so it isn't overridden by
the width/height wrapper: pass alignment into the second .frame call (i.e.,
.frame(width: dim("width"), height: dim("height"), alignment: alignment)) or
otherwise apply the same alignment to both frames instead of only the min/max
frame. In resolveFont (resolveFont function), actually parse and honor the font
weight token referenced in the docs by reading the weight modifier (e.g.,
modifier.value("weight") or corresponding token), map common names
("ultraLight","thin","light","regular","medium","semibold","bold","heavy","black")
to Font.Weight, and supply that mapped weight into Font.system(size: weight:
design:) when creating system fonts (while preserving existing
size/monospaced/named-style handling).

In `@Sources/DSLSidebarPlayground.swift`:
- Around line 27-34: The current coercion only tries Int; update the
params-to-typed conversion in DSLSidebarPlayground.swift (the loop that builds
typed from params and assigns payload["params"]) to: for each value, attempt
Int(value), then Double(value), then Bool(value) and set the first successful
typed value; if the string clearly represents a numeric/boolean literal (e.g.,
contains digits, a decimal point, exponent, or "true"/"false") but all parses
fail, treat that as an invalid param and return/error rather than leaving it as
a string so downstream v2 validation/dispatch fails fast.
🪄 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: e093001c-00c7-4c35-8e7e-149bedd6cd27

📥 Commits

Reviewing files that changed from the base of the PR and between 1c90f23 and 261d051.

📒 Files selected for processing (23)
  • Packages/CmuxSwiftRender/Package.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/a-multi-repo-maintainer-juggling-the-cmu.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/backend-devops-engineer-running-a-fleet-.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/cs-undergrad-mid-semester-juggling-three.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/data-scientist-running-ml-training-jobs-.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/engineering-manager-for-cmux-my-day-is-t.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/frontend-developer-running-multiple-loca.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/ios-developer-who-lives-in-build-run-deb.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/keyboard-first-vim-purist-i-live-in-norm.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/open-source-maintainer-triaging-issues-a.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/power-user-running-6-12-coding-agents-in.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/product-designer-running-a-design-system.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/project-manager-running-a-small-eng-team.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/sre-on-call-wants-active-alerts-ranked-b.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/technical-writer-who-lives-in-markdown-d.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/CorpusCoverageTests.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift
  • Sources/DSLSidebarPlayground.swift

Comment on lines +14 to +15
private var functions: [String: FunctionDeclSyntax]
private let parent: Environment?

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

User-defined helpers are dynamically scoped today.

defineFunction only stores the FunctionDeclSyntax. Both call paths then bind parameters into a child of the caller environment, so free variables inside the helper resolve from the call site instead of the definition site. A helper declared next to let accent = "#..." will lose that binding or pick up a shadowed accent when invoked from another nested scope. Store the defining environment alongside the declaration and create the call scope from that captured parent.

Also applies to: 33-40

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift` around
lines 14 - 15, The functions dictionary currently maps names to
FunctionDeclSyntax only, causing helpers to be dynamically scoped because
call-time code binds parameters into the caller environment; update
defineFunction/Environment to capture and store the defining Environment
alongside the FunctionDeclSyntax (e.g., store a tuple or small struct {decl:
FunctionDeclSyntax, definingEnv: Environment?} in functions) and, when invoking
a function, create the call scope as a child of that captured definingEnv
instead of the call-site parent so free variables resolve from the definition
site; apply the same change to the related code paths referenced around the
existing functions and parent usage (also lines ~33-40).

Comment on lines +27 to +40
let notesByDir = [
"/Users/me/cmux": [
"next: wire pbxproj test target for new file",
"gotcha: macOS 26 CFURL normalization differs from 14/15",
"blocked: waiting on dogfood approval before merge"
],
"/Users/me/web": [
"waiting on Vercel preview URL before reporting done",
"Effect: map typed errors at route boundary"
],
"/Users/me/ios": [
"next: reload sim + best-effort iPhone, short tag (<=6 chars)"
]
]

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 | ⚡ Quick win

The hard-coded roots never match the shared corpus stub.

corpusStubContext() seeds workspace directories as /Users/me/proj0...proj3, so notesByDir and roots never hit in coverage. This sidebar will only exercise the empty-notes branch and the OTHER catch-all instead of the intended note/grouping paths.

Also applies to: 137-186

🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/a-multi-repo-maintainer-juggling-the-cmu.swift`
around lines 27 - 40, The test uses hard-coded roots in notesByDir that don't
match the seeded workspace from corpusStubContext(), so the intended
note/grouping branches aren't exercised; update notesByDir (and any roots array)
to use the same paths produced by corpusStubContext()—e.g., /Users/me/proj0,
/Users/me/proj1, /Users/me/proj2, /Users/me/proj3—or derive roots directly from
corpusStubContext() when constructing notesByDir so the tests hit the
non-empty-note branches and not the OTHER fallback (adjust the variables named
notesByDir and roots accordingly).

Comment on lines +84 to +159
HStack {
Image(systemName: "arrow.triangle.branch")
Text(git.branch).fontWeight(.semibold)
if git.dirty {
Text("●").foregroundColor("#FF9F0A")
Text("\(git.changedFiles) changed").font(.caption).foregroundColor("#FF9F0A")
} else {
Text("clean").font(.caption).foregroundColor("#34C759")
}
Spacer()
Text("↑\(git.ahead) ↓\(git.behind)").font(.caption).foregroundColor("#8E8E93")
}

Divider()

// --- CI build status ---
HStack(spacing: 8) {
Text("●").foregroundColor(
build.status == "passing" ? "#34C759" :
build.status == "running" ? "#FF9F0A" : "#FF453A"
)
VStack(alignment: .leading, spacing: 1) {
Text("CI: \(build.status)").fontWeight(.semibold)
Text("\(build.sha) · \(build.durationSec)s").font(.caption).foregroundColor("#8E8E93")
}
Spacer()
Button(action: { cmux("ci.rerun", run_id: build.runId) }) {
Image(systemName: "arrow.clockwise")
}
Button(action: { cmux("ci.open", run_id: build.runId) }) {
Image(systemName: "arrow.up.forward.square")
}
}

Divider()

// --- deploy targets ---
Text("Deploy").font(.headline).bold()
ForEach(deploys) { d in
VStack(alignment: .leading, spacing: 3) {
HStack {
Text("●").foregroundColor(
d.state == "live" ? "#34C759" :
d.state == "deploying" ? "#FF9F0A" : "#FF453A"
)
Text(d.env).fontWeight(.semibold)
Spacer()
Text(d.relativeTime).font(.caption).foregroundColor("#8E8E93")
}
HStack {
Text(d.version).font(.caption).foregroundColor("#8E8E93")
Spacer()
Button(d.env == "prod" ? "Ship prod" : "Deploy") {
cmux("deploy.start", env: d.env, sha: build.sha)
}
.foregroundColor(d.env == "prod" ? "#FF453A" : "#0A84FF")
.bold()
}
}
.padding(6)
}

Divider()

// --- recent deploy log tail ---
Text("Recent log").font(.caption).bold().foregroundColor("#8E8E93")
ForEach(deployLog) { line in
HStack(spacing: 6) {
Text(line.level == "error" ? "✗" : line.level == "warn" ? "!" : "·")
.foregroundColor(
line.level == "error" ? "#FF453A" :
line.level == "warn" ? "#FF9F0A" : "#8E8E93"
)
Text(line.text).font(.caption).foregroundColor(line.level == "error" ? "#FF453A" : "#C7C7CC")
}
}

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 | ⚡ Quick win

The pipeline pane is bound to nonexistent corpus data.

corpusStubContext() seeds ci and builds, but not build, deploys, or deployLog. Under CorpusCoverageTests, this entire half will render mostly blank/empty and stop validating the interpreter paths added for CI/deploy content.

🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/backend-devops-engineer-running-a-fleet-.swift`
around lines 84 - 159, corpusStubContext() currently seeds ci and builds but not
the single-item view-model fields used by the pipeline pane (build, deploys,
deployLog), so the UI under CorpusCoverageTests renders empty; update
corpusStubContext() to also populate a representative Build object (with runId,
status, sha, durationSec, ahead/behind, branch, dirty, changedFiles) and assign
it to the key/variable named build, add an array of Deploy objects (with env,
state, version, relativeTime) for deploys, and add an array of LogLine entries
for deployLog; this ensures the view code (which references build, deploys,
deployLog) has concrete data to exercise the CI/deploy interpreter paths.

Comment on lines +12 to +72
ForEach(gpus) { gpu in
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 6) {
Text("GPU \(gpu.index)").font(.caption).bold()
Text(gpu.name).font(.caption).foregroundColor("#8b95a5")
Spacer()
Text("\(gpu.tempC)°C")
.font(.caption)
.foregroundColor(gpu.tempC >= 84 ? "#ff5c5c" : "#8b95a5")
}
// utilization bar
HStack(spacing: 2) {
for seg in 0..<20 {
Text("▉")
.font(.caption)
.foregroundColor(seg < gpu.utilPct / 5
? (gpu.utilPct >= 95 ? "#3ddc84" : "#4aa8ff")
: "#2a2f3a")
}
Text("\(gpu.utilPct)%").font(.caption).bold()
}
// VRAM bar with OOM warning
HStack(spacing: 2) {
for seg in 0..<20 {
Text("▉")
.font(.caption)
.foregroundColor(seg < gpu.vramUsedGB * 20 / gpu.vramTotalGB
? (gpu.vramUsedGB * 100 / gpu.vramTotalGB >= 92 ? "#ff5c5c" : "#b072ff")
: "#2a2f3a")
}
Text("\(gpu.vramUsedGB)/\(gpu.vramTotalGB)G").font(.caption)
}
}
.padding(6)
.background(RoundedRectangle(cornerRadius: 8).fill("#161a22"))
.onTapGesture { cmux("surface.focus", surface_id: gpu.ownerSurfaceId) }
}

Divider()

// Host RAM
HStack(spacing: 6) {
Image(systemName: "memorychip")
Text("RAM").font(.caption).bold()
Spacer()
Text("\(host.ramUsedGB)/\(host.ramTotalGB)G")
.font(.caption)
.foregroundColor(host.ramUsedGB * 100 / host.ramTotalGB >= 90 ? "#ff5c5c" : .primary)
}
HStack(spacing: 2) {
for seg in 0..<24 {
Text("▉").font(.caption)
.foregroundColor(seg < host.ramUsedGB * 24 / host.ramTotalGB ? "#f0a14a" : "#2a2f3a")
}
}
HStack(spacing: 6) {
Image(systemName: "internaldrive")
Text("Disk \(host.diskFreeGB)G free").font(.caption).foregroundColor("#8b95a5")
Spacer()
Text("CPU \(host.cpuPct)%").font(.caption).foregroundColor("#8b95a5")
}

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 | ⚡ Quick win

The telemetry pane outgrew the shared GPU/host stub.

corpusStubContext() does not provide gpu.name, gpu.vramTotalGB, gpu.ownerSurfaceId, or host.diskFreeGB. That leaves several labels/actions unresolved here, so coverage no longer exercises the intended GPU-card and host-summary behavior.

🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/data-scientist-running-ml-training-jobs-.swift`
around lines 12 - 72, The test stub is missing fields used by the telemetry pane
(gpu.name, gpu.vramTotalGB, gpu.ownerSurfaceId, host.diskFreeGB), so update the
corpusStubContext() test fixture (and any GPU/Host test object builders it uses)
to populate those fields with realistic sample values; specifically add a name
string, vramTotalGB Int, and ownerSurfaceId (string/int as used by
cmux("surface.focus")) to each GPU entry and add diskFreeGB to the host object
so the ForEach GPU rendering and host Disk label/usage paths exercise the
intended UI branches. Ensure any helpers that construct GPU/Host samples (e.g.,
the GPU struct or factory used in corpusStubContext()) are adjusted accordingly
so existing tests compile and exercise the new labels/actions.

Comment on lines +88 to +142
ForEach(runs) { run in
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 6) {
Text(run.status == "running" ? "●" : (run.status == "failed" ? "✕" : "✓"))
.foregroundColor(run.status == "running" ? "#3ddc84"
: (run.status == "failed" ? "#ff5c5c" : "#8b95a5"))
Text(run.name).font(.caption).bold()
Spacer()
Text("ep \(run.epoch)/\(run.totalEpochs)").font(.caption).foregroundColor("#8b95a5")
}
HStack(spacing: 8) {
Text("loss \(run.loss)").font(.caption).foregroundColor("#4aa8ff")
Text("lr \(run.lr)").font(.caption).foregroundColor("#8b95a5")
Spacer()
Text("ETA \(run.etaMin)m").font(.caption).foregroundColor("#f0a14a")
}
// step progress
HStack(spacing: 2) {
for seg in 0..<18 {
Text("▬").font(.caption)
.foregroundColor(seg < run.step * 18 / run.totalSteps ? "#3ddc84" : "#2a2f3a")
}
}
}
.padding(6)
.background(RoundedRectangle(cornerRadius: 8).fill("#161a22"))
.onTapGesture { cmux("surface.focus", surface_id: run.surfaceId) }
}
}

Divider()

// Dataset shortcuts
HStack(spacing: 6) {
Image(systemName: "tray.full")
Text("DATASETS").font(.caption).bold().foregroundColor("#8b95a5")
}
ForEach(datasets) { ds in
Button(action: {
cmux("workspace.select", workspace_id: ds.workspaceId)
cmux("surface.run", surface_id: ds.surfaceId, command: ds.mountCommand)
}) {
HStack(spacing: 8) {
Image(systemName: ds.mounted ? "checkmark.circle.fill" : "circle")
.foregroundColor(ds.mounted ? "#3ddc84" : "#8b95a5")
VStack(alignment: .leading, spacing: 1) {
Text(ds.name).font(.caption).bold()
Text("\(ds.sizeGB)G · \(ds.rows) rows").font(.caption).foregroundColor("#8b95a5")
}
Spacer()
Image(systemName: "arrow.right.circle").foregroundColor("#4aa8ff")
}
.padding(6)
}
}

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 | ⚡ Quick win

The run and dataset cards depend on fields the corpus stub never seeds.

The shared stub only gives runs { id, step, loss, eta } and datasets { id, name }, but this file expects status, epoch, totalEpochs, lr, etaMin, totalSteps, surfaceId, workspaceId, mountCommand, mounted, sizeGB, and rows. Those cards therefore render with missing data/actions in CorpusCoverageTests instead of covering the intended interpreter paths.

🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/data-scientist-running-ml-training-jobs-.swift`
around lines 88 - 142, The runs and datasets returned by the shared corpus stub
are missing fields used by the UI (in the ForEach rendering of runs and datasets
in this test file), so update the corpus stub used by CorpusCoverageTests to
seed runs with status, epoch, totalEpochs, lr, etaMin, totalSteps, surfaceId and
datasets with workspaceId, surfaceId, mountCommand, mounted, sizeGB, rows (in
addition to existing id/step/loss/eta/name) so the rendering paths in the run
card (Text checks, progress bar using totalSteps, cmux surface.focus using
surfaceId) and dataset buttons (workspace.select, surface.run, mounted state,
size/rows display) exercise the intended interpreter logic.

Comment on lines +14 to +65
@Test func reportCorpusCoverage() {
let dir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent("Corpus")
let files = ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? [])
.filter { $0.pathExtension == "swift" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }

let interp = SwiftViewInterpreter()
let ctx = corpusStubContext()

let supportedCalls: Set<String> = [
"Text", "VStack", "HStack", "ZStack", "HSplitView", "Button", "Image",
"Spacer", "Divider", "Rectangle", "RoundedRectangle", "Capsule", "Circle",
"ForEach", "Reorderable", "cmux", "log", "Color", "ScrollView", "openURL",
]
let supportedMembers: Set<String> = [
"font", "bold", "fontWeight", "foregroundColor", "foregroundStyle", "fill",
"tint", "padding", "background", "cornerRadius", "opacity", "lineLimit",
"frame", "onTapGesture",
"filter", "map", "flatMap", "reduce", "sorted", "first", "contains", "count",
"reversed", "prefix", "isEmpty", "hasPrefix", "hasSuffix", "uppercased",
"lowercased", "split", "formatted", "currency", "notation", "percent", "strikethrough", "system",
]

var unsupportedCalls: [String: Int] = [:]
var unsupportedMembers: [String: Int] = [:]
var coverage: [String] = []

for file in files {
let src = (try? String(contentsOf: file, encoding: .utf8)) ?? ""
let node = interp.evaluate(src, state: ctx)
let count = node.map(Self.nodeCount) ?? 0
let collector = SymbolCollector(viewMode: .sourceAccurate)
collector.walk(Parser.parse(source: src))
// User-defined funcs in the same file are supported via the
// interpreter's function table; don't flag them.
let userFuncs = collector.declaredFunctions
for (name, n) in collector.declCalls where !supportedCalls.contains(name) && !userFuncs.contains(name) {
unsupportedCalls[name, default: 0] += n
}
for (name, n) in collector.memberCalls where !supportedMembers.contains(name) {
unsupportedMembers[name, default: 0] += n
}
coverage.append(String(format: "%@ %4d nodes %@", node != nil ? "OK " : "NIL", count, file.lastPathComponent))
}

print("\n===== CORPUS RENDER COVERAGE (\(files.count) sidebars) =====")
coverage.forEach { print($0) }
print("\n===== UNSUPPORTED CONSTRUCTORS / FUNCTIONS =====")
for (k, n) in unsupportedCalls.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("\n===== UNSUPPORTED MODIFIERS / METHODS =====")
for (k, n) in unsupportedMembers.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("===== END =====\n")

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 | ⚡ Quick win

Make the coverage test fail on render regressions.

This test only prints diagnostics today. If a future change makes a corpus file return nil or collapse to a trivial tree, CI still passes, so the suite does not actually protect the interpreter behavior this PR is trying to lock in.

Suggested assertion layer
     `@Test` func reportCorpusCoverage() {
+        var failedFiles: [String] = []
         let dir = URL(fileURLWithPath: `#filePath`).deletingLastPathComponent().appendingPathComponent("Corpus")
         let files = ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? [])
             .filter { $0.pathExtension == "swift" }
             .sorted { $0.lastPathComponent < $1.lastPathComponent }
@@
         for file in files {
             let src = (try? String(contentsOf: file, encoding: .utf8)) ?? ""
             let node = interp.evaluate(src, state: ctx)
             let count = node.map(Self.nodeCount) ?? 0
+            if node == nil || count == 0 {
+                failedFiles.append(file.lastPathComponent)
+            }
             let collector = SymbolCollector(viewMode: .sourceAccurate)
             collector.walk(Parser.parse(source: src))
@@
         print("\n===== CORPUS RENDER COVERAGE (\(files.count) sidebars) =====")
         coverage.forEach { print($0) }
@@
         print("===== END =====\n")
+        `#expect`(failedFiles.isEmpty, "Corpus files failed to render: \(failedFiles.joined(separator: ", "))")
     }

Based on learnings: Applies to **/Test.swift : Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns. Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape.

📝 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
@Test func reportCorpusCoverage() {
let dir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent("Corpus")
let files = ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? [])
.filter { $0.pathExtension == "swift" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
let interp = SwiftViewInterpreter()
let ctx = corpusStubContext()
let supportedCalls: Set<String> = [
"Text", "VStack", "HStack", "ZStack", "HSplitView", "Button", "Image",
"Spacer", "Divider", "Rectangle", "RoundedRectangle", "Capsule", "Circle",
"ForEach", "Reorderable", "cmux", "log", "Color", "ScrollView", "openURL",
]
let supportedMembers: Set<String> = [
"font", "bold", "fontWeight", "foregroundColor", "foregroundStyle", "fill",
"tint", "padding", "background", "cornerRadius", "opacity", "lineLimit",
"frame", "onTapGesture",
"filter", "map", "flatMap", "reduce", "sorted", "first", "contains", "count",
"reversed", "prefix", "isEmpty", "hasPrefix", "hasSuffix", "uppercased",
"lowercased", "split", "formatted", "currency", "notation", "percent", "strikethrough", "system",
]
var unsupportedCalls: [String: Int] = [:]
var unsupportedMembers: [String: Int] = [:]
var coverage: [String] = []
for file in files {
let src = (try? String(contentsOf: file, encoding: .utf8)) ?? ""
let node = interp.evaluate(src, state: ctx)
let count = node.map(Self.nodeCount) ?? 0
let collector = SymbolCollector(viewMode: .sourceAccurate)
collector.walk(Parser.parse(source: src))
// User-defined funcs in the same file are supported via the
// interpreter's function table; don't flag them.
let userFuncs = collector.declaredFunctions
for (name, n) in collector.declCalls where !supportedCalls.contains(name) && !userFuncs.contains(name) {
unsupportedCalls[name, default: 0] += n
}
for (name, n) in collector.memberCalls where !supportedMembers.contains(name) {
unsupportedMembers[name, default: 0] += n
}
coverage.append(String(format: "%@ %4d nodes %@", node != nil ? "OK " : "NIL", count, file.lastPathComponent))
}
print("\n===== CORPUS RENDER COVERAGE (\(files.count) sidebars) =====")
coverage.forEach { print($0) }
print("\n===== UNSUPPORTED CONSTRUCTORS / FUNCTIONS =====")
for (k, n) in unsupportedCalls.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("\n===== UNSUPPORTED MODIFIERS / METHODS =====")
for (k, n) in unsupportedMembers.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("===== END =====\n")
`@Test` func reportCorpusCoverage() {
var failedFiles: [String] = []
let dir = URL(fileURLWithPath: `#filePath`).deletingLastPathComponent().appendingPathComponent("Corpus")
let files = ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? [])
.filter { $0.pathExtension == "swift" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
let interp = SwiftViewInterpreter()
let ctx = corpusStubContext()
let supportedCalls: Set<String> = [
"Text", "VStack", "HStack", "ZStack", "HSplitView", "Button", "Image",
"Spacer", "Divider", "Rectangle", "RoundedRectangle", "Capsule", "Circle",
"ForEach", "Reorderable", "cmux", "log", "Color", "ScrollView", "openURL",
]
let supportedMembers: Set<String> = [
"font", "bold", "fontWeight", "foregroundColor", "foregroundStyle", "fill",
"tint", "padding", "background", "cornerRadius", "opacity", "lineLimit",
"frame", "onTapGesture",
"filter", "map", "flatMap", "reduce", "sorted", "first", "contains", "count",
"reversed", "prefix", "isEmpty", "hasPrefix", "hasSuffix", "uppercased",
"lowercased", "split", "formatted", "currency", "notation", "percent", "strikethrough", "system",
]
var unsupportedCalls: [String: Int] = [:]
var unsupportedMembers: [String: Int] = [:]
var coverage: [String] = []
for file in files {
let src = (try? String(contentsOf: file, encoding: .utf8)) ?? ""
let node = interp.evaluate(src, state: ctx)
let count = node.map(Self.nodeCount) ?? 0
if node == nil || count == 0 {
failedFiles.append(file.lastPathComponent)
}
let collector = SymbolCollector(viewMode: .sourceAccurate)
collector.walk(Parser.parse(source: src))
// User-defined funcs in the same file are supported via the
// interpreter's function table; don't flag them.
let userFuncs = collector.declaredFunctions
for (name, n) in collector.declCalls where !supportedCalls.contains(name) && !userFuncs.contains(name) {
unsupportedCalls[name, default: 0] += n
}
for (name, n) in collector.memberCalls where !supportedMembers.contains(name) {
unsupportedMembers[name, default: 0] += n
}
coverage.append(String(format: "%@ %4d nodes %@", node != nil ? "OK " : "NIL", count, file.lastPathComponent))
}
print("\n===== CORPUS RENDER COVERAGE (\(files.count) sidebars) =====")
coverage.forEach { print($0) }
print("\n===== UNSUPPORTED CONSTRUCTORS / FUNCTIONS =====")
for (k, n) in unsupportedCalls.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("\n===== UNSUPPORTED MODIFIERS / METHODS =====")
for (k, n) in unsupportedMembers.sorted(by: { ($0.value, $1.key) > ($1.value, $0.key) }) { print("\(n)x \(k)") }
print("===== END =====\n")
`#expect`(failedFiles.isEmpty, "Corpus files failed to render: \(failedFiles.joined(separator: ", "))")
}
🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/CorpusCoverageTests.swift`
around lines 14 - 65, The test currently only prints diagnostics; change
reportCorpusCoverage() to assert failures when rendering regresses by (1)
asserting node is non-nil for each file (use XCTAssertNotNil(node, "...
\(file.lastPathComponent)")) and (2) asserting the parsed tree size is
meaningful (e.g., XCTAssertTrue(count > 1, "trivial tree for
\(file.lastPathComponent)")) using the existing nodeCount helper, and (3) fail
the test if unsupportedCalls or unsupportedMembers are non-empty (XCTFail or
XCTAssertTrue(unsupportedCalls.isEmpty, ...) and similarly for
unsupportedMembers) so CI fails on regressions; keep references to
SwiftViewInterpreter, nodeCount, collector.declaredFunctions, unsupportedCalls
and unsupportedMembers to locate where to add the assertions.

@cubic-dev-ai cubic-dev-ai 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.

10 issues found across 23 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:172">
P2: `resolveFont` ignores `weight:` for `.system(size:weight:design:)`, so authored font weights are silently dropped.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:179">
P1: Font-style lookup matches "headline" instead of "subheadline" due to substring collision. `token.contains("headline")` returns true for the string `"subheadline"`, so `.system(.subheadline)` is resolved to `.headline`.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:143">
P2: View helper functions with an explicit `return` are not interpreted, so `func ... -> some View { return ... }` can render empty output.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:154">
P2: `registerFunctions(items, env)` inside `evalItems` writes function declarations into whichever `Environment` is passed — but for VStack, HStack, ZStack, and HSplitView, `evalCall` passes the **parent** `env` directly (not a child scope). This means any `func` defined inside a stack's trailing closure body leaks to the outer lexical scope and silently overwrites any function with the same name declared outside the stack, creating a scoping bug.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:14">
P2: Concurrency model gap widened: adding `private var functions` and mutating methods to a non-Sendable `final class` under Swift 6 strict mode. The class holds mutable state (`values`, `functions`) without `@MainActor`, actor isolation, or any concurrency primitive, violating the project's Swift 6 concurrency requirement.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:35">
P1: Capture and reuse the function’s defining environment when registering user functions; binding from the caller scope here gives dynamic scoping and can resolve shadowed/free variables incorrectly.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:288">
P3: Dead code: unreachable `IfExprSyntax` check inside `ExprSyntax` branch of `evalBlockValue`. The preceding check (`node.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self) ?? node.as(IfExprSyntax.self)`) already catches all `IfExprSyntax` nodes and either returns or continues past this point.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:300">
P2: `evalIfValue` treats non-expression conditions (e.g., `if let x = optional`, `if #available(...)`) as always false. Optional-binding conditions silently skip the if-body, which will confound authors who use them in sidebar functions.</violation>

<violation number="3" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:322">
P2: This hardcodes USD formatting for every `.currency(code:)` call. Parse and honor the requested currency code (or use a formatter) so non-USD values render correctly.</violation>

<violation number="4" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:366">
P2: `evalClosure2` only handles single-expression closures. Multi-statement closures with `let` declarations, `return` statements, or `if` expressions silently return `nil`. This is inconsistent with `evalBlockValue` which supports full multi-statement function bodies, and could silently corrupt `reduce` results.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

("headline", .headline), ("subheadline", .subheadline), ("body", .body), ("callout", .callout),
("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
]
for (name, style) in styles where token.contains(name) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Font-style lookup matches "headline" instead of "subheadline" due to substring collision. token.contains("headline") returns true for the string "subheadline", so .system(.subheadline) is resolved to .headline.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift, line 179:

<comment>Font-style lookup matches "headline" instead of "subheadline" due to substring collision. `token.contains("headline")` returns true for the string `"subheadline"`, so `.system(.subheadline)` is resolved to `.headline`.</comment>

<file context>
@@ -158,6 +160,28 @@ struct RenderNodeView: View {
+            ("headline", .headline), ("subheadline", .subheadline), ("body", .body), ("callout", .callout),
+            ("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
+        ]
+        for (name, style) in styles where token.contains(name) {
+            return .system(style, design: design)
+        }
</file context>


/// Registers a user-defined function in this scope.
func defineFunction(_ name: String, _ decl: FunctionDeclSyntax) {
functions[name] = decl

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Capture and reuse the function’s defining environment when registering user functions; binding from the caller scope here gives dynamic scoping and can resolve shadowed/free variables incorrectly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift, line 35:

<comment>Capture and reuse the function’s defining environment when registering user functions; binding from the caller scope here gives dynamic scoping and can resolve shadowed/free variables incorrectly.</comment>

<file context>
@@ -24,6 +30,16 @@ final class Environment {
 
+    /// Registers a user-defined function in this scope.
+    func defineFunction(_ name: String, _ decl: FunctionDeclSyntax) {
+        functions[name] = decl
+    }
+
</file context>

// MARK: - ViewBuilder statements

private func evalItems(_ items: CodeBlockItemListSyntax, _ env: Environment) -> [RenderNode] {
registerFunctions(items, env)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: registerFunctions(items, env) inside evalItems writes function declarations into whichever Environment is passed — but for VStack, HStack, ZStack, and HSplitView, evalCall passes the parent env directly (not a child scope). This means any func defined inside a stack's trailing closure body leaks to the outer lexical scope and silently overwrites any function with the same name declared outside the stack, creating a scoping bug.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift, line 154:

<comment>`registerFunctions(items, env)` inside `evalItems` writes function declarations into whichever `Environment` is passed — but for VStack, HStack, ZStack, and HSplitView, `evalCall` passes the **parent** `env` directly (not a child scope). This means any `func` defined inside a stack's trailing closure body leaks to the outer lexical scope and silently overwrites any function with the same name declared outside the stack, creating a scoping bug.</comment>

<file context>
@@ -114,16 +129,29 @@ public struct SwiftViewInterpreter: Sendable {
     // MARK: - ViewBuilder statements
 
     private func evalItems(_ items: CodeBlockItemListSyntax, _ env: Environment) -> [RenderNode] {
+        registerFunctions(items, env)
         var out: [RenderNode] = []
         for item in items {
</file context>

/// called from anywhere in the same or a nested scope.
final class Environment {
private var values: [String: SwiftValue]
private var functions: [String: FunctionDeclSyntax]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Concurrency model gap widened: adding private var functions and mutating methods to a non-Sendable final class under Swift 6 strict mode. The class holds mutable state (values, functions) without @MainActor, actor isolation, or any concurrency primitive, violating the project's Swift 6 concurrency requirement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift, line 14:

<comment>Concurrency model gap widened: adding `private var functions` and mutating methods to a non-Sendable `final class` under Swift 6 strict mode. The class holds mutable state (`values`, `functions`) without `@MainActor`, actor isolation, or any concurrency primitive, violating the project's Swift 6 concurrency requirement.</comment>

<file context>
@@ -1,16 +1,22 @@
+/// called from anywhere in the same or a nested scope.
 final class Environment {
     private var values: [String: SwiftValue]
+    private var functions: [String: FunctionDeclSyntax]
     private let parent: Environment?
 
</file context>

if names.count > 1 { scope.define(names[1].name.text, b) }
}
for item in closure.statements {
if let expr = item.item.as(ExprSyntax.self) { return eval(expr, scope) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: evalClosure2 only handles single-expression closures. Multi-statement closures with let declarations, return statements, or if expressions silently return nil. This is inconsistent with evalBlockValue which supports full multi-statement function bodies, and could silently corrupt reduce results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift, line 366:

<comment>`evalClosure2` only handles single-expression closures. Multi-statement closures with `let` declarations, `return` statements, or `if` expressions silently return `nil`. This is inconsistent with `evalBlockValue` which supports full multi-statement function bodies, and could silently corrupt `reduce` results.</comment>

<file context>
@@ -209,6 +241,133 @@ struct ExpressionEvaluator {
+            if names.count > 1 { scope.define(names[1].name.text, b) }
+        }
+        for item in closure.statements {
+            if let expr = item.item.as(ExprSyntax.self) { return eval(expr, scope) }
+        }
+        return nil
</file context>


private func evalIfValue(_ ifExpr: IfExprSyntax, _ scope: Environment) -> SwiftValue? {
let taken = ifExpr.conditions.allSatisfy { element in
guard let expr = element.condition.as(ExprSyntax.self) else { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: evalIfValue treats non-expression conditions (e.g., if let x = optional, if #available(...)) as always false. Optional-binding conditions silently skip the if-body, which will confound authors who use them in sidebar functions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift, line 300:

<comment>`evalIfValue` treats non-expression conditions (e.g., `if let x = optional`, `if #available(...)`) as always false. Optional-binding conditions silently skip the if-body, which will confound authors who use them in sidebar functions.</comment>

<file context>
@@ -209,6 +241,133 @@ struct ExpressionEvaluator {
+
+    private func evalIfValue(_ ifExpr: IfExprSyntax, _ scope: Environment) -> SwiftValue? {
+        let taken = ifExpr.conditions.allSatisfy { element in
+            guard let expr = element.condition.as(ExprSyntax.self) else { return false }
+            return eval(expr, scope)?.isTruthy ?? false
+        }
</file context>

if let range = token.range(of: "size:") {
let digits = token[range.upperBound...].drop(while: { $0 == " " })
.prefix(while: { $0.isNumber || $0 == "." })
if let n = Double(digits) { return .system(size: CGFloat(n), design: design) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: resolveFont ignores weight: for .system(size:weight:design:), so authored font weights are silently dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift, line 172:

<comment>`resolveFont` ignores `weight:` for `.system(size:weight:design:)`, so authored font weights are silently dropped.</comment>

<file context>
@@ -158,6 +160,28 @@ struct RenderNodeView: View {
+        if let range = token.range(of: "size:") {
+            let digits = token[range.upperBound...].drop(while: { $0 == " " })
+                .prefix(while: { $0.isNumber || $0 == "." })
+            if let n = Double(digits) { return .system(size: CGFloat(n), design: design) }
+        }
+        let styles: [(String, Font.TextStyle)] = [
</file context>
Suggested change
if let n = Double(digits) { return .system(size: CGFloat(n), design: design) }
if let n = Double(digits) {
let weight: Font.Weight = {
if token.contains("ultraLight") { return .ultraLight }
if token.contains("thin") { return .thin }
if token.contains("light") { return .light }
if token.contains("regular") { return .regular }
if token.contains("medium") { return .medium }
if token.contains("semibold") { return .semibold }
if token.contains("bold") { return .bold }
if token.contains("heavy") { return .heavy }
if token.contains("black") { return .black }
return .regular
}()
return .system(size: CGFloat(n), weight: weight, design: design)
}

Comment thread Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift Outdated
continue
}
if let expr = node.as(ExprSyntax.self) {
if let ifExpr = expr.as(IfExprSyntax.self) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Dead code: unreachable IfExprSyntax check inside ExprSyntax branch of evalBlockValue. The preceding check (node.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self) ?? node.as(IfExprSyntax.self)) already catches all IfExprSyntax nodes and either returns or continues past this point.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift, line 288:

<comment>Dead code: unreachable `IfExprSyntax` check inside `ExprSyntax` branch of `evalBlockValue`. The preceding check (`node.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self) ?? node.as(IfExprSyntax.self)`) already catches all `IfExprSyntax` nodes and either returns or continues past this point.</comment>

<file context>
@@ -209,6 +241,133 @@ struct ExpressionEvaluator {
+                continue
+            }
+            if let expr = node.as(ExprSyntax.self) {
+                if let ifExpr = expr.as(IfExprSyntax.self) {
+                    if let value = evalIfValue(ifExpr, scope) { return value }
+                    continue
</file context>

… mask

The interpreted sidebar content (both the non-split scroll wrapper and each
ResizableHSplit column) started flush at y=0, so headers like "Agent Ops" /
"Task Queue" underlapped the window's titlebar accessory strip, and content
clipped sharply at the top edge instead of dissolving into the sidebar's fade
mask the way the default workspace sidebar does.

Add a host-injected CustomSidebarContentInsets (top/bottom), carried through
the SwiftUI environment so the nested ResizableHSplit (built inside the generic
RenderNodeView) picks it up without threading params through the IR renderer.
Both scroll regions now apply matching top/bottom safeAreaInsets, mirroring the
default sidebar: ContentView passes SidebarWorkspaceScrollInsets.workspaceList
top/bottom, and the existing top fade mask makes scrolled content dissolve
under the accessory bar instead of clipping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +57 to +63
public var iterationValues: [SwiftValue]? {
switch self {
case let .range(lower, upper, inclusive):
let end = inclusive ? upper + 1 : upper
guard end >= lower else { return [] }
return (lower..<end).map(SwiftValue.int)
case let .array(values):

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 Unbounded range materialization on every 1-second main-thread tick

iterationValues eagerly materializes every integer in a range as a SwiftValue array before the caller can iterate. The previous thread flagged the Int.max + 1 trap for inclusive ranges; there is a second failure mode for exclusive ranges: any for i in 0..<N { } with a large-but-sub-overflow N (e.g. 0..<100_000) allocates 100 K heap objects per evaluation, and because the interpreter runs inside the 1-second TimelineView body on the main actor, a single authoring mistake stalls UI on every tick until the file is edited and saved. A safety cap (e.g. min(end - lower, 50_000) with a failed node emitted when the cap fires) prevents both the main-thread freeze and gives the author feedback without crashing the host.

Replace the 4-key custom-sidebar data context with a full read-surface
projection of cmux runtime state so interpreted sidebars can be fully
data-driven. Each workspace now exposes: pinned, index, directory, ports +
portCount, unread, tabs + tabCount, and (when present) description, color hex,
git branch + dirty, pull request {number,label,url,status,stale,branch},
progress {value,label}, latest agent message / submitted prompt / timestamp,
and remote {target,state,connected}. Each surface exposes focused, pinned, and
when available directory, branch + dirty, ports. Top level adds selectedId and
unreadTotal; clock adds weekday. Optional fields are omitted when absent so
interpreted `if let` / ternary truthiness works.

Also lands the design artifacts from the multi-agent surface study:
docs/swiftui-interpreter-surface.md (14-domain capability matrix + roadmap)
and docs/data-driven-sidebar-plan.md (data/command/hook architecture + waves),
and documents the new data keys in docs/custom-sidebars.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gap report

The app-target file now contains only makeCmuxSidebarActionDispatch (the
cmux-coupled action sink); its old "playground" name no longer fits. Rename the
file and rewire its four project.pbxproj references (file IDs unchanged),
normalized + check-pbxproj clean. Remove docs/custom-sidebar-gap-report.md,
superseded by docs/swiftui-interpreter-surface.md (full capability matrix) and
docs/data-driven-sidebar-plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +174 to +179
let styles: [(String, Font.TextStyle)] = [
("largeTitle", .largeTitle), ("title3", .title3), ("title2", .title2), ("title", .title),
("headline", .headline), ("subheadline", .subheadline), ("body", .body), ("callout", .callout),
("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
]
for (name, style) in styles where token.contains(name) {

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 .subheadline silently resolves to .headline. The loop uses token.contains(name) for matching, and "subheadline" contains "headline" as a substring — so the earlier ("headline", .headline) entry fires first for any .system(.subheadline) modifier argument. Moving "subheadline" before "headline" in the list ensures the more-specific name is checked first.

Suggested change
let styles: [(String, Font.TextStyle)] = [
("largeTitle", .largeTitle), ("title3", .title3), ("title2", .title2), ("title", .title),
("headline", .headline), ("subheadline", .subheadline), ("body", .body), ("callout", .callout),
("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
]
for (name, style) in styles where token.contains(name) {
let styles: [(String, Font.TextStyle)] = [
("largeTitle", .largeTitle), ("title3", .title3), ("title2", .title2), ("title", .title),
("subheadline", .subheadline), ("headline", .headline), ("body", .body), ("callout", .callout),
("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
]
for (name, style) in styles where token.contains(name) {

Comment on lines +130 to +132
case "+": return bothInt ? .int(Int(l + r)) : .double(l + r)
case "-": return bothInt ? .int(Int(l - r)) : .double(l - r)
case "*": return bothInt ? .int(Int(l * r)) : .double(l * r)

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 Integer arithmetic for +, -, and * is computed in Double (via numericPair) and then narrowed back with Int(...). Int(someDouble) is an unconditional trap when the value falls outside Int range — for example Int.max + 1 as two Int literals in user sidebar code will crash the host process. The already-flagged division-by-zero guard should accompany an Int.init(exactly:) check here so overflow silently returns nil instead of trapping.

Suggested change
case "+": return bothInt ? .int(Int(l + r)) : .double(l + r)
case "-": return bothInt ? .int(Int(l - r)) : .double(l - r)
case "*": return bothInt ? .int(Int(l * r)) : .double(l * r)
case "+": return bothInt ? Int(exactly: l + r).map(SwiftValue.int) ?? .double(l + r) : .double(l + r)
case "-": return bothInt ? Int(exactly: l - r).map(SwiftValue.int) ?? .double(l - r) : .double(l - r)
case "*": return bothInt ? Int(exactly: l * r).map(SwiftValue.int) ?? .double(l * r) : .double(l * r)

@cubic-dev-ai cubic-dev-ai 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.

4 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:172">
P2: `resolveFont` ignores `weight:` for `.system(size:weight:design:)`, so authored font weights are silently dropped.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:179">
P1: Font-style lookup matches "headline" instead of "subheadline" due to substring collision. `token.contains("headline")` returns true for the string `"subheadline"`, so `.system(.subheadline)` is resolved to `.headline`.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:75">
P1: `Button("title", action: { … })` drops the title string because the "label form" branch (triggered by finding an `action:` labeled closure argument) returns a node with no text and an empty children array, ignoring the unlabeled first argument.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:154">
P2: `registerFunctions(items, env)` inside `evalItems` writes function declarations into whichever `Environment` is passed — but for VStack, HStack, ZStack, and HSplitView, `evalCall` passes the **parent** `env` directly (not a child scope). This means any `func` defined inside a stack's trailing closure body leaks to the outer lexical scope and silently overwrites any function with the same name declared outside the stack, creating a scoping bug.</violation>

<violation number="3" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P3: `closureParameterName(_:)` is duplicated verbatim in both `SwiftViewInterpreter` and `ExpressionEvaluator`, each as a private method doing the same closure-signature parameter-name extraction.</violation>

<violation number="4" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P1: Empty-string sentinel for missing item IDs in Reorderable: `item.member(idField)?.displayString ?? ""` inserts `""` into `itemIds` when the id field is absent, causing invalid reorder commands (e.g., `workspace_id: ""`) instead of a clear failure or graceful skip.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:14">
P2: Concurrency model gap widened: adding `private var functions` and mutating methods to a non-Sendable `final class` under Swift 6 strict mode. The class holds mutable state (`values`, `functions`) without `@MainActor`, actor isolation, or any concurrency primitive, violating the project's Swift 6 concurrency requirement.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:35">
P1: Capture and reuse the function’s defining environment when registering user functions; binding from the caller scope here gives dynamic scoping and can resolve shadowed/free variables incorrectly.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift:4">
P1: Missing fallback for unknown raw values during JSON decoding — user-authored sidebar files with an unrecognized node kind (e.g., a typo like `"vstak"` or a future plugin authoring a new kind before this code adds it) will throw a hard `DecodingError.dataCorrupted` and fail to load the entire sidebar. Add a custom `Decodable` that maps unknown strings to a fallback `.unknown` case.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift:66">
P2: Drag gesture writes to `@AppStorage` on every `.onChanged` event (~60 writes/sec), causing unnecessary `UserDefaults` synchronization and potential UI jank. Use a local `@State` for the transient drag value and persist only on `.onEnded`.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift:51">
P2: Uses `FileManager.default` directly instead of an injected dependency, making the model untestable without real file I/O. The repository guidance (`CLAUDE.md`) requires package APIs to be testable without launching the app or relying on `FileManager.default`. The `reload()` method also reads files via `Data(contentsOf:)` and `String(contentsOf:encoding:)` which bypass any controllable FileManager.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift:51">
P2: JSON sidebar action handler silently discards all button actions. The `onAction:` closure is `{ _ in }`, so any interactive element (button, tap) in a `.json` sidebar produces zero side effects — the buttons render but do nothing when tapped. Meanwhile, interpreted Swift sidebars correctly dispatch actions through the environment's `sidebarActionDispatch`.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift:33">
P2: Button node does not apply `resolvedFont`, unlike `.text` and `.image` cases. When a JSON DSL button specifies `font` or `size`, those properties are silently ignored, while the same properties work correctly on text and image nodes. This means the button's title text will always appear at the system default font regardless of the authored style.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift:4">
P3: Three ViewModifier types in one file violates the repo's one-major-type-per-file convention. Split `OptionalForeground`, `OptionalPadding`, and `OptionalBackground` into separate files for consistency with the codebase convention.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift:9">
P2: Auto-synthesized `Equatable` includes `id` (`UUID()`) in the comparison, making equality checks always return `false` for distinct instances even when semantically identical. The doc comment explicitly says `id` is "Not decoded; a stable identity per decoded node" — it is a runtime identity marker, not part of the value. Including it in `Equatable` means `==` can never be true for two separately-initialized or decoded nodes with identical content, breaking any code that relies on value equality (SwiftUI `EquatableView`, test assertions, collection diffing).</violation>
</file>

<file name="cmux.xcodeproj/project.pbxproj">

<violation number="1" location="cmux.xcodeproj/project.pbxproj:1932">
P2: CmuxSwiftRenderUI (a SwiftUI+AppKit UI-rendering package) is added as a dependency of the cmux-cli command-line tool target. Every source file in this package imports SwiftUI, and ResizableHSplit.swift imports AppKit. The cmux-cli target (product-type tool) has no UI runtime and cannot meaningfully use SwiftUI View types, which will unnecessarily link SwiftUI/AppKit frameworks into the CLI binary and create a maintenance burden.</violation>
</file>

<file name="Sources/DSLSidebarPlayground.swift">

<violation number="1" location="Sources/DSLSidebarPlayground.swift:31">
P2: Unconditionally coercing all numeric-looking string params to Int can break commands that require string-typed values.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:18">
P1: ForEach uses index-based identity (`id: \.offset`) in a reorderable list, even though stable per-item identifiers are available via `spec.itemIds`. When a row is dragged to a new position and `rows` updates, SwiftUI identifies each row by its index (0, 1, 2...) rather than by its stable id. This prevents SwiftUI from tracking which view corresponds to which item across the reorder, losing view state, breaking transition animations, and making the reorder feel unresponsive or glitchy.

Use stable IDs from `spec.itemIds` as the ForEach identity, paired with each row, so SwiftUI correctly preserves view identity when items move.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:37">
P2: Validate `ReorderSpec` command/key strings (and `draggedId`) before dispatching `.cmux`; currently malformed or empty dynamic keys can produce invalid reorder commands.

(Based on your team's feedback about avoiding sentinel/missing-key patterns in cmux param construction.) [FEEDBACK_USED].</violation>
</file>

<file name="CLI/cmux.swift">

<violation number="1" location="CLI/cmux.swift:31283">
P2: Inconsistent docs topic list: main CLI help advertises `sidebars` but the `docs` command's own usage string and error message still show the old topic list without it. Users running `cmux docs --help` or triggering the extra-args error path will see `docs [settings|shortcuts|api|browser|agents|dock]` — no mention of sidebars.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:288">
P3: Dead code: unreachable `IfExprSyntax` check inside `ExprSyntax` branch of `evalBlockValue`. The preceding check (`node.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self) ?? node.as(IfExprSyntax.self)`) already catches all `IfExprSyntax` nodes and either returns or continues past this point.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:300">
P2: `evalIfValue` treats non-expression conditions (e.g., `if let x = optional`, `if #available(...)`) as always false. Optional-binding conditions silently skip the if-body, which will confound authors who use them in sidebar functions.</violation>

<violation number="3" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:366">
P2: `evalClosure2` only handles single-expression closures. Multi-statement closures with `let` declarations, `return` statements, or `if` expressions silently return `nil`. This is inconsistent with `evalBlockValue` which supports full multi-statement function bodies, and could silently corrupt `reduce` results.</violation>
</file>

<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:10498">
P2: `selectedId` uses an empty-string sentinel for “no selection” instead of omission, which breaks the key-presence pattern this sidebar data model relies on.

(Based on your team's feedback about checking key presence instead of string sentinels.) [FEEDBACK_USED].</violation>

<violation number="2" location="Sources/ContentView.swift:10521">
P2: tabCount counts all bonsplit tabs unconditionally, but the `tabs` array filters out entries where `panelIdFromSurfaceId(tab.id)` returns nil. When a surface has no panel mapping (possible during lifecycle transitions), `tabCount` will be greater than `tabs.count`, exposing a misleading raw count that doesn't match the actual array. Sidebar authors iterating over `tabs` by index or comparing counts will encounter off-by-one or out-of-bounds bugs.</violation>

<violation number="3" location="Sources/ContentView.swift:10521">
P2: The new `tabCount` line rescans all panes/tabs even though `tabs` was just built from the same traversal, adding avoidable repeated work in a ~1s refresh path.</violation>
</file>

<file name="docs/data-driven-sidebar-plan.md">

<violation number="1" location="docs/data-driven-sidebar-plan.md:56">
P2: Agent-hook coverage list is inaccurate: it omits `amp` and uses `hermes` instead of the wired `hermes-agent` identifier, which can mislead hook setup and review scope.

(Based on your team's feedback about including Hermes/Amp hook wiring in scope.) [FEEDBACK_USED].</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread Sources/ContentView.swift
"portCount": .int(workspace.listeningPorts.count),
"unread": .int(notificationStore.unreadCount(forTabId: workspace.id)),
"tabs": .array(customSidebarSurfaceValues(workspace, focusedPanelId: focusedPanelId)),
"tabCount": .int(workspace.bonsplitController.allPaneIds.reduce(0) { $0 + workspace.bonsplitController.tabs(inPane: $1).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.

P2: tabCount counts all bonsplit tabs unconditionally, but the tabs array filters out entries where panelIdFromSurfaceId(tab.id) returns nil. When a surface has no panel mapping (possible during lifecycle transitions), tabCount will be greater than tabs.count, exposing a misleading raw count that doesn't match the actual array. Sidebar authors iterating over tabs by index or comparing counts will encounter off-by-one or out-of-bounds bugs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 10521:

<comment>tabCount counts all bonsplit tabs unconditionally, but the `tabs` array filters out entries where `panelIdFromSurfaceId(tab.id)` returns nil. When a surface has no panel mapping (possible during lifecycle transitions), `tabCount` will be greater than `tabs.count`, exposing a misleading raw count that doesn't match the actual array. Sidebar authors iterating over `tabs` by index or comparing counts will encounter off-by-one or out-of-bounds bugs.</comment>

<file context>
@@ -10506,15 +10488,102 @@ struct VerticalTabsSidebar: View {
+            "portCount": .int(workspace.listeningPorts.count),
+            "unread": .int(notificationStore.unreadCount(forTabId: workspace.id)),
+            "tabs": .array(customSidebarSurfaceValues(workspace, focusedPanelId: focusedPanelId)),
+            "tabCount": .int(workspace.bonsplitController.allPaneIds.reduce(0) { $0 + workspace.bonsplitController.tabs(inPane: $1).count }),
+        ]
+        if let description = workspace.customDescription, !description.isEmpty { fields["description"] = .string(description) }
</file context>
Suggested change
"tabCount": .int(workspace.bonsplitController.allPaneIds.reduce(0) { $0 + workspace.bonsplitController.tabs(inPane: $1).count }),
"tabCount": .int(customSidebarSurfaceValues(workspace, focusedPanelId: focusedPanelId).count),

Comment thread Sources/ContentView.swift
"portCount": .int(workspace.listeningPorts.count),
"unread": .int(notificationStore.unreadCount(forTabId: workspace.id)),
"tabs": .array(customSidebarSurfaceValues(workspace, focusedPanelId: focusedPanelId)),
"tabCount": .int(workspace.bonsplitController.allPaneIds.reduce(0) { $0 + workspace.bonsplitController.tabs(inPane: $1).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.

P2: The new tabCount line rescans all panes/tabs even though tabs was just built from the same traversal, adding avoidable repeated work in a ~1s refresh path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 10521:

<comment>The new `tabCount` line rescans all panes/tabs even though `tabs` was just built from the same traversal, adding avoidable repeated work in a ~1s refresh path.</comment>

<file context>
@@ -10506,15 +10488,102 @@ struct VerticalTabsSidebar: View {
+            "portCount": .int(workspace.listeningPorts.count),
+            "unread": .int(notificationStore.unreadCount(forTabId: workspace.id)),
+            "tabs": .array(customSidebarSurfaceValues(workspace, focusedPanelId: focusedPanelId)),
+            "tabCount": .int(workspace.bonsplitController.allPaneIds.reduce(0) { $0 + workspace.bonsplitController.tabs(inPane: $1).count }),
+        ]
+        if let description = workspace.customDescription, !description.isEmpty { fields["description"] = .string(description) }
</file context>

Comment thread Sources/ContentView.swift
"workspaces": .array(workspaces),
"workspaceCount": .int(tabManager.tabs.count),
"selectedTitle": .string(selectedWorkspace?.customTitle ?? selectedWorkspace?.title ?? ""),
"selectedId": .string(selectedId?.uuidString ?? ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: selectedId uses an empty-string sentinel for “no selection” instead of omission, which breaks the key-presence pattern this sidebar data model relies on.

(Based on your team's feedback about checking key presence instead of string sentinels.) .

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 10498:

<comment>`selectedId` uses an empty-string sentinel for “no selection” instead of omission, which breaks the key-presence pattern this sidebar data model relies on.

(Based on your team's feedback about checking key presence instead of string sentinels.) .</comment>

<file context>
@@ -10506,15 +10488,102 @@ struct VerticalTabsSidebar: View {
             "workspaces": .array(workspaces),
             "workspaceCount": .int(tabManager.tabs.count),
             "selectedTitle": .string(selectedWorkspace?.customTitle ?? selectedWorkspace?.title ?? ""),
+            "selectedId": .string(selectedId?.uuidString ?? ""),
+            "unreadTotal": .int(notificationStore.unreadCount),
             "clock": clock,
</file context>

- `window.*` — lifecycle
- `workstream.*` — start / progress / complete

Subscribe via `CmuxEventBus.subscribe(afterSequence:names:categories:)`; the socket `events` command streams them. Agent lifecycle hooks (`CLI/CMUXCLI+AgentHookDefinitions.swift`) exist for codex/grok/cursor/gemini/kiro/antigravity/hermes — session-start/prompt-submit/stop/notification/session-end/shell-exec — recorded to `~/.cmuxterm/{agent}-hook-sessions.json`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Agent-hook coverage list is inaccurate: it omits amp and uses hermes instead of the wired hermes-agent identifier, which can mislead hook setup and review scope.

(Based on your team's feedback about including Hermes/Amp hook wiring in scope.) .

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/data-driven-sidebar-plan.md, line 56:

<comment>Agent-hook coverage list is inaccurate: it omits `amp` and uses `hermes` instead of the wired `hermes-agent` identifier, which can mislead hook setup and review scope.

(Based on your team's feedback about including Hermes/Amp hook wiring in scope.) .</comment>

<file context>
@@ -0,0 +1,84 @@
+- `window.*` — lifecycle
+- `workstream.*` — start / progress / complete
+
+Subscribe via `CmuxEventBus.subscribe(afterSequence:names:categories:)`; the socket `events` command streams them. Agent lifecycle hooks (`CLI/CMUXCLI+AgentHookDefinitions.swift`) exist for codex/grok/cursor/gemini/kiro/antigravity/hermes — session-start/prompt-submit/stop/notification/session-end/shell-exec — recorded to `~/.cmuxterm/{agent}-hook-sessions.json`.
+
+## Architecture
</file context>

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:246">
P1: Comparators with an explicit `return` are evaluated as `nil`/`false` here, so `sorted { a, b in return ... }` can yield incorrect ordering. Handle `ReturnStmtSyntax` in closure evaluation before consuming the comparator result.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:288">
P3: Dead code: unreachable `IfExprSyntax` check inside `ExprSyntax` branch of `evalBlockValue`. The preceding check (`node.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self) ?? node.as(IfExprSyntax.self)`) already catches all `IfExprSyntax` nodes and either returns or continues past this point.</violation>

<violation number="3" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:300">
P2: `evalIfValue` treats non-expression conditions (e.g., `if let x = optional`, `if #available(...)`) as always false. Optional-binding conditions silently skip the if-body, which will confound authors who use them in sidebar functions.</violation>

<violation number="4" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift:366">
P2: `evalClosure2` only handles single-expression closures. Multi-statement closures with `let` declarations, `return` statements, or `if` expressions silently return `nil`. This is inconsistent with `evalBlockValue` which supports full multi-statement function bodies, and could silently corrupt `reduce` results.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:172">
P2: `resolveFont` ignores `weight:` for `.system(size:weight:design:)`, so authored font weights are silently dropped.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/RenderNodeView.swift:179">
P1: Font-style lookup matches "headline" instead of "subheadline" due to substring collision. `token.contains("headline")` returns true for the string `"subheadline"`, so `.system(.subheadline)` is resolved to `.headline`.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:75">
P1: `Button("title", action: { … })` drops the title string because the "label form" branch (triggered by finding an `action:` labeled closure argument) returns a node with no text and an empty children array, ignoring the unlabeled first argument.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:154">
P2: `registerFunctions(items, env)` inside `evalItems` writes function declarations into whichever `Environment` is passed — but for VStack, HStack, ZStack, and HSplitView, `evalCall` passes the **parent** `env` directly (not a child scope). This means any `func` defined inside a stack's trailing closure body leaks to the outer lexical scope and silently overwrites any function with the same name declared outside the stack, creating a scoping bug.</violation>

<violation number="3" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:164">
P2: `return` in view-helper bodies does not terminate statement evaluation, so subsequent statements can still render and produce incorrect duplicate output.</violation>

<violation number="4" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P3: `closureParameterName(_:)` is duplicated verbatim in both `SwiftViewInterpreter` and `ExpressionEvaluator`, each as a private method doing the same closure-signature parameter-name extraction.</violation>

<violation number="5" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift:178">
P1: Empty-string sentinel for missing item IDs in Reorderable: `item.member(idField)?.displayString ?? ""` inserts `""` into `itemIds` when the id field is absent, causing invalid reorder commands (e.g., `workspace_id: ""`) instead of a clear failure or graceful skip.</violation>
</file>

<file name="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift">

<violation number="1" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:14">
P2: Concurrency model gap widened: adding `private var functions` and mutating methods to a non-Sendable `final class` under Swift 6 strict mode. The class holds mutable state (`values`, `functions`) without `@MainActor`, actor isolation, or any concurrency primitive, violating the project's Swift 6 concurrency requirement.</violation>

<violation number="2" location="Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/Environment.swift:35">
P1: Capture and reuse the function’s defining environment when registering user functions; binding from the caller scope here gives dynamic scoping and can resolve shadowed/free variables incorrectly.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNodeKind.swift:4">
P1: Missing fallback for unknown raw values during JSON decoding — user-authored sidebar files with an unrecognized node kind (e.g., a typo like `"vstak"` or a future plugin authoring a new kind before this code adds it) will throw a hard `DecodingError.dataCorrupted` and fail to load the entire sidebar. Add a custom `Decodable` that maps unknown strings to a fallback `.unknown` case.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift:66">
P2: Drag gesture writes to `@AppStorage` on every `.onChanged` event (~60 writes/sec), causing unnecessary `UserDefaults` synchronization and potential UI jank. Use a local `@State` for the transient drag value and persist only on `.onEnded`.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift:51">
P2: Uses `FileManager.default` directly instead of an injected dependency, making the model untestable without real file I/O. The repository guidance (`CLAUDE.md`) requires package APIs to be testable without launching the app or relying on `FileManager.default`. The `reload()` method also reads files via `Data(contentsOf:)` and `String(contentsOf:encoding:)` which bypass any controllable FileManager.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLSidebarRenderer.swift:33">
P2: Button node does not apply `resolvedFont`, unlike `.text` and `.image` cases. When a JSON DSL button specifies `font` or `size`, those properties are silently ignored, while the same properties work correctly on text and image nodes. This means the button's title text will always appear at the system default font regardless of the authored style.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/OptionalStyleModifiers.swift:4">
P3: Three ViewModifier types in one file violates the repo's one-major-type-per-file convention. Split `OptionalForeground`, `OptionalPadding`, and `OptionalBackground` into separate files for consistency with the codebase convention.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLNode.swift:9">
P2: Auto-synthesized `Equatable` includes `id` (`UUID()`) in the comparison, making equality checks always return `false` for distinct instances even when semantically identical. The doc comment explicitly says `id` is "Not decoded; a stable identity per decoded node" — it is a runtime identity marker, not part of the value. Including it in `Equatable` means `==` can never be true for two separately-initialized or decoded nodes with identical content, breaking any code that relies on value equality (SwiftUI `EquatableView`, test assertions, collection diffing).</violation>
</file>

<file name="cmux.xcodeproj/project.pbxproj">

<violation number="1" location="cmux.xcodeproj/project.pbxproj:1932">
P2: CmuxSwiftRenderUI (a SwiftUI+AppKit UI-rendering package) is added as a dependency of the cmux-cli command-line tool target. Every source file in this package imports SwiftUI, and ResizableHSplit.swift imports AppKit. The cmux-cli target (product-type tool) has no UI runtime and cannot meaningfully use SwiftUI View types, which will unnecessarily link SwiftUI/AppKit frameworks into the CLI binary and create a maintenance burden.</violation>
</file>

<file name="Sources/DSLSidebarPlayground.swift">

<violation number="1" location="Sources/DSLSidebarPlayground.swift:31">
P2: Unconditionally coercing all numeric-looking string params to Int can break commands that require string-typed values.</violation>
</file>

<file name="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift">

<violation number="1" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:18">
P1: ForEach uses index-based identity (`id: \.offset`) in a reorderable list, even though stable per-item identifiers are available via `spec.itemIds`. When a row is dragged to a new position and `rows` updates, SwiftUI identifies each row by its index (0, 1, 2...) rather than by its stable id. This prevents SwiftUI from tracking which view corresponds to which item across the reorder, losing view state, breaking transition animations, and making the reorder feel unresponsive or glitchy.

Use stable IDs from `spec.itemIds` as the ForEach identity, paired with each row, so SwiftUI correctly preserves view identity when items move.</violation>

<violation number="2" location="Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ReorderableList.swift:37">
P2: Validate `ReorderSpec` command/key strings (and `draggedId`) before dispatching `.cmux`; currently malformed or empty dynamic keys can produce invalid reorder commands.

(Based on your team's feedback about avoiding sentinel/missing-key patterns in cmux param construction.) [FEEDBACK_USED].</violation>
</file>

<file name="CLI/cmux.swift">

<violation number="1" location="CLI/cmux.swift:31283">
P2: Inconsistent docs topic list: main CLI help advertises `sidebars` but the `docs` command's own usage string and error message still show the old topic list without it. Users running `cmux docs --help` or triggering the extra-args error path will see `docs [settings|shortcuts|api|browser|agents|dock]` — no mention of sidebars.</violation>
</file>

<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:10498">
P2: `selectedId` uses an empty-string sentinel for “no selection” instead of omission, which breaks the key-presence pattern this sidebar data model relies on.

(Based on your team's feedback about checking key presence instead of string sentinels.) [FEEDBACK_USED].</violation>

<violation number="2" location="Sources/ContentView.swift:10521">
P2: tabCount counts all bonsplit tabs unconditionally, but the `tabs` array filters out entries where `panelIdFromSurfaceId(tab.id)` returns nil. When a surface has no panel mapping (possible during lifecycle transitions), `tabCount` will be greater than `tabs.count`, exposing a misleading raw count that doesn't match the actual array. Sidebar authors iterating over `tabs` by index or comparing counts will encounter off-by-one or out-of-bounds bugs.</violation>

<violation number="3" location="Sources/ContentView.swift:10521">
P2: The new `tabCount` line rescans all panes/tabs even though `tabs` was just built from the same traversal, adding avoidable repeated work in a ~1s refresh path.</violation>
</file>

<file name="docs/data-driven-sidebar-plan.md">

<violation number="1" location="docs/data-driven-sidebar-plan.md:56">
P2: Agent-hook coverage list is inaccurate: it omits `amp` and uses `hermes` instead of the wired `hermes-agent` identifier, which can mislead hook setup and review scope.

(Based on your team's feedback about including Hermes/Amp hook wiring in scope.) [FEEDBACK_USED].</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

var result: [SwiftValue] = []
for value in values {
var insertAt = result.count
for index in result.indices where evalClosure2(closure, value, result[index], env)?.isTruthy ?? false {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Comparators with an explicit return are evaluated as nil/false here, so sorted { a, b in return ... } can yield incorrect ordering. Handle ReturnStmtSyntax in closure evaluation before consuming the comparator result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift, line 246:

<comment>Comparators with an explicit `return` are evaluated as `nil`/`false` here, so `sorted { a, b in return ... }` can yield incorrect ordering. Handle `ReturnStmtSyntax` in closure evaluation before consuming the comparator result.</comment>

<file context>
@@ -211,7 +235,21 @@ struct ExpressionEvaluator {
+                var result: [SwiftValue] = []
+                for value in values {
+                    var insertAt = result.count
+                    for index in result.indices where evalClosure2(closure, value, result[index], env)?.isTruthy ?? false {
+                        insertAt = index
+                        break
</file context>

Comment on lines +164 to +172
} else if let ret = node.as(ReturnStmtSyntax.self), let expr = ret.expression {
// A view helper with an explicit `return SomeView` (or
// `return ForEach(...) { }`) renders its returned expression,
// not nothing.
if let call = expr.as(FunctionCallExprSyntax.self), isForEach(call) {
out += evalForEach(call, env)
} else if let child = evalView(expr, env) {
out.append(child)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: return in view-helper bodies does not terminate statement evaluation, so subsequent statements can still render and produce incorrect duplicate output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift, line 164:

<comment>`return` in view-helper bodies does not terminate statement evaluation, so subsequent statements can still render and produce incorrect duplicate output.</comment>

<file context>
@@ -161,6 +161,15 @@ public struct SwiftViewInterpreter: Sendable {
                 out += evalFor(loop, env)
             } else if let ifExpr = ifExpression(node) {
                 out += evalIf(ifExpr, env)
+            } else if let ret = node.as(ReturnStmtSyntax.self), let expr = ret.expression {
+                // A view helper with an explicit `return SomeView` (or
+                // `return ForEach(...) { }`) renders its returned expression,
</file context>
Suggested change
} else if let ret = node.as(ReturnStmtSyntax.self), let expr = ret.expression {
// A view helper with an explicit `return SomeView` (or
// `return ForEach(...) { }`) renders its returned expression,
// not nothing.
if let call = expr.as(FunctionCallExprSyntax.self), isForEach(call) {
out += evalForEach(call, env)
} else if let child = evalView(expr, env) {
out.append(child)
}
} else if let ret = node.as(ReturnStmtSyntax.self), let expr = ret.expression {
// A view helper with an explicit `return SomeView` (or
// `return ForEach(...) { }`) renders its returned expression,
// then exits the helper body.
if let call = expr.as(FunctionCallExprSyntax.self), isForEach(call) {
return out + evalForEach(call, env)
} else if let child = evalView(expr, env) {
out.append(child)
}
return out

@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: 1

♻️ Duplicate comments (5)
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift (2)

62-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

JSON sidebar actions are silently dropped.

The .json path constructs DSLSidebarRenderer(node: document.root) { _ in }, discarding any DSLAction from interactive JSON nodes. Route JSON actions through the same dispatch mechanism used by the .swiftSource path.

🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift`
around lines 62 - 63, The .json case currently creates DSLSidebarRenderer(node:
document.root) with an empty closure, dropping DSLAction events; change it to
forward actions to the same dispatch used in the .swiftSource path by passing
the dispatch function as the renderer's action handler (i.e., replace the { _ in
} closure with the existing dispatch handler used for .swiftSource so
DSLSidebarRenderer(node: document.root) calls dispatch when it receives a
DSLAction).

64-78: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Swift source is re-parsed on every body render.

SwiftViewInterpreter().evaluate(source, state: dataContext) parses and folds the AST on each dataContext change. Since TimelineView ticks every second (per the host mounting at Sources/ContentView.swift:11141), this re-parses every second even when the source hasn't changed.

Cache the parsed AST in CustomSidebarModel and evaluate only against dataContext here.

🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift`
around lines 64 - 78, The view is reparsing Swift source on every render via
SwiftViewInterpreter().evaluate(source, state: dataContext); move
parsing/folding into CustomSidebarModel so the AST/node is cached and only
re-evaluated against dataContext here. Add a cachedParsedNode (or similar) on
CustomSidebarModel that performs the expensive parse when the source changes,
expose a method/property like evaluateCachedNode(state:) or
evaluate(node:cachedNode, state:dataContext) so CustomSidebarView uses
model.cachedParsedNode (or model.evaluateCachedNode(dataContext)) instead of
creating a new SwiftViewInterpreter each render; update CustomSidebarView to
call that cached-evaluate path and keep the scrollWrap/hsplit logic unchanged.
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift (1)

78-78: ⚠️ Potential issue | 🟡 Minor | 💤 Low value

Cursor may remain stuck after drag ends outside hover bounds.

The onHover handler won't fire an exit event if the user drags outside the divider bounds, leaving the cursor as resizeLeftRight. Reset the cursor in .onEnded:

Proposed fix
             .onEnded { _ in
+                NSCursor.arrow.set()
                 dragStartFraction = nil
             }
🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift`
at line 78, The drag end handler only clears dragStartFraction but doesn't reset
the cursor if the user releases outside hover bounds, so update the .onEnded
closure (where dragStartFraction is set to nil) to also restore the cursor to
the default by undoing whatever the onHover handler did (e.g., if onHover pushes
NSCursor.resizeLeftRight, pop or set NSCursor.arrow back). Reference the
existing drag end closure and the onHover cursor logic (the resizeLeftRight
cursor) and ensure both dragStartFraction = nil and the cursor reset are
performed.
Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift (1)

80-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the user-facing decoding-error messages.

describe(_:) returns hardcoded English ("Missing key …", "Type mismatch at …", etc.) that flows into state.failed and is rendered verbatim by CustomSidebarView.errorView. As per coding guidelines, Swift UI text must use String(localized:defaultValue:).

🤖 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/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift`
around lines 80 - 96, The decoding error messages in describe(_:) are
user-facing and must be localized; update describe(_ error: Error) to wrap each
returned message using String(localized: , defaultValue: ) (e.g., for the
"Missing key …", "Type mismatch …", "Missing value …", "Invalid JSON …" branches
and the `@unknown/default` case) so the strings are localized before they flow
into state.failed and are shown by CustomSidebarView.errorView; keep the
existing interpolation (key.stringValue, path(ctx), ctx.debugDescription) but
pass the composed message as the localized string's defaultValue and provide a
suitable localization key or comment as needed.
Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift (1)

37-49: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add recursion-depth guard to prevent stack overflow from user input.

The interpreter still has no depth limit: evaluate → evalView → evalCall → evalItems → evalFor/evalIf/evalForEach → back to evalItems, and evalIf recursively handles else-if chains. Deeply nested sidebar source (200-level if/else if or member chains) will overflow the stack and crash the app. Thread a depth counter through the call chain and fail gracefully past a threshold (e.g., 100).

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`
around lines 37 - 49, Add a recursion-depth guard propagated through the
interpreter call chain: extend evaluate to accept/initialize a depth counter
(start at 0) and thread it into evalView, evalCall, evalItems, evalFor, evalIf,
evalForEach (and any other recursive eval functions), incrementing at each
recursive entry and returning a graceful failure (nil or a specific error node)
once a threshold (e.g., 100) is exceeded; ensure every call site of
evalView/evalCall/evalItems/etc. is updated to pass the incremented depth so
deep user input cannot overflow the stack.
🤖 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/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift`:
- Around line 344-356: The test integerDivisionByZeroDoesNotCrash currently only
asserts the VStack exists and the last child is "ok"; update it to assert the
exact children behavior for node (the value returned by interp.evaluate) so the
soft-fail is verified: inspect the current interpreter behavior and then add
either (A) an assertion that node?.children.count == 1 and
node?.children.first?.text == "ok" if zero-divisions are dropped, or (B)
assertions that node?.children.count == 3 and node?.children[0].text == "v=",
node?.children[1].text == "m=", and node?.children[2].text == "ok" if the
interpolations render empty strings; use the test function
integerDivisionByZeroDoesNotCrash and the node/children/text properties to
locate and implement the correct assertion variant.

---

Duplicate comments:
In `@Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift`:
- Around line 37-49: Add a recursion-depth guard propagated through the
interpreter call chain: extend evaluate to accept/initialize a depth counter
(start at 0) and thread it into evalView, evalCall, evalItems, evalFor, evalIf,
evalForEach (and any other recursive eval functions), incrementing at each
recursive entry and returning a graceful failure (nil or a specific error node)
once a threshold (e.g., 100) is exceeded; ensure every call site of
evalView/evalCall/evalItems/etc. is updated to pass the incremented depth so
deep user input cannot overflow the stack.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift`:
- Line 78: The drag end handler only clears dragStartFraction but doesn't reset
the cursor if the user releases outside hover bounds, so update the .onEnded
closure (where dragStartFraction is set to nil) to also restore the cursor to
the default by undoing whatever the onHover handler did (e.g., if onHover pushes
NSCursor.resizeLeftRight, pop or set NSCursor.arrow back). Reference the
existing drag end closure and the onHover cursor logic (the resizeLeftRight
cursor) and ensure both dragStartFraction = nil and the cursor reset are
performed.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift`:
- Around line 80-96: The decoding error messages in describe(_:) are user-facing
and must be localized; update describe(_ error: Error) to wrap each returned
message using String(localized: , defaultValue: ) (e.g., for the "Missing key
…", "Type mismatch …", "Missing value …", "Invalid JSON …" branches and the
`@unknown/default` case) so the strings are localized before they flow into
state.failed and are shown by CustomSidebarView.errorView; keep the existing
interpolation (key.stringValue, path(ctx), ctx.debugDescription) but pass the
composed message as the localized string's defaultValue and provide a suitable
localization key or comment as needed.

In
`@Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift`:
- Around line 62-63: The .json case currently creates DSLSidebarRenderer(node:
document.root) with an empty closure, dropping DSLAction events; change it to
forward actions to the same dispatch used in the .swiftSource path by passing
the dispatch function as the renderer's action handler (i.e., replace the { _ in
} closure with the existing dispatch handler used for .swiftSource so
DSLSidebarRenderer(node: document.root) calls dispatch when it receives a
DSLAction).
- Around line 64-78: The view is reparsing Swift source on every render via
SwiftViewInterpreter().evaluate(source, state: dataContext); move
parsing/folding into CustomSidebarModel so the AST/node is cached and only
re-evaluated against dataContext here. Add a cachedParsedNode (or similar) on
CustomSidebarModel that performs the expensive parse when the source changes,
expose a method/property like evaluateCachedNode(state:) or
evaluate(node:cachedNode, state:dataContext) so CustomSidebarView uses
model.cachedParsedNode (or model.evaluateCachedNode(dataContext)) instead of
creating a new SwiftViewInterpreter each render; update CustomSidebarView to
call that cached-evaluate path and keep the scrollWrap/hsplit logic unchanged.
🪄 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: f2a0e23e-852f-454a-a4a9-a3faaab08aa2

📥 Commits

Reviewing files that changed from the base of the PR and between 261d051 and 283987f.

📒 Files selected for processing (23)
  • CLI/cmux.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ActionCommand.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ButtonAction.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ModifierArg.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderModifier.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ReorderSpec.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
  • Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
  • Packages/CmuxSwiftRenderUI/Package.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/CustomSidebarContentInsets.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Rendering/ResizableHSplit.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift
  • Sources/CmuxSidebarActionDispatch.swift
  • Sources/ContentView.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • docs/custom-sidebars.md
  • docs/data-driven-sidebar-plan.md
  • docs/swiftui-interpreter-surface.md
💤 Files with no reviewable changes (6)
  • docs/data-driven-sidebar-plan.md
  • docs/custom-sidebars.md
  • docs/swiftui-interpreter-surface.md
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/RenderNode.swift
  • Sources/CmuxSidebarActionDispatch.swift
  • cmux.xcodeproj/project.pbxproj

Assert the interpolation drops the nil division/modulo segment ("v=", "m=")
rather than only checking a later row renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Strengthened the div-by-zero test to assert the soft-fail output (["v=", "m=", "ok"]) per the review. That was the only new finding. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

@azooz2003-bit Great — asserting the concrete soft-fail rows (["v=", "m=", "ok"]) makes the test a real regression guard rather than just a crash check. Kicking off a fresh review now.

(ミ・ω・)ミ


🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-05-05T07:01:16.438Z
Learning: In the cmux macOS project (manaflow-ai/cmux), a deferred/async resync approach for portal geometry (terminal and browser portals) after sidebar visibility toggles was tested and explicitly reverted because it could leave the right sidebar path stale until a later command. The correct fix uses state-owned visibility revisions plus an immediate terminal-host geometry sync request (via `synchronizeExternalGeometryNow(for:)`) for the next layout pass in `synchronizePortalGeometry(immediately:)` in ContentView.swift.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-02T22:01:27.544Z
Learning: Applies to **/*.swift : Fail when diffs to production Swift changes introduce or materially worsen Swift 6 actor isolation mistakes from `.github/review-bot-rules/swift-actor-isolation.md`: implicit MainActor value models or service protocols, shared mutable Sendable reference types without isolation, or UI-bound stores accessed from background contexts

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-01T11:38:13.313Z
Learning: Applies to **/*.swift : For production Swift changes, fail when the diff introduces or materially worsens Swift 6 actor isolation mistakes from `.github/review-bot-rules/swift-actor-isolation.md`: implicit MainActor value models or service protocols, shared mutable Sendable reference types without isolation, or UI-bound stores accessed from background contexts

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-28T22:24:18.018Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift (PR `#3237`, commit 9dcb9c2), the explicit-vs-baseConfig.initialInput resolution logic was extracted from `TerminalSurface.createSurface(for:)` into a standalone `TerminalSurface.resolveInitialInput(...)` static/instance method for unit-testability. Behavior is unchanged. A companion `TerminalSurfaceResolveInitialInputTests` suite asserts the contract, including byte-for-byte preservation of Ghostty raw-bytes startup-input via `baseConfig.initialInput`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T18:01:17.320Z
Learning: In manaflow-ai/cmux, extracting dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.) from Sources/WorkspaceContentView.swift into dedicated source files is explicitly deferred as a non-blocking follow-up. Do not raise this as a blocking review comment on PRs that add dock views to WorkspaceContentView.swift; the snapshot/actions decomposition (WorkspaceDockPaneSnapshot, WorkspaceDockPaneActions) already addresses the correctness risk for SwiftUI invalidation, and file splitting has no runtime impact.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-01T11:38:13.313Z
Learning: Applies to **/*.swift : For cmux-owned Swift code, fail when the diff introduces or materially expands legacy async patterns from `.github/review-bot-rules/swift-concurrency-modernization.md`: background Dispatch queues for ordinary async work, new Combine app state, completion-handler APIs where async throws is under our control, or fire-and-forget Tasks with real lifecycle

Learnt from: tranquillum
Repo: manaflow-ai/cmux PR: 2827
File: Sources/Sidebar/ProviderAccountsFooterPanel.swift:174-198
Timestamp: 2026-04-17T15:46:53.298Z
Learning: Repo: manaflow-ai/cmux — Sources/Sidebar/ProviderAccountsFooterPanel.swift + Sources/Sidebar/ProviderAccountsPopover.swift (PR `#2827`):
- `ProviderStatusRanking.impactSeverity(_:)` intentionally returns `-1` for any unrecognized/`"none"` impact string.
- When `worstImpactSeverity == -1` (every incident in the list has an unknown/none impact), `ProviderStatusLabel.statusText` and `.dotColor` fall through to the `default` branch, which shows "Operational" / green.
- This is a deliberate design choice: `StatuspageIOFetcher.fetch` already defaults missing impact fields to `"none"`, so the only way to reach `worstImpactSeverity == -1` is a genuinely benign incident array; showing "Operational" here avoids false-positive yellow.
- The popover always surfaces the full raw incident rows independently, so no incident is invisible to the user.
- Do NOT flag the `-1 → Operational` fallback as a bug or suggest raising unknown impacts to `minor` severity.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-02T22:35:15.166Z
Learning: Applies to **/*.swift : Fail when diffs to cmux-owned Swift code introduce or materially expand legacy async patterns from `.github/review-bot-rules/swift-concurrency-modernization.md`: background Dispatch queues for ordinary async work, new Combine app state, completion-handler APIs where async throws is under our control, or fire-and-forget Tasks with real lifecycle

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3139
File: Sources/Panels/FilePreviewPanel.swift:534-537
Timestamp: 2026-04-28T05:45:34.719Z
Learning: Repo: manaflow-ai/cmux — Sources/Workspace.swift & Sources/Panels/FilePreviewPanel.swift (PR `#3139`): The shared close-confirmation gate in Workspace uses a generic `panel.isDirty` check (line ~8703) that applies to all panel types, including FilePreviewPanel. FilePreviewPanel exposes `Published private(set) var isDirty` and installFilePreviewPanelSubscription keeps the Bonsplit tab's isDirty flag in sync with it. All three teardown paths (tab close, pane collapse, workspace close) go through this gate, so no special-casing is needed for FilePreviewPanel's dirty state on teardown.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:27.511Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3247
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-29T01:08:44.497Z
Learning: Repo: manaflow-ai/cmux — Behavior contract: Sidebar “Copy Workspace ID(s)” (e.g., in Sources/ContentView.swift TabItemView context menu) must copy plain UUIDs (IDs-only), not refs. Command palette identifier-copy commands should explicitly pass includeRefs: true when refs are desired. This split preserves backward compatibility for scripts while enabling richer payloads in the palette.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T17:59:46.216Z
Learning: In manaflow-ai/cmux, when large SwiftUI views (e.g., Sources/WorkspaceContentView.swift) gain new dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.), extracting them into separate source files is a non-blocking follow-up refactor. The primary correctness concern for SwiftUI list/lazy-stack row invalidation should be addressed in-place (e.g., via snapshot value types like WorkspaceDockPaneSnapshot and action closures like WorkspaceDockPaneActions), and the file-split itself can be deferred without changing runtime behavior.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3179
File: Sources/ContentView.swift:4005-4021
Timestamp: 2026-04-27T11:59:15.622Z
Learning: Repo: manaflow-ai/cmux — PR `#3179` — Sources/ContentView.swift
Learning: For separate-surface transparent windows, installNativeTitlebarBackdrop intentionally inserts NativeTitlebarBackdropView below contentView (NSHostingView) — positioned .below relativeTo: contentView — so the backdrop fills through transparent hosting without tinting SwiftUI titlebar text/buttons. The titlebar‑chrome ancestor fallback is only for edge cases where contentView isn’t directly under the NSThemeFrame. This layering was pixel‑verified in PR `#3179`; do not “fix” it to sit below the titlebar chrome on this path.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3568
File: GhosttyTabs.xcodeproj/project.pbxproj:1588-1590
Timestamp: 2026-05-05T20:41:12.478Z
Learning: In the manaflow-ai/cmux repository, the SwiftPM package-boundary extraction of `JSONCValueEditor.swift` and `CmuxSettingsManagedValues.swift` into a dedicated Foundation-only package is intentionally deferred to a separate refactor PR. For the regression/fix PR (issue `#3551`, PR `#3568`), these files are scoped to the app target and covered by behavioral CI tests. Do not flag this as an unresolved issue within the context of that PR.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2889-2911
Timestamp: 2026-04-14T19:59:54.878Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — On macOS 26, the NavigationSplitView sidebar width is synchronized back to model state via a GeometryReader that updates sidebarWidth and SidebarState.persistedWidth on width changes. Do not flag “write-only sidebar width” drift for this path going forward.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3988
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-24T03:40:53.759Z
Learning: In manaflow-ai/cmux (Swift), compute and persist sidebar selection anchors against the rendered order, not raw visible IDs. Specifically, inside Sources/ContentView.swift’s VerticalTabsSidebar, derive lastSidebarSelectionIndex from currentRenderedWorkspaceIdsForSidebarSelection() (which respects collapsedGroups) via syncLastSidebarSelectionIndexForCurrentRenderedOrder(_:). Do not use SidebarWorkspaceGroupingPlanner.plan(...).visibleWorkspaceIds outside this context, and clear the index when the selected workspace isn’t rendered.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment on lines +312 to +328
private func evalIf(_ ifExpr: IfExprSyntax, _ env: Environment) -> [RenderNode] {
let taken = ifExpr.conditions.allSatisfy { element in
guard let expr = element.condition.as(ExprSyntax.self) else { return false }
return expressions.eval(expr, env)?.isTruthy ?? false
}
if taken {
return evalItems(ifExpr.body.statements, env.makeChild())
}
guard let elseBody = ifExpr.elseBody else { return [] }
if let block = elseBody.as(CodeBlockSyntax.self) {
return evalItems(block.statements, env.makeChild())
}
if let elseIf = elseBody.as(IfExprSyntax.self) {
return evalIf(elseIf, env)
}
return []
}

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 if let optional-binding conditions always evaluate to false

evalIf casts every condition element to ExprSyntax. An if let condition uses OptionalBindingConditionSyntax, so the cast fails and allSatisfy returns false, which means the body of any if let block is silently never rendered. The docs at docs/custom-sidebars.md explicitly recommend if let b = w.branch { ... } as the idiom for optional workspace fields — authors following that pattern will always get an empty view with no feedback. The ternary form (w.branch != nil ? ... : ...) is the only working alternative today, but it isn't mentioned as the only supported form and can't bind the unwrapped value cleanly.

…ions

Two review findings on the custom-sidebar render path:

- The Swift source was re-parsed every render. The host mounts the sidebar
  under a TimelineView that ticks every second, so SwiftViewInterpreter
  re-parsed and operator-folded the AST on every tick even when the source
  hadn't changed. Split the interpreter into parse() -> ParsedProgram and
  evaluate(ParsedProgram, state:); CustomSidebarModel caches the parse keyed
  by source and re-evaluates only against the live dataContext. File-change
  reload swaps in new source, which invalidates the cache on next render.

- JSON sidebar actions were silently dropped: the .json path constructed
  DSLSidebarRenderer with an empty action closure. Route them through the
  same SidebarActionDispatch the interpreted path uses, via a
  DSLAction.buttonAction mapping (log/openURL/cmux-method).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cubic-dev-ai

cubic-dev-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Addressed both substantive findings in c87fdf7:

  1. Swift source re-parsed every render. Split SwiftViewInterpreter into parse(_:) -> ParsedProgram (the expensive, source-only step) and evaluate(_ program:state:). CustomSidebarModel now caches the parse keyed by source and re-evaluates only against the live dataContext, so the per-second TimelineView tick no longer re-parses unchanged source. File-change reload swaps in new source, invalidating the cache on the next render.
  2. JSON sidebar actions silently dropped. The .json path now routes node actions through the same SidebarActionDispatch the interpreted path uses, via a DSLAction.buttonAction mapping (log / openURL / cmux-method).

Both packages build clean; 34 CmuxSwiftRender tests pass.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

@azooz2003-bit Both fixes look well-targeted:

  • The parse(_:) → ParsedProgram / evaluate(_ program:state:) split with a source-keyed cache in CustomSidebarModel is a clean solution — the expensive swift-syntax parse happens once per file version, and the per-second TimelineView tick only drives the cheap evaluation pass against the live data context.
  • Routing JSON sidebar DSLAction through SidebarActionDispatch via a buttonAction mapping closes the silent-drop bug and gives both rendering paths consistent action dispatch semantics.

Kicking off a fresh review now.

(ミ・ω・)ミ


🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3139
File: Sources/Panels/FilePreviewPanel.swift:534-537
Timestamp: 2026-04-28T05:45:34.719Z
Learning: Repo: manaflow-ai/cmux — Sources/Workspace.swift & Sources/Panels/FilePreviewPanel.swift (PR `#3139`): The shared close-confirmation gate in Workspace uses a generic `panel.isDirty` check (line ~8703) that applies to all panel types, including FilePreviewPanel. FilePreviewPanel exposes `Published private(set) var isDirty` and installFilePreviewPanelSubscription keeps the Bonsplit tab's isDirty flag in sync with it. All three teardown paths (tab close, pane collapse, workspace close) go through this gate, so no special-casing is needed for FilePreviewPanel's dirty state on teardown.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3247
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-29T01:08:44.497Z
Learning: Repo: manaflow-ai/cmux — Behavior contract: Sidebar “Copy Workspace ID(s)” (e.g., in Sources/ContentView.swift TabItemView context menu) must copy plain UUIDs (IDs-only), not refs. Command palette identifier-copy commands should explicitly pass includeRefs: true when refs are desired. This split preserves backward compatibility for scripts while enabling richer payloads in the palette.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-05-05T07:01:16.438Z
Learning: In the cmux macOS project (manaflow-ai/cmux), a deferred/async resync approach for portal geometry (terminal and browser portals) after sidebar visibility toggles was tested and explicitly reverted because it could leave the right sidebar path stale until a later command. The correct fix uses state-owned visibility revisions plus an immediate terminal-host geometry sync request (via `synchronizeExternalGeometryNow(for:)`) for the next layout pass in `synchronizePortalGeometry(immediately:)` in ContentView.swift.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3988
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-24T03:40:53.759Z
Learning: In manaflow-ai/cmux (Swift), compute and persist sidebar selection anchors against the rendered order, not raw visible IDs. Specifically, inside Sources/ContentView.swift’s VerticalTabsSidebar, derive lastSidebarSelectionIndex from currentRenderedWorkspaceIdsForSidebarSelection() (which respects collapsedGroups) via syncLastSidebarSelectionIndexForCurrentRenderedOrder(_:). Do not use SidebarWorkspaceGroupingPlanner.plan(...).visibleWorkspaceIds outside this context, and clear the index when the selected workspace isn’t rendered.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T18:01:17.320Z
Learning: In manaflow-ai/cmux, extracting dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.) from Sources/WorkspaceContentView.swift into dedicated source files is explicitly deferred as a non-blocking follow-up. Do not raise this as a blocking review comment on PRs that add dock views to WorkspaceContentView.swift; the snapshot/actions decomposition (WorkspaceDockPaneSnapshot, WorkspaceDockPaneActions) already addresses the correctness risk for SwiftUI invalidation, and file splitting has no runtime impact.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:27.511Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T17:59:46.216Z
Learning: In manaflow-ai/cmux, when large SwiftUI views (e.g., Sources/WorkspaceContentView.swift) gain new dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.), extracting them into separate source files is a non-blocking follow-up refactor. The primary correctness concern for SwiftUI list/lazy-stack row invalidation should be addressed in-place (e.g., via snapshot value types like WorkspaceDockPaneSnapshot and action closures like WorkspaceDockPaneActions), and the file-split itself can be deferred without changing runtime behavior.

Learnt from: say8425
Repo: manaflow-ai/cmux PR: 3680
File: Sources/ContentView.swift:7880-7886
Timestamp: 2026-05-08T02:39:09.527Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, the command-palette action "palette.toggleRightSidebar" must mutate the per-ContentView EnvironmentObject FileExplorerState by calling fileExplorerState.toggle() rather than routing through a global active-window toggle. This keeps the action consistent with CommandPaletteContextSnapshot.rightSidebarVisible and prevents multi-window desync.

Learnt from: nanami-he
Repo: manaflow-ai/cmux PR: 4633
File: Sources/ContentView.swift:14060-14062
Timestamp: 2026-05-23T08:40:19.510Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, TabItemView rows under Lazy stacks may hold plain references to stores (e.g., TabManager, TerminalNotificationStore) as long as (1) the body renders from immutable snapshots only, (2) WorkspaceContextMenuOverlay receives value snapshots (no ObservableObject refs), and (3) all mutations happen solely inside action handlers (e.g., handleMenuAction(_:)). Do not request lifting those handlers to a parent when these conditions are met.

Learnt from: pgbezerra
Repo: manaflow-ai/cmux PR: 3307
File: Sources/Workspace.swift:7832-7836
Timestamp: 2026-04-30T11:55:02.455Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, when the last pane closes in a workspace created with closePanesOnInitialCommandExit = true (SSH/custom-program profiles), the teardown should skip createReplacementTerminalPanel() and allow the workspace to close cleanly. A follow-up issue will track gating the last-pane path in splitTabBar(_:didCloseTab:fromPane:) on closePanesOnInitialCommandExit.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: .github/review-bot-rules/swiftui-state-layout.md:0-0
Timestamp: 2026-06-01T11:37:30.652Z
Learning: Applies to **/*.swift : Large list and sidebar rows must receive value snapshots and closures rather than store references to prevent re-rendering every row and CPU spin loops (cmux-specific)

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4825
File: Sources/AppDelegate.swift:921-948
Timestamp: 2026-05-26T21:44:44.637Z
Learning: In manaflow-ai/cmux PRs that make targeted fixes to AppDelegate-owned session restore, main-window registry, autosave, or display/window geometry behavior, do not insist on extracting new helper/cache logic from Sources/AppDelegate.swift when doing so would broaden the PR into an ownership refactor. It is acceptable to keep narrowly scoped geometry-cache changes in AppDelegate for a targeted bug fix if the implementation tightens boundaries with typed transition sources and guarded update paths; suggest extraction only as an optional follow-up.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2889-2911
Timestamp: 2026-04-14T19:59:54.878Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — On macOS 26, the NavigationSplitView sidebar width is synchronized back to model state via a GeometryReader that updates sidebarWidth and SidebarState.persistedWidth on width changes. Do not flag “write-only sidebar width” drift for this path going forward.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3568
File: GhosttyTabs.xcodeproj/project.pbxproj:1588-1590
Timestamp: 2026-05-05T20:41:12.478Z
Learning: In the manaflow-ai/cmux repository, the SwiftPM package-boundary extraction of `JSONCValueEditor.swift` and `CmuxSettingsManagedValues.swift` into a dedicated Foundation-only package is intentionally deferred to a separate refactor PR. For the regression/fix PR (issue `#3551`, PR `#3568`), these files are scoped to the app target and covered by behavioral CI tests. Do not flag this as an unresolved issue within the context of that PR.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/AppDelegate.swift:2196-2200
Timestamp: 2026-04-03T03:36:45.112Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate.swift, when KeyboardShortcutSettings.didChangeNotification fires, AppDelegate must clear configured-chord caches (pendingConfiguredShortcutChord and activeConfiguredShortcutChordPrefixForCurrentEvent) via clearConfiguredShortcutChordState() before refreshing tooltips/UI. Also clear chord state on applicationWillResignActive to avoid cross-activity leakage. Verified by cmuxTests/AppDelegateShortcutRoutingTests.swift::testShortcutChangeClearsPendingConfiguredChord.

Learnt from: tranquillum
Repo: manaflow-ai/cmux PR: 2827
File: Sources/Sidebar/ProviderAccountsFooterPanel.swift:174-198
Timestamp: 2026-04-17T15:46:53.298Z
Learning: Repo: manaflow-ai/cmux — Sources/Sidebar/ProviderAccountsFooterPanel.swift + Sources/Sidebar/ProviderAccountsPopover.swift (PR `#2827`):
- `ProviderStatusRanking.impactSeverity(_:)` intentionally returns `-1` for any unrecognized/`"none"` impact string.
- When `worstImpactSeverity == -1` (every incident in the list has an unknown/none impact), `ProviderStatusLabel.statusText` and `.dotColor` fall through to the `default` branch, which shows "Operational" / green.
- This is a deliberate design choice: `StatuspageIOFetcher.fetch` already defaults missing impact fields to `"none"`, so the only way to reach `worstImpactSeverity == -1` is a genuinely benign incident array; showing "Operational" here avoids false-positive yellow.
- The popover always surfaces the full raw incident rows independently, so no incident is invisible to the user.
- Do NOT flag the `-1 → Operational` fallback as a bug or suggest raising unknown impacts to `minor` severity.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3179
File: Sources/ContentView.swift:4005-4021
Timestamp: 2026-04-27T11:59:15.622Z
Learning: Repo: manaflow-ai/cmux — PR `#3179` — Sources/ContentView.swift
Learning: For separate-surface transparent windows, installNativeTitlebarBackdrop intentionally inserts NativeTitlebarBackdropView below contentView (NSHostingView) — positioned .below relativeTo: contentView — so the backdrop fills through transparent hosting without tinting SwiftUI titlebar text/buttons. The titlebar‑chrome ancestor fallback is only for edge cases where contentView isn’t directly under the NSThemeFrame. This layering was pixel‑verified in PR `#3179`; do not “fix” it to sit below the titlebar chrome on this path.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3608
File: Sources/Panels/FilePreviewFileWatcher.swift:1-202
Timestamp: 2026-05-06T15:57:20.716Z
Learning: In manaflow-ai/cmux, do not recommend extracting `FilePreviewTextFileWatcher` (Sources/Panels/FilePreviewFileWatcher.swift) or other file-preview-scoped DispatchSource watchers into a SwiftPM package as part of the file preview feature PR. The watcher is intentionally kept app-target-scoped in Sources/Panels/ because it is tightly coupled to the file preview panel lifecycle. A broader DispatchSource watcher consolidation (covering MarkdownPanel.swift, KeyboardShortcutSettingsFileStore.swift, TerminalController.swift, FileExplorerStore.swift, CmuxConfig.swift) is a separate API design and migration effort with its own risk profile and should be tracked as a dedicated follow-up.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-08T03:36:30.160Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.FileBrowserDrawerState threading pattern (PR `#1909`, commit e0e57809): FileBrowserDrawerState must be threaded through AppDelegate.configure() as a weak stored property (matching the sidebarState pattern), passed through both configure() call sites, with registerMainWindow parameter made non-optional. The fallback `?? FileBrowserDrawerState()` must NOT be used as it creates detached instances that are not properly owned by the window context.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-01T11:38:13.313Z
Learning: Applies to **/*.swift : For Swift changes that add or materially change standalone cmux-owned windows, fail when the diff violates `.github/review-bot-rules/swift-auxiliary-window-close-shortcuts.md`: user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code without a stable cmux.* identifier and shared close-shortcut ownership through cmuxAuxiliaryWindowIdentifiers
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​aws-sdk/​rds-signer@​3.1045.0991009298100

View full report

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 3, 2026

@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: 1

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/ParsedProgram.swift`:
- Around line 17-24: ParsedProgram lacks Sendable conformance while
SwiftViewInterpreter (which produces ParsedProgram via parse(_:)) is Sendable
and ParsedProgram instances are cached and passed across actor boundaries (e.g.
CustomSidebarModel); make ParsedProgram conform to Sendable by declaring it
Sendable (add : Sendable to the ParsedProgram declaration) — since
SourceFileSyntax is already Sendable this is safe and requires no further
changes to the init or stored property `file`.
🪄 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: 04a84abf-486b-48ce-8b97-17a35d2f47fe

📥 Commits

Reviewing files that changed from the base of the PR and between 651d2a5 and c87fdf7.

📒 Files selected for processing (5)
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/ParsedProgram.swift
  • Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/JSON/DSLAction.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarModel.swift
  • Packages/CmuxSwiftRenderUI/Sources/CmuxSwiftRenderUI/Sidebar/CustomSidebarView.swift

Comment on lines +17 to +24
public struct ParsedProgram {
/// The folded syntax tree the interpreter walks.
let file: SourceFileSyntax

init(file: SourceFileSyntax) {
self.file = file
}
}

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

Add Sendable conformance to match SwiftViewInterpreter.

SwiftViewInterpreter is Sendable and produces ParsedProgram via parse(_:). Downstream, CustomSidebarModel caches this value and may pass it across actor boundaries. Since SourceFileSyntax from swift-syntax 600 is already Sendable, the fix is trivial:

🛠️ Proposed fix
-public struct ParsedProgram {
+public struct ParsedProgram: Sendable {
🧰 Tools
🪛 SwiftLint (0.63.3)

[Warning] 21-21: This memberwise initializer would be synthesized automatically - you do not need to define it

(unneeded_synthesized_initializer)

🤖 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/CmuxSwiftRender/Sources/CmuxSwiftRender/ParsedProgram.swift` around
lines 17 - 24, ParsedProgram lacks Sendable conformance while
SwiftViewInterpreter (which produces ParsedProgram via parse(_:)) is Sendable
and ParsedProgram instances are cached and passed across actor boundaries (e.g.
CustomSidebarModel); make ParsedProgram conform to Sendable by declaring it
Sendable (add : Sendable to the ParsedProgram declaration) — since
SourceFileSyntax is already Sendable this is safe and requires no further
changes to the init or stored property `file`.

@azooz2003-bit
azooz2003-bit dismissed coderabbitai[bot]’s stale review June 3, 2026 16:02

Substantive findings (re-parse-every-tick perf, dropped JSON actions) resolved in c87fdf7. Remaining items: minor localization nit + low-value nit, tracked as fast-follow. User authorized dismissal.

@azooz2003-bit
azooz2003-bit merged commit 81e409c into main Jun 3, 2026
20 checks passed
azooz2003-bit added a commit that referenced this pull request Jun 3, 2026
#5254 merged the parse()->ParsedProgram / evaluate(program:) split into main.
Reconcile with this branch's 16MB-stack robustness fix: both parse() and
evaluate(program:) now run their recursive work on a shared onLargeStack
worker (LargeStackResultBox), and ParsedProgram is Sendable so it can cross
the worker boundary (also addresses CodeRabbit's Sendable suggestion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 3, 2026
…in-package

CmuxSwiftRenderUI gains its own Localizable.xcstrings (en + ja) for the
sidebar.custom.* strings, including the previously unlocalized decode-error
descriptions flagged on #5254;
String(localized:) call sites now read from .module. Also document the
SidebarTapTarget/SidebarTapTargetsKey public members and DSLDocument, and
annotate the render worker's -> Never backstop exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@austinywang austinywang mentioned this pull request Jun 4, 2026
azooz2003-bit added a commit that referenced this pull request Jun 4, 2026
The customSidebars.beta.enabled flag existed in the catalog and gated the
picker and rendering since #5254,
but the Beta Features section never grew a row for it, so the only way to
flip it was a raw defaults write. Add the toggle (with search entry and
en/ja strings) mirroring the Extensions row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 4, 2026
…ng (#5294)

* Out-of-process sidebar interpreter: crash-isolating worker + client

Untrusted vibe-coded sidebars run through a hand-written tree-walking
interpreter. No matter how well-guarded, an interpreter bug (an unguarded
path, a parser edge case, stack/memory exhaustion) running in-process can
crash the whole terminal app. This isolates that risk behind a process
boundary.

- Make the RenderNode IR Codable (RenderNode/Kind, RenderModifier, ModifierArg,
  ActionCommand, ButtonAction, ReorderSpec, SwiftValue) so it crosses the
  process boundary.
- New package CmuxSidebarInterpreterService:
  - cmux-sidebar-interpreter: a worker executable running the interpreter in a
    read-eval-write loop over a length-prefixed stdin/stdout channel.
  - InterpreterClient: a host-side actor that spawns/supervises the worker,
    correlates responses by id, and on a crash (closed pipe) or per-render
    timeout returns nil + relaunches the worker on the next render. The host
    never crashes regardless of what the source does.
  - LengthPrefixedMessageChannel: fd-backed (Sendable) framing shared by both.

Verified by swift test: valid render, host data-context binding, worker reuse,
and the two isolation guarantees — a crashing worker and a hanging worker both
return nil to the host and recover transparently (8 tests).

Not NSRemoteView: the goal is guarding against interpreter bugs, so the
interpreter (not the rendering) goes out-of-process; the validated RenderNode
IR renders in-process. This needs no private API and is verifiable headlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Render custom sidebars through an injected SidebarInterpreting seam

Adds the seam the app uses to run interpreted sidebars out-of-process, without
the UI layer depending on the worker/service:

- SidebarInterpreting protocol (CmuxSwiftRender) abstracts where interpretation
  happens; InProcessSidebarInterpreter is the in-process default, Interpreter
  Client (out-of-process, crash-isolating) conforms.
- CustomSidebarModel renders asynchronously through the injected interpreter
  (no longer interprets in-process in the view body); publishes the result to
  swiftRender, keeping the last view until the next render lands so live
  re-renders don't flicker.
- CustomSidebarView drives renderSwift via .task(id:) keyed on source revision
  + data context, and reads the published node.
- The worker's RenderInterpreterRunner caches the last parse so per-tick
  re-renders against changing data don't re-parse unchanged source.

UI package builds; the app injects the out-of-process client next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire the out-of-process interpreter into the app (re-exec-self worker)

- Link CmuxSidebarInterpreterClient into the cmux and cmux-cli targets
  (package ref + product dependency, mirroring CmuxSwiftRenderUI's wiring).
- Entry point: CmuxMain.main() checks for the worker-mode flag before any
  AppKit/SwiftUI setup; in worker mode it runs the shared interpreter loop and
  exits. The worker is this same binary re-executed with the flag, so no
  separate helper needs to be bundled or signed.
- InterpreterClient gains worker arguments + a reexecingCurrentBinary()
  factory; the shared worker loop moves into the library so the standalone
  executable and the re-exec path run identical code.
- VerticalTabsSidebar holds one InterpreterClient and injects it into
  CustomSidebarView, so custom sidebars now interpret out-of-process: an
  interpreter crash kills only the worker, which relaunches on the next render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix strict-concurrency error: no optional-chained actor calls in Tasks

CI's Swift compiler rejects `Task { await self?.method() }` (the closure
infers as non-Sendable `() async -> ()?`). Use `guard let self` inside the
task closures instead. Behavior unchanged; 8 package tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Extract value-driven CustomSidebarContentView and report tap targets

Split the pure presentation out of CustomSidebarView so the out-of-process
render worker can mount the exact same sidebar UI from value snapshots, and
publish CustomSidebarModel (+ State, DSLDocument) for reuse by the worker.
Buttons and tap-gesture nodes now report their global frames through a
SidebarTapTargets preference, letting a remote host hit-test forwarded
clicks geometrically (SwiftUI control gestures never fire in a
never-on-screen window).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add the remote render worker: protocol, supervised client, faceless renderer

Stage 2 of out-of-process custom sidebars: the worker now interprets AND
renders the untrusted sidebar file, sharing its layer tree with the host
via a remote CoreAnimation context (CAContext.remoteContextWithOptions: in
the worker, CALayerHost in the host; the contextId is a plain uint32 over
the existing framed channel, so no mach-port handoff is needed). The public
CARemoteLayerServer/Client API renders blank on current macOS and is a
verified dead end (docs/remote-sidebar-rendering/spike/ in the hq repo).

- RenderWorkerInbound/Outbound wire protocol: scenes (file + data context +
  insets, acked by seq for the hang watchdog), resizes, pointers in;
  context announcements and ButtonActions out.
- RenderWorkerClient actor: lazy spawn, crash/hang supervision, scene
  replay on respawn, retry-once sends, multicast event subscriptions.
- CmuxSidebarRemoteRender target: runSidebarRenderWorker() (faceless AppKit
  loop), RenderWorkerCoordinator (offscreen never-ordered window +
  NSHostingView pump; AppKit re-claims the backing layer on every window
  layout pass, so the pump steals it back before each commit), geometric
  tap-target activation, NSScrollView-direct scrolling, contentsScale walk;
  host-side RemoteCustomSidebarView (CALayerHost + input forwarding).
- cmux-sidebar-render-fixture + supervision tests (announce/ack/action/
  crash-respawn/hang-discard through a real process boundary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Render custom sidebars fully out-of-process in the app

CmuxMain grows a render-worker branch (re-exec-self with
--cmux-sidebar-render-worker, before any app startup), and the sidebar
mounts RemoteCustomSidebarView: interpretation and rendering both happen in
the supervised worker; the host only displays the worker's remote layer,
forwards input, and dispatches returned ButtonActions. The stage-1
in-process interpreter path stays behind the SidebarInterpreting seam as
the fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review: DocC on new public surface, localize sidebar strings in-package

CmuxSwiftRenderUI gains its own Localizable.xcstrings (en + ja) for the
sidebar.custom.* strings, including the previously unlocalized decode-error
descriptions flagged on #5254;
String(localized:) call sites now read from .module. Also document the
SidebarTapTarget/SidebarTapTargetsKey public members and DSLDocument, and
annotate the render worker's -> Never backstop exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix remote sidebar scroll pinning under the titlebar inset

The worker's scroll path clamped the clip origin to y >= 0, but with the
host's safe-area top inset the resting origin is -topInset — the first
scroll snapped content up under the traffic lights and the clamp never let
it return. Clamp through NSClipView.constrainBoundsRect instead, which
honors content insets and document bounds exactly like a real wheel event.

Harness-verified: scroll down reveals later rows, over-scrolling back up
settles at the inset rest position instead of pinning to the window top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Keep the remote sidebar surface mounted across picker switches

Dropping the .id(fileURL) remount: the worker swaps files in place on the
next scene message, so remounting the surface only flashed the previous
sidebar's pixels (the fresh CALayerHost adopts the live context, which
still shows the old file for the IPC beat, and SwiftUI can overlap the
outgoing/incoming views). One surface, one hosted layer, content switches
in the worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Adopt the live remote layer synchronously on sidebar surface mount

Dogfood (Codex-driven) flagged transient blank sidebars when cycling
providers: a remounting surface waited on the async subscribe round-trip
before adopting the worker's layer. RenderWorkerClient now mirrors the live
context id into a main-actor RemoteContextCache the surface reads in init,
so provider switches and sidebar toggles show the worker's last rendered
frame in the same frame the surface appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make hot reload last-good sticky: broken saves never replace a working sidebar

A save now only replaces what's on screen when it actually interprets to a
view (or decodes, for JSON). Broken intermediate saves — editors writing
mid-edit states, atomic saves' transient missing file — keep the previous
working render, with the last GOOD source re-interpreted against fresh data
each tick so live values keep flowing while the file is broken. Error
states still show when a file is broken from the start (nothing good to
fall back to), and switching files resets the fallback. Same stickiness in
the in-process fallback path.

Harness-verified: valid -> garbage keeps rendering the valid version with
data ticks continuing; fixing the file swaps in the new content; a file
broken from first selection still shows the error state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Show the Custom Sidebars beta toggle in Settings

The customSidebars.beta.enabled flag existed in the catalog and gated the
picker and rendering since #5254,
but the Beta Features section never grew a row for it, so the only way to
flip it was a raw defaults write. Add the toggle (with search entry and
en/ja strings) mirroring the Extensions row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix CI Swift 6.1 sendability in the reload-notification forwarder

CI's Xcode 16.4 rejects awaiting NotificationCenter's async stream from a
MainActor task (non-Sendable Notification crossing isolation in next()).
Consume the stream in a nonisolated task; only the Sendable names array
leaves it through the outbox. Also add the parameter docs Codex review
flagged on the public wire-type initializers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Use a token observer for reload forwarding (CI Swift 6.1)

Task {} in a MainActor init inherits MainActor isolation, so the
nonisolated rewrite still tripped 6.1's non-Sendable Notification check on
the async stream's next(). Use the closure observer API instead — the same
pattern CustomSidebarModel ships, already proven on CI's toolchain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address CodeRabbit re-review: frame guards, watcher re-arm, worker reaping

- LengthPrefixedMessageChannel caps frames at 64 MB in both directions:
  an inbound peer-controlled length beyond the cap reads as EOF instead of
  an allocation, and an oversized outbound payload throws (regression
  tests for both).
- The hot-reload watcher follows .swift/.json extension flips (reload
  re-resolves the file by name; the kqueue watcher previously stayed on
  the original path and went silently stale).
- SidebarTapTargetsKey moves to its own file (public major type).
- The render worker is terminated when its window closes (surface
  observes NSWindow.willCloseNotification); provider switches and sidebar
  toggles still keep it warm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 5, 2026
…5275)

* Interpreter primitives B1a: List/Section/LazyVStack/LazyHStack/Group/EmptyView + horizontal ScrollView

Leaf-tier container coverage from docs/swiftui-interpreter-surface.md:
- New RenderNode kinds: lazyVStack, lazyHStack, group, list, section, hscroll.
- Interpreter constructors for LazyVStack/LazyHStack, Group, EmptyView (-> empty
  group), List, Section("Header") { } (leading string literal becomes header),
  and ScrollView(.horizontal) -> hscroll (vertical stays passthrough so it does
  not double-scroll inside the already-scrolling sidebar).
- RenderNodeView renders each: LazyVStack/LazyHStack, transparent Group, plain
  chrome-light List (.plain + hidden scroll background), Section with a styled
  caption header above its content, and a horizontal ScrollView+HStack.
- Harness allow-list + 3 behavior tests (37 interpreter tests pass; UI package
  builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B1b: ForEach enumerated/indices/2-arg + array slicing + conversions

- ForEach over `Array(xs.enumerated())` with a 2-arg closure `{ index, item in }`
  now destructures the pair (offset/element). `.enumerated()` yields pairs
  addressable as `.offset`/`.element` or `$0`/`$1`.
- `.indices` resolves as a property (bare, no parens) on arrays, so
  `ForEach(xs.indices) { i in ... }` works; also `.first`/`.last` properties.
- Array `.dropFirst(_:)`, `.dropLast(_:)`, `.suffix(_:)` methods.
- `Array(seq)` identity passthrough and `Int()/Double()/String()` conversions in
  value position.
- Allow-list + 3 behavior tests (40 interpreter tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B2: text/typography modifiers + Label

- New view-level text modifiers in the bridge: .italic, .underline, .monospaced,
  .monospacedDigit, .fontDesign, .multilineTextAlignment, .textCase,
  .truncationMode (resolvers in RenderStyle). All apply to the erased view, so
  they affect contained text.
- Label(title, systemImage:) -> new .label kind rendered as a native Label.
- Allow-list (Label/Array/Int/Double/String + the new members) + 2 tests
  (42 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B3: shapes + semantic colors + cosmetic/transform modifiers

- New shape kinds: Ellipse, UnevenRoundedRectangle (uniform-radius approx).
- Widened dslColor palette: tertiary/quaternary + mint/teal/cyan/indigo/brown.
- New bridge modifiers: shadow(color:radius:x:y:), border(_,width:), blur(radius:),
  offset(x:y:), scaleEffect, rotationEffect(.degrees/.radians), zIndex,
  brightness/contrast/saturation/grayscale, clipShape(<shape>), clipped,
  fixedSize, layoutPriority. Angle token parser for rotationEffect.
- Allow-list + 2 tests (44 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B5: arbitrary-child modifiers (.overlay/.background/.mask/.safeAreaInset)

The composability keystone: RenderModifier now carries an optional child
RenderNode subtree captured from a modifier's trailing closure. The interpreter
lowers `.overlay { ... }`, `.background { ... }`, `.mask { ... }`, and
`.safeAreaInset(edge:) { ... }` content through evalItems, and the bridge renders
it (alignment-aware for overlay/background; ZStack for multiple children). This
lets interpreted views nest arbitrary content inside modifiers, not just colors.
Background/overlay still accept a plain color token when there is no closure.

Allow-list + a test (45 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B6: SF Symbol modifiers + min/max/abs builtins

- Bridge modifiers: .imageScale, .symbolRenderingMode (hierarchical/multicolor/
  palette/monochrome), .symbolVariant (fill/circle/square/slash) — the symbol
  controls SF-symbol sidebars actually use.
- Numeric builtins min(_,_)/max(_,_)/abs(_) in value position (int-preserving).
- Allow-list + 2 tests (47 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B7: Grid/GridRow, LazyVGrid/LazyHGrid, ViewThatFits

- New kinds grid/gridRow/lazyVGrid/lazyHGrid/viewThatFits with interpreter
  constructors and native bridge rendering. Grid uses leading alignment;
  LazyV/HGrid use adaptive columns/rows (sidebar-appropriate approximation of
  the GridItem spec); ViewThatFits lets SwiftUI pick the first fitting child.
- Allow-list + a test (48 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B8: ProgressView/Gauge/Menu + contextMenu/help/disabled

- New kinds progressView (determinate via normalized value:/total: or
  indeterminate), gauge, menu (Menu(title) { items }). RenderNode gains a `value`
  field for determinate controls.
- contextMenu joins the child-bearing modifiers (its closure items render as the
  menu); .help (tooltip) and .disabled bridge modifiers.
- Allow-list + 2 tests (49 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B9: redaction, accessibility, scroll/list tokens, aspectRatio

Generic view-level bridge modifiers (no IR change; the interpreter already
captures them): .redacted(reason:)/.unredacted, .accessibilityLabel/Hint/Value/
Hidden, .scrollIndicators, .scrollContentBackground, .aspectRatio(contentMode:),
.scaledToFit/.scaledToFill. Allow-list updated; UI builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter robustness: large-stack worker + recursion budget; stress corpus

Deeply-composed sidebars (and pathological nesting) overflowed the caller stack
during parse/interpret and crashed the process (SIGBUS). Fixes:
- evaluate() now runs parse + interpret on a dedicated 16MB-stack Thread, so
  realistic deep nesting renders instead of overflowing the small caller stack.
- A per-Environment RecursionBudget (shared down the scope chain) backstops
  genuinely unbounded interpreter recursion (mutually-recursive view helpers),
  guarding evalView/eval/evalItems/callValueFunction.
- Add 8 multi-agent-authored stress sidebars to the corpus (deeply nested
  overlays/backgrounds, grids, menus, progress, data-driven). All 22 corpus
  sidebars now render; 50 interpreter tests pass, incl a 600-deep nesting test.

Surfaced next gap: shape `.stroke` (13 uses) / `.trim` / `.strokeBorder`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives B-stroke: shape .stroke/.strokeBorder/.trim

The stress corpus surfaced .stroke as the top gap (13 uses). Apply stroke/trim
at the concrete-shape level (before AnyView erasure) via AnyShape: shapes
(Circle/Rectangle/RoundedRectangle/Capsule/Ellipse) now honor
`.trim(from:to:)` then `.stroke(color, lineWidth:)` / `.strokeBorder`, rendering
real outlines; with no stroke the plain shape renders so .fill/.foregroundColor
still fill it. Allow-list + a test (51 interpreter tests pass; UI builds clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: AnyView(_:) passthrough renders the wrapped view

Common in author/agent helpers that erase to AnyView; render the wrapped
expression instead of dropping it. 52 interpreter tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: expand custom-sidebars authoring guide to the full interpreter surface

Document the ~80 added views/modifiers/language features (lists/grids/lazy
stacks, full text/typography, shapes + stroke/trim, arbitrary-child overlay/
background/mask/contextMenu, ProgressView/Gauge/Menu, symbol modifiers, value
methods) and narrow the 'not yet supported' list to the real remaining gaps
(@State + input controls, gradients, navigation, custom structs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter primitives: Linear/Radial/Angular gradients

New gradient view kinds with interpreter constructors (parse `colors: [...]` or
`gradient: Gradient(colors:)` stops + start/end/center UnitPoint tokens) and
native rendering via dslUnitPoint. Usable standalone in ZStack and inside the
arbitrary-child `.background { }` / `.overlay { }` modifiers. RenderNode gains
`colors`/`points`. Allow-list + a test (53 interpreter tests pass; UI builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: @State engine design note (the interactivity tier)

Concrete implementation plan for the remaining big capability (@State / $bindings /
input controls): mutable host-owned state bag keyed by declaration site, binding
values, action executor for assignments, re-walk on change, staged S1-S3. Left as
a design (not half-built) because interactive behavior needs real dogfooding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: .keyboardShortcut(key, modifiers:)

Bridge .keyboardShortcut with a KeyEquivalent resolver (.return/.escape/arrows/
single char) and EventModifiers parser (command/shift/option/control). Closes the
last non-niche modifier gap from the stress corpus. UI builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter language tier: if let / guard let binding + view-position switch

- evalIf now evaluates condition lists via conditionsPass, binding
  `if let name = expr` (and shorthand `if let name`) optionals into the
  then-branch scope (previously only boolean conditions were honored, so
  `if let` never rendered).
- View-position `switch subject { case "x": ... default: ... }` selects the
  first matching case; matches string/int literal patterns and bare `.member`
  enum-style patterns against the subject. The type-system tier begins.
- 2 tests (55 interpreter tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter language tier: value-position switch + if let in user value funcs

evalBlockValue/evalIfValue now mirror the view-side language support: a value
helper can use `switch x { case ...: return ... }` and `if let y = opt { return y }`
(conditionsPass binds optionals into the branch scope). Lets authors write
natural value helpers (e.g. a status->color switch). 56 interpreter tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Corpus: add 6 stress2 sidebars exercising switch/if-let/gradients/trim

Second multi-agent stress round, now leaning on the language tier (value-func
`switch` mapping status->color/symbol, `if let pr = w.pr`), gradients in
`.background { }`, `.trim` arcs, AngularGradient. All 28 corpus sidebars render
(0 NIL); permanent regression fixtures for the language + gradient tiers. Only
remaining unsupported symbol is `.resizable` (+ undefined persona helper funcs,
which are authoring gaps, not interpreter gaps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: Image .resizable() (applied before erasure)

styledImage applies .resizable() on the concrete Image (an Image method, not
available post-AnyView-erasure); .scaledToFit/.aspectRatio from the generic pass
apply on top. Closes the last unsupported modifier from the stress corpus — the
only remaining gaps are AsyncImage/URL (async/file) and the @State tier. UI builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: fix bugs from adversarial correctness review

Confirmed findings from a multi-agent review of tonight's changes:
- CRITICAL: evalClosure/evalClosure2 returned the first expression, skipping
  local `let` bindings — `xs.map { x in let d = x*2; d+1 }` (and reduce/sorted
  with locals) silently broke. Both now route the body through evalBlockValue,
  honoring let/if/switch + trailing expression.
- HIGH: Color(red:green:blue:) channel did `Int(d*255)` which TRAPS on
  Double.infinity/NaN — guard `isFinite` + clamp to [0,1] before converting.
- range iterationValues: overflow-safe end (addingReportingOverflow) + a 100k
  materialization cap so `0...Int.max` can't overflow or exhaust memory.
- .disabled() now disables only when the arg is explicitly true (unresolved
  expr -> enabled, not disabled).
- .aspectRatio applies an explicit ratio only when positive (zero/negative is
  invalid in SwiftUI) -> falls back to mode-only.

Regression tests for the closure-let and non-finite-color cases (58 tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: array .joined, string .capitalized / .replacingOccurrences

Common value-method long-tail: array `.joined(separator:)`, string
`.replacingOccurrences(of:with:)` (methods, via evalMethod), and `.capitalized`
(a property — resolved in SwiftValue.member like .indices, alongside
.uppercased/.lowercased property forms). 59 interpreter tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Interpreter: string .trimmingCharacters(in:)

Common everyday helper (trim titles/branches): .trimmingCharacters(in:
.whitespaces / .whitespacesAndNewlines / .newlines). 60 interpreter tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Rebase onto main: thread parse/evaluate split through large-stack worker

#5254 merged the parse()->ParsedProgram / evaluate(program:) split into main.
Reconcile with this branch's 16MB-stack robustness fix: both parse() and
evaluate(program:) now run their recursive work on a shared onLargeStack
worker (LargeStackResultBox), and ParsedProgram is Sendable so it can cross
the worker boundary (also addresses CodeRabbit's Sendable suggestion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add failing tests: Int() of non-finite double traps; Gauge ignores total:

Int(1.0 / 0.0) in authored sidebar source crashes the interpreting process
(Swift's Int(_: Double) traps on non-finite/overflow), and
Gauge(value:total:) renders full because only the raw value: is captured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix Int() trap on non-finite doubles and Gauge total: normalization

Int(exactly:) on the truncated value returns nil for NaN/infinity/overflow
instead of trapping, so authored source like Int(1.0 / 0.0) renders
best-effort rather than killing the interpreting process. Gauge(value:total:)
now normalizes through the same progressValue logic as ProgressView, so
total-relative gauges render proportionally instead of pinned full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ShubhamPatilsd pushed a commit to emergent-inc/mosaic that referenced this pull request Jul 9, 2026
…ng (#5294)

* Out-of-process sidebar interpreter: crash-isolating worker + client

Untrusted vibe-coded sidebars run through a hand-written tree-walking
interpreter. No matter how well-guarded, an interpreter bug (an unguarded
path, a parser edge case, stack/memory exhaustion) running in-process can
crash the whole terminal app. This isolates that risk behind a process
boundary.

- Make the RenderNode IR Codable (RenderNode/Kind, RenderModifier, ModifierArg,
  ActionCommand, ButtonAction, ReorderSpec, SwiftValue) so it crosses the
  process boundary.
- New package CmuxSidebarInterpreterService:
  - cmux-sidebar-interpreter: a worker executable running the interpreter in a
    read-eval-write loop over a length-prefixed stdin/stdout channel.
  - InterpreterClient: a host-side actor that spawns/supervises the worker,
    correlates responses by id, and on a crash (closed pipe) or per-render
    timeout returns nil + relaunches the worker on the next render. The host
    never crashes regardless of what the source does.
  - LengthPrefixedMessageChannel: fd-backed (Sendable) framing shared by both.

Verified by swift test: valid render, host data-context binding, worker reuse,
and the two isolation guarantees — a crashing worker and a hanging worker both
return nil to the host and recover transparently (8 tests).

Not NSRemoteView: the goal is guarding against interpreter bugs, so the
interpreter (not the rendering) goes out-of-process; the validated RenderNode
IR renders in-process. This needs no private API and is verifiable headlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Render custom sidebars through an injected SidebarInterpreting seam

Adds the seam the app uses to run interpreted sidebars out-of-process, without
the UI layer depending on the worker/service:

- SidebarInterpreting protocol (CmuxSwiftRender) abstracts where interpretation
  happens; InProcessSidebarInterpreter is the in-process default, Interpreter
  Client (out-of-process, crash-isolating) conforms.
- CustomSidebarModel renders asynchronously through the injected interpreter
  (no longer interprets in-process in the view body); publishes the result to
  swiftRender, keeping the last view until the next render lands so live
  re-renders don't flicker.
- CustomSidebarView drives renderSwift via .task(id:) keyed on source revision
  + data context, and reads the published node.
- The worker's RenderInterpreterRunner caches the last parse so per-tick
  re-renders against changing data don't re-parse unchanged source.

UI package builds; the app injects the out-of-process client next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire the out-of-process interpreter into the app (re-exec-self worker)

- Link CmuxSidebarInterpreterClient into the cmux and cmux-cli targets
  (package ref + product dependency, mirroring CmuxSwiftRenderUI's wiring).
- Entry point: CmuxMain.main() checks for the worker-mode flag before any
  AppKit/SwiftUI setup; in worker mode it runs the shared interpreter loop and
  exits. The worker is this same binary re-executed with the flag, so no
  separate helper needs to be bundled or signed.
- InterpreterClient gains worker arguments + a reexecingCurrentBinary()
  factory; the shared worker loop moves into the library so the standalone
  executable and the re-exec path run identical code.
- VerticalTabsSidebar holds one InterpreterClient and injects it into
  CustomSidebarView, so custom sidebars now interpret out-of-process: an
  interpreter crash kills only the worker, which relaunches on the next render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix strict-concurrency error: no optional-chained actor calls in Tasks

CI's Swift compiler rejects `Task { await self?.method() }` (the closure
infers as non-Sendable `() async -> ()?`). Use `guard let self` inside the
task closures instead. Behavior unchanged; 8 package tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Extract value-driven CustomSidebarContentView and report tap targets

Split the pure presentation out of CustomSidebarView so the out-of-process
render worker can mount the exact same sidebar UI from value snapshots, and
publish CustomSidebarModel (+ State, DSLDocument) for reuse by the worker.
Buttons and tap-gesture nodes now report their global frames through a
SidebarTapTargets preference, letting a remote host hit-test forwarded
clicks geometrically (SwiftUI control gestures never fire in a
never-on-screen window).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add the remote render worker: protocol, supervised client, faceless renderer

Stage 2 of out-of-process custom sidebars: the worker now interprets AND
renders the untrusted sidebar file, sharing its layer tree with the host
via a remote CoreAnimation context (CAContext.remoteContextWithOptions: in
the worker, CALayerHost in the host; the contextId is a plain uint32 over
the existing framed channel, so no mach-port handoff is needed). The public
CARemoteLayerServer/Client API renders blank on current macOS and is a
verified dead end (docs/remote-sidebar-rendering/spike/ in the hq repo).

- RenderWorkerInbound/Outbound wire protocol: scenes (file + data context +
  insets, acked by seq for the hang watchdog), resizes, pointers in;
  context announcements and ButtonActions out.
- RenderWorkerClient actor: lazy spawn, crash/hang supervision, scene
  replay on respawn, retry-once sends, multicast event subscriptions.
- CmuxSidebarRemoteRender target: runSidebarRenderWorker() (faceless AppKit
  loop), RenderWorkerCoordinator (offscreen never-ordered window +
  NSHostingView pump; AppKit re-claims the backing layer on every window
  layout pass, so the pump steals it back before each commit), geometric
  tap-target activation, NSScrollView-direct scrolling, contentsScale walk;
  host-side RemoteCustomSidebarView (CALayerHost + input forwarding).
- cmux-sidebar-render-fixture + supervision tests (announce/ack/action/
  crash-respawn/hang-discard through a real process boundary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Render custom sidebars fully out-of-process in the app

CmuxMain grows a render-worker branch (re-exec-self with
--cmux-sidebar-render-worker, before any app startup), and the sidebar
mounts RemoteCustomSidebarView: interpretation and rendering both happen in
the supervised worker; the host only displays the worker's remote layer,
forwards input, and dispatches returned ButtonActions. The stage-1
in-process interpreter path stays behind the SidebarInterpreting seam as
the fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review: DocC on new public surface, localize sidebar strings in-package

CmuxSwiftRenderUI gains its own Localizable.xcstrings (en + ja) for the
sidebar.custom.* strings, including the previously unlocalized decode-error
descriptions flagged on manaflow-ai/cmux#5254;
String(localized:) call sites now read from .module. Also document the
SidebarTapTarget/SidebarTapTargetsKey public members and DSLDocument, and
annotate the render worker's -> Never backstop exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix remote sidebar scroll pinning under the titlebar inset

The worker's scroll path clamped the clip origin to y >= 0, but with the
host's safe-area top inset the resting origin is -topInset — the first
scroll snapped content up under the traffic lights and the clamp never let
it return. Clamp through NSClipView.constrainBoundsRect instead, which
honors content insets and document bounds exactly like a real wheel event.

Harness-verified: scroll down reveals later rows, over-scrolling back up
settles at the inset rest position instead of pinning to the window top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Keep the remote sidebar surface mounted across picker switches

Dropping the .id(fileURL) remount: the worker swaps files in place on the
next scene message, so remounting the surface only flashed the previous
sidebar's pixels (the fresh CALayerHost adopts the live context, which
still shows the old file for the IPC beat, and SwiftUI can overlap the
outgoing/incoming views). One surface, one hosted layer, content switches
in the worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Adopt the live remote layer synchronously on sidebar surface mount

Dogfood (Codex-driven) flagged transient blank sidebars when cycling
providers: a remounting surface waited on the async subscribe round-trip
before adopting the worker's layer. RenderWorkerClient now mirrors the live
context id into a main-actor RemoteContextCache the surface reads in init,
so provider switches and sidebar toggles show the worker's last rendered
frame in the same frame the surface appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make hot reload last-good sticky: broken saves never replace a working sidebar

A save now only replaces what's on screen when it actually interprets to a
view (or decodes, for JSON). Broken intermediate saves — editors writing
mid-edit states, atomic saves' transient missing file — keep the previous
working render, with the last GOOD source re-interpreted against fresh data
each tick so live values keep flowing while the file is broken. Error
states still show when a file is broken from the start (nothing good to
fall back to), and switching files resets the fallback. Same stickiness in
the in-process fallback path.

Harness-verified: valid -> garbage keeps rendering the valid version with
data ticks continuing; fixing the file swaps in the new content; a file
broken from first selection still shows the error state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Show the Custom Sidebars beta toggle in Settings

The customSidebars.beta.enabled flag existed in the catalog and gated the
picker and rendering since manaflow-ai/cmux#5254,
but the Beta Features section never grew a row for it, so the only way to
flip it was a raw defaults write. Add the toggle (with search entry and
en/ja strings) mirroring the Extensions row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix CI Swift 6.1 sendability in the reload-notification forwarder

CI's Xcode 16.4 rejects awaiting NotificationCenter's async stream from a
MainActor task (non-Sendable Notification crossing isolation in next()).
Consume the stream in a nonisolated task; only the Sendable names array
leaves it through the outbox. Also add the parameter docs Codex review
flagged on the public wire-type initializers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Use a token observer for reload forwarding (CI Swift 6.1)

Task {} in a MainActor init inherits MainActor isolation, so the
nonisolated rewrite still tripped 6.1's non-Sendable Notification check on
the async stream's next(). Use the closure observer API instead — the same
pattern CustomSidebarModel ships, already proven on CI's toolchain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address CodeRabbit re-review: frame guards, watcher re-arm, worker reaping

- LengthPrefixedMessageChannel caps frames at 64 MB in both directions:
  an inbound peer-controlled length beyond the cap reads as EOF instead of
  an allocation, and an oversized outbound payload throws (regression
  tests for both).
- The hot-reload watcher follows .swift/.json extension flips (reload
  re-resolves the file by name; the kqueue watcher previously stayed on
  the original path and went silently stale).
- SidebarTapTargetsKey moves to its own file (public major type).
- The render worker is terminated when its window closes (surface
  observes NSWindow.willCloseNotification); provider switches and sidebar
  toggles still keep it warm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — c87fdf7e Deployed Jun 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant