Skip to content

Descriptive agent notifications: workspace title + specific bodies - #7336

Closed
lawrencecchen wants to merge 22 commits into
mainfrom
feat-descriptive-agent-notifs
Closed

lawrencecchen wants to merge 22 commits into
mainfrom
feat-descriptive-agent-notifs

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Agent notification banners were generic: title was the agent name ("Claude Code", "Codex"), the workspace never appeared, and bodies frequently fell back to boilerplate ("Claude is waiting for your input", " session completed", "Agent is asking a question").

Now every agent banner is composed at delivery time as: title = workspace name, subtitle = " · ", body = the most specific message available.

Mechanism:

  • The notify wire meta segment gains agent identity: c=<cat>;p=<0|1>;a=<agent> plus an agent-only a=<agent> form for ungated notifications. Parsing stays strict; legacy 3-field and c=..;p=.. payloads parse and gate byte-identically (regression-tested at both the meta parser and a live-socket process test).
  • New pure composeNotificationBannerContent helper applied at every banner materialization point (UN content, local/suppressed feedback, custom notification command). Stored notification fields are unchanged, so the in-app Notifications page keeps its rendering.
  • Claude idle nag ("Claude is waiting for your input") is replaced by the saved last assistant message from Stop, else the transcript's last assistant message. permission_prompt messages are untouched, and category/pending gating logic is unchanged.
  • Generic agents persist the last user prompt at prompt-submit; bare stops emit localized "Finished: " instead of " session completed", and structured-output JSON blobs are skipped as bodies (fixes raw {"findings":[]...} banners from Codex).
  • Feed attention banners (PermissionRequest / AskUserQuestion / ExitPlanMode from opencode, pi, claude, codex feed sources) resolve the workspace title and build specific bodies from tool input: command for Bash-like tools, file basename, or the first question text.
  • opencode gains a turn-complete banner: the plugin's session.idle event now carries the last assistant/user message and the app posts a Path-A notification gated by the same turn-complete setting, restricted to source == "opencode" so hook-based agents cannot double-notify.
  • Codex monitor notifications (publishCodexMonitorUserInput/Failure) are tagged a=codex so they get workspace composition too.

All new user-facing strings are localized (en + ja). New test files are pbxproj-wired: AgentNotificationGateTests extensions, NotificationBannerCompositionTests, FeedNotificationContentTests, CLI process tests for idle-body replacement, prompt fallback, JSON-blob guard, and legacy byte-identity.

🤖 Generated with Claude Code


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


Note

Medium Risk
Touches notification delivery, wire meta parsing, and multiple agent hook paths; legacy payload compatibility is explicitly preserved but the surface area for missed banners or double-notify is non-trivial.

Overview
Agent and terminal notifications are reworked so OS banners emphasize the workspace title, an agent · status subtitle, and a specific body instead of generic agent names and boilerplate.

Notify payloads gain optional a=<agent-id> on the existing c=…;p=… meta (including c=other;p=0;a=… for ungated alerts). Parsing stays strict so legacy 3-field payloads behave unchanged. Hook paths for Claude, Codex, and generic agents attach agent identity; completion bodies can use "Finished: <prompt>" from per-turn lastPrompt stored on prompt-submit.

NotificationBannerComposer centralizes banner text at delivery (native notifications, suppressed feedback, phone push) with secret/path scrubbing before truncation; stored notification fields are unchanged for the in-app list. TerminalNotification carries optional agentId and workspaceTitle.

Claude idle nags replace stock "waiting for your input" copy with the last assistant message from the session store or transcript, skipping JSON-shaped assistant text. OpenCode plugin sends surface_id, clears stale assistant text per user message, includes assistant text on session.idle, disables CLI stop notifications, and the app posts gated turn-complete banners with per-surface dedupe reset on each prompt. Feed permission/question/plan notifications use the shared composer with workspace titles and tool-input-derived bodies.

Reviewed by Cursor Bugbot for commit 7eadab5. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Make agent notifications descriptive and workspace-aware. OS banners now show the workspace title, an " · " subtitle, and specific bodies; all fields are scrubbed before truncation, and phone push uses the same content.

  • New Features

    • Notify meta adds agent identity as c=<category>;p=<0|1>;a=<agent>; the parser only accepts this c=-anchored form. Uncategorized alerts use c=other;p=0;a=<agent>.
    • Added NotificationBannerComposer.composeNotificationBannerContent and use it for agent, feed, and custom banners; TerminalNotification stores optional agentId and workspaceTitle for composition.
    • Claude idle reminders reuse the last assistant message from Stop or the transcript; generic agent stops show "Finished: " and skip JSON-shaped bodies.
    • Feed attention banners (permission, question, exit plan) use the workspace title and concrete bodies from tool input; opencode posts a gated turn-complete banner via session.idle with per-surface dedupe re-armed on each user prompt. All user strings localized (en, ja).
  • Bug Fixes

    • Centralized redaction: titles, subtitles, and bodies are scrubbed before truncation at a single composition point for every producer; phone push builds from the same scrubbed content.
    • Hardened JSON-blob guards so raw JSON never appears in stop/idle bodies or opencode snippets; permission banners fall back to " needs approval" when no detail exists; notification-hook rewrites are preserved verbatim.
    • Tightened meta parsing: only byte-exact canonical c=...;p=<0|1>[;a=<id>] tails are stripped; whitespace-prefixed or malformed tails remain in the body to preserve legacy behavior.
    • Deduped opencode turn-complete banners per surface and disabled the legacy CLI stop notification path (publishesStopNotification: false); reset the OpenCode assistant preamble per turn; cleared lastPrompt on every prompt-submit; legacy 3-field and c=..;p=.. payloads parse and gate exactly as before.

Written for commit 7eadab5. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Improved, localized notification banner composition for completion, permissions, questions, and exit-plan modes, with smarter snippet formatting and truncation.
    • Notifications can now include optional agent identity metadata end-to-end, enhancing OpenCode stop notifications.
  • Bug Fixes
    • Reduced noise from JSON-like assistant output by avoiding raw JSON in banner content.
    • Tightened notification metadata parsing and payload handling while preserving legacy behavior where applicable.
  • Tests
    • Added/expanded unit and CLI/integration tests covering banner content, metadata parsing, and updated notification/payload behaviors.

Banner title becomes the workspace name for agent notifications, with
"<Agent> · <status>" as the subtitle, via a new agent-identity meta field
(a=<agent>) on the notify wire format and a pure banner-composition helper.
Claude idle nags reuse the saved/transcript last assistant message instead
of "Claude is waiting for your input"; generic agent stops fall back to
"Finished: <prompt>" instead of "session completed" (and skip JSON-blob
assistant messages); feed permission/question/plan banners show the
workspace plus the concrete command, file, or question; opencode gains a
gated turn-complete banner fed by the plugin's session.idle event.

Legacy 3-field and c=..;p=.. payloads parse and gate byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 4, 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 Jul 7, 2026 12:02am
cmux-staging Building Building Preview, Comment Jul 7, 2026 12:02am

@coderabbitai

coderabbitai Bot commented Jul 4, 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 threads agent identity and prompt context through notification parsing, delivery, storage, and banner rendering. It adds a new opencode stop-notification path, updates localized strings, and expands tests and project wiring for the new notification flows.

Changes

Agent-tagged notification pipeline

Layer / File(s) Summary
CLI notification helpers
CLI/CMUXCLI+NotificationSummaryHelpers.swift
Adds assistant-message extraction, JSON-blob detection, generic Claude body detection, cue scanning, payload formatting, and agent-id validation helpers.
CLI hook prompt and notification wiring
CLI/cmux.swift
Tracks lastPrompt, updates Claude and Codex notification bodies and meta, derives prompt fallback text, and updates notification parsing and agent metadata propagation.
AgentNotificationMeta parsing
Sources/AgentNotificationGate.swift, cmuxTests/AgentNotificationGateTests.swift
Extends meta parsing to accept canonical a=<agent-id> forms and adds coverage for valid and invalid agent-id cases.
Banner composition and feed notification wiring
Sources/NotificationBannerComposition.swift, Sources/Feed/FeedCoordinator.swift, cmuxTests/NotificationBannerCompositionTests.swift, cmuxTests/FeedNotificationContentTests.swift
Introduces centralized banner composition, feed notification body selection, workspace title resolution, subtitle propagation, and tests for composed outputs.
Notification delivery and storage metadata
Sources/TerminalNotification.swift, Sources/TerminalNotificationQueue.swift, Sources/TerminalNotificationStore.swift, Sources/TerminalNotificationStore+BannerContent.swift, cmuxTests/ClaudeBackgroundWorkNotifyTests.swift, cmuxTests/DescriptiveAgentNotificationCLITests.swift
Adds notification metadata fields, threads agentId through queue and store delivery, preserves metadata during reconstruction, switches banner rendering to composed content, and adds coverage for idle and legacy notification flows.
OpenCode stop notifications and plugin telemetry
Sources/TerminalController+AgentNotifications.swift, Resources/opencode-plugin.js, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj, cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift, cmuxTests/WorkspaceRemoteConnectionTests.swift
Adds the opencode stop-notification helper, propagates surface and assistant context into plugin events, updates localized strings, wires the new source and tests into the Xcode project, and updates expected notification command strings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#3218: Also routes queued notify_target_async notifications through the terminal notification ingress.
  • manaflow-ai/cmux#4409: Touches the same CLI notification-generation paths and agent-specific notification handling.
  • manaflow-ai/cmux#7129: Uses the same notification meta grammar expanded here with optional a=<agent-id> support.

Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error Pure, unit-testable notification composition/gating was added in app-target Sources/ (NotificationBannerComposition.swift, AgentNotificationGate.swift) despite a reusable package boundary bei... Move NotificationBannerComposer/AgentNotificationMeta and related helpers into a small SwiftPM target (e.g. CmuxNotifications or a new core package) and keep app files as thin adapters.
Cmux Full Internationalization ❌ Error New xcstrings keys are only localized for en/ja, but the catalog already supports 20 locales; the added agent/feed strings are missing 18 locales. Add translated entries for every supported locale in Resources/Localizable.xcstrings (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant).
Cmux No Ambient Global State ❌ Error FAIL: NotificationBannerComposer is a new caseless enum namespace with only static funcs, which the rule forbids as ambient global state. Replace the namespace enum with file-private helpers or an instance type owned by the delivery path; avoid new static-only API surface.
Description check ⚠️ Warning The description is missing the required template sections for Testing, Demo Video, Review Trigger, and Checklist. Reformat the PR description to match the template and add Testing, Demo Video, Review Trigger, and Checklist sections with the requested details.
✅ Passed checks (21 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation regression: pure helpers are nonisolated, TerminalNotificationStore stays @MainActor, and background delivery hops to MainActor where needed.
Cmux Swift Blocking Runtime ✅ Passed Added production code is pure parsing/composition; diff scans found no new waits, semaphores, sleeps, main-queue syncs, or locks. Blocking appears only in deterministic tests.
Cmux Browser Automation Off-Main ✅ Passed PR only changes notification/meta code; the diff shows no browser.* routing or policy changes, so the browser off-main rule is not implicated.
Cmux Expensive Synchronous Load ✅ Passed No added main-actor/UI path does broad agent-history loading; new transcript reads are bounded fallbacks in CLI hook code, not UI render paths.
Cmux Cache Substitution Correctness ✅ Passed PASS: Cached stop/body fields have event-driven refresh plus transcript fallbacks and documented freshness rationale; banner-only workspace titles are transient UI hints.
Cmux No Hacky Sleeps ✅ Passed The only non-Swift runtime change is opencode-plugin.js; its diff adds surface_id/last_assistant_message plumbing and leaves the existing setTimeout timeout unchanged, so no new hacky sleep.
Cmux Algorithmic Complexity ✅ Passed Added logic is fixed-size or single-pass; no new nested rescans, repeated sorts/filters, or per-workspace batch scans in hot paths.
Cmux Swift Concurrency ✅ Passed PASS: The only new DispatchQueue/continuation usage is in an XCTest helper; production diffs add only synchronous helpers and no new Combine/Task/legacy async APIs.
Cmux Swift @Concurrent ✅ Passed Diff only tweaks banner metadata; no new @concurrent/nonisolated-async changes. The added opencode helper is synchronous nonisolated, not async.
Cmux Swiftpm Lockfiles ✅ Passed PR only adds pbxproj source refs; no SwiftPM package refs, .gitignore, or Package.resolved diffs were changed.
Cmux Swift Logging ✅ Passed No added/mutated production Swift logging was found; the only Logger is correctly nonisolated private let, and no new print/NSLog/debug diagnostics appeared in runtime code.
Cmux User-Facing Error Privacy ✅ Passed PASS: The new alert copy is generic/localized, and the diff sanitizes JSON/raw payloads without exposing secrets, ids, or unredacted upstream data.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state/layout anti-patterns were introduced; changed files are pure notification/composition helpers, and existing ObservableObject/@published state is legacy.
Cmux Architecture Rethink ✅ Passed Single-store fix preserves hook-owned banner text; no timing, observers, caches, or duplicate ownership added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The only changed file is Sources/TerminalNotificationStore.swift, and it adds no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/tests/localization/config files; none match forbidden artifact dirs or artifact-like outputs.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Only production-source diff is a hookModifiedText/workspaceTitle tweak in TerminalNotificationStore.swift; no new DEBUG/ForTesting seam was added.
Title check ✅ Passed The title is concise and accurately reflects the main change: more descriptive, workspace-aware agent notifications.
✨ 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-descriptive-agent-notifs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread CLI/cmux.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces generic agent notification banners with workspace-aware, context-specific content: the OS banner title becomes the workspace name, subtitle becomes <Agent> · <status>, and the body uses the most specific available text. A new NotificationBannerComposer centralizes scrubbing and composition across all three delivery surfaces (native banner, local feedback, phone push).

  • Wire format extended: the notify meta segment gains an optional ;a=<agent-id> field; uncategorized notifications now carry c=other;p=0;a=<agent> instead of being untagged; parser remains strictly c=-anchored so legacy 3- and 4-field payloads are byte-identical.
  • Body improvement: Claude idle nags fall back to the saved/transcript assistant message instead of the generic "waiting for input" copy; generic-agent stops show "Finished: <prompt>" (with JSON-blob guard) before the boilerplate fallback; opencode gains a path-A stop notification via session.idle with per-surface deduplication.
  • Feed banners richer: permission, exit-plan, and question attention banners now compose workspace-scoped titles and tool-specific bodies (command, file basename, question text) through the same NotificationBannerComposer helper.

Confidence Score: 5/5

Safe to merge — the notification wire format is extended strictly backwards-compatibly, and all three delivery surfaces share the same scrubbed composition path.

The change spans CLI hook payloads, socket meta parsing, the notification store, feed coordinator, and the opencode JS plugin, but each boundary is well-tested with regression and integration tests. The meta parser is strictly c=-anchored so legacy 3-field payloads are byte-identical. The hookModifiedText guard correctly preserves user-defined hook rewrites. Localization is complete. No new actor-isolation or blocking-runtime issues were introduced. Both main architectural concerns from prior rounds were addressed by the author.

No files require special attention.

Important Files Changed

Filename Overview
Sources/NotificationBannerComposition.swift New 260-line file introducing NotificationBannerComposer (caseless enum namespace addressed in prior review) and private file-scope helpers; scrub-before-truncate pattern is correct.
Sources/AgentNotificationGate.swift Parser extended for optional ;a= 3-field form; c=other enforced to require the agent field; strict field-count/prefix guards preserved.
Sources/TerminalController+AgentNotifications.swift New file: OpenCodeStopNotificationDeduper (bounded OSAllocatedUnfairLock map) and v2PostOpenCodeStopNotificationIfNeeded nonisolated func with surfaceId guard.
Sources/TerminalNotificationStore.swift agentId/workspaceTitle plumbed through all applyNotification paths; hookModifiedText guard correctly suppresses workspace recomposition for hook-rewritten notifications.
Sources/Feed/FeedCoordinator.swift Feed attention banner composition delegated to NotificationBannerComposer; workspaceTitle helper @mainactor; subtitle now flows through makeFeedNotificationPolicyContext.
CLI/cmux.swift lastPrompt cleared on every PromptSubmit; promptCompletionBody fallback with JSON-blob guard; all meta payloads now carry a=claude/codex/def.name.
CLI/CMUXCLI+NotificationSummaryHelpers.swift New file extracting notification helpers; pure string utilities with no shared mutable state.
Resources/opencode-plugin.js surface_id from env var; assistantPreamble cleared on user message; last_assistant_message forwarded on session.idle.
Resources/Localizable.xcstrings Seven new string keys with both en and ja translations; matches existing catalog pattern.

Reviews (13): Last reviewed commit: "Merge origin/main into feat-descriptive-..." | Re-trigger Greptile

Comment on lines +119 to +125
let detail = notificationBannerNonEmpty(object?["command"] as? String)
?? notificationBannerFileBasename(object?["file_path"] as? String)
?? notificationBannerFileBasename(object?["filePath"] as? String)
?? notificationBannerFirstPattern(object?["patterns"])
?? tool

if let tool, let detail {

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 Permission body shows "Allow Shell: Shell" when no specific detail exists

When no command, file_path, or patterns field is found in the tool input JSON, detail falls back to tool (line 123). Both tool and detail are then non-nil and equal, so the two-argument format fires and produces the degenerate string "Allow Shell: Shell" — confirmed by permissionBodyFallsBackToToolNameForOpaqueInput which asserts that exact output. The single-argument "%@ needs approval" branch is unreachable for this case. The fix is to separate the specific-detail lookup from the tool variable so the two-arg form is only used when a meaningful distinguishing detail exists.

Suggested change
let detail = notificationBannerNonEmpty(object?["command"] as? String)
?? notificationBannerFileBasename(object?["file_path"] as? String)
?? notificationBannerFileBasename(object?["filePath"] as? String)
?? notificationBannerFirstPattern(object?["patterns"])
?? tool
if let tool, let detail {
let detail = notificationBannerNonEmpty(object?["command"] as? String)
?? notificationBannerFileBasename(object?["file_path"] as? String)
?? notificationBannerFileBasename(object?["filePath"] as? String)
?? notificationBannerFirstPattern(object?["patterns"])
if let tool, let detail {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6df8a54: detail no longer falls back to tool, so the two-arg "Allow %@: %@" form only fires with a specific detail (command / file basename / pattern); opaque input now yields "Shell needs approval". Test renamed and updated accordingly.

Comment on lines +10 to +17
nonisolated func composeNotificationBannerContent(
title: String,
subtitle: String,
body: String,
agentId: String?,
workspaceTitle: String?,
appName: String
) -> NotificationBannerContent {

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 Top-level API functions violate the no-ambient-global-state rule

composeNotificationBannerContent, composeFeedNotificationContent, and notificationBannerSnippet are module-internal nonisolated funcs at file scope, called from three different production files (TerminalNotificationStore, FeedCoordinator, TerminalController). The rule requires top-level free functions used as API to be owned by a constructable, injectable type — a caseless enum or a static-only namespace type hits the same flag. Prefer static funcs on NotificationBannerContent itself, or a dedicated NotificationBannerComposer struct whose callers construct or inject it.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6df8a54: composeNotificationBannerContent, composeFeedNotificationContent, notificationBannerSnippet, and the new JSON-blob helpers are now static members of caseless enum NotificationBannerComposer; all call sites and tests updated.

Comment thread CLI/cmux.swift Outdated
Comment on lines +26007 to +26018
return classified
}

private func isGenericClaudeNotificationBody(_ body: String) -> Bool {
let normalized = normalizedSingleLine(body)
let placeholders: Set<String> = [
"Claude is waiting for your input",
"Claude needs your input",
"Claude needs your attention"
]
return placeholders.contains(normalized)
}

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 Hardcoded English strings as a body-detection heuristic

isGenericClaudeNotificationBody matches against three hardcoded English literals to decide whether to replace the notification body. If any of those strings are changed on the generation side — or if Claude's CLI begins emitting a localized variant — the replacement silently stops firing and users see the generic body again. The cmux-reliability-single-source-of-truth rule requires correctness-critical detection to use a structured signal, not a string/title heuristic. A notification_type value of e.g. "idle_nag" emitted by the Claude hook (or a shared constant matched byte-for-byte against the generator) would make the detection robust to copy changes.

Rule Used: Flag correctness-critical detection/identity deriv... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the literal set, now documented in 6df8a54: these strings are external copy emitted by the Claude Code CLI notification hook, which offers no structured idle-nag signal today. A match miss degrades gracefully to the same generic body users saw before this PR, so it is a cosmetic fallback rather than correctness-critical detection. The doc comment says to extend the set if Claude Code copy changes.

lawrencecchen and others added 2 commits July 4, 2026 16:13
… length budget

Pure code motion, no logic changes: CLI summary helpers to CLI/CMUXCLI+NotificationSummaryHelpers.swift, opencode turn-complete banner to Sources/TerminalController+AgentNotifications.swift, TerminalNotification model to Sources/TerminalNotification.swift, banner content plumbing to Sources/TerminalNotificationStore+BannerContent.swift, and this PR's new CLI tests to cmuxTests/DescriptiveAgentNotificationCLITests.swift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…banner composer namespace

- readTranscriptSummary rejects JSON-shaped assistant messages so raw JSON never becomes a stop/idle banner body (cursor)
- opencode turn-complete banner checks the untruncated assistantFinalMessage for JSON before snippeting via new NotificationBannerComposer.assistantMessageSnippetRejectingJSONBlob (cursor + judge)
- permission body uses "Allow <tool>: <detail>" only when a specific detail exists; opaque input now yields "<tool> needs approval" instead of "Allow Shell: Shell" (greptile P1)
- banner composition helpers move under caseless enum NotificationBannerComposer (greptile P2)
- document why isGenericClaudeNotificationBody matches Claude Code copy literals (greptile P2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread CLI/cmux.swift
…ations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/TerminalController+AgentNotifications.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (2)
Sources/TerminalNotificationStore.swift (1)

1636-1665: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stale workspaceTitle after rebind to a new workspace.

tabId is updated to destinationTabId, but workspaceTitle is copied verbatim from the old notification (resolved for sourceTabId), never recomputed for destinationTabId. Since the banner composer uses workspaceTitle as the primary banner title for agent-tagged notifications, a rebound notification will display the wrong workspace name after this move.

🐛 Proposed fix
             return TerminalNotification(
                 id: notification.id,
                 tabId: destinationTabId,
                 surfaceId: notification.surfaceId,
                 panelId: notification.panelId,
                 title: notification.title,
                 subtitle: notification.subtitle,
                 body: notification.body,
                 agentId: notification.agentId,
-                workspaceTitle: notification.workspaceTitle,
+                workspaceTitle: resolvedWorkspaceTitle(forTabId: destinationTabId) ?? notification.workspaceTitle,
                 createdAt: notification.createdAt,
                 isRead: notification.isRead,
                 paneFlash: notification.paneFlash,
                 clickAction: notification.clickAction
             )
🤖 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/TerminalNotificationStore.swift` around lines 1636 - 1665, The rebind
logic in rebindSurfaceNotifications keeps the old workspaceTitle when moving a
notification from sourceTabId to destinationTabId, so the banner can show the
wrong workspace name. Update the TerminalNotification reconstruction to
recompute workspaceTitle for the destination tab/surface using the same
resolution path the composer uses, and ensure the moved notification reflects
destinationTabId consistently while preserving the other fields.
Sources/TerminalController.swift (1)

12452-12481: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a regression for legacy bodies ending in |a=...

parseNotificationPayload treats any 4th segment starting with a= as metadata, so a legacy body like Legacy Body|a=claude will be stripped instead of preserved. Add a test alongside the existing |c=... legacy-body case, or only promote the 4th segment when the full AgentNotificationMeta grammar matches.

🤖 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/TerminalController.swift` around lines 12452 - 12481,
`parseNotificationPayload` is incorrectly treating legacy bodies that end with
`|a=...` as metadata and stripping that text from the body. Update the parsing
in `TerminalController.parseNotificationPayload(_:)` so the 4th segment is only
promoted to `AgentNotificationMeta` when the full grammar parses, otherwise it
stays in the body; also add a regression test next to the existing `|c=...`
legacy-body case to cover `|a=...`.
🤖 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 `@CLI/CMUXCLI`+NotificationSummaryHelpers.swift:
- Around line 4-21: Update claudeAssistantMessageFromHookPayload to take a
non-optional [String: Any] input with a default empty dictionary at the call
site, since the function’s nil case is equivalent to no matching keys. Keep the
existing lookup logic with firstString(in:keys:) over the main object and its
extra field, then preserve the normalizedSingleLine and
isJSONBlobAssistantMessage filtering before returning the final message.
- Around line 23-31: The JSON-blob predicate is duplicated between
isJSONBlobAssistantMessage in CMUXCLI+NotificationSummaryHelpers and the
matching logic in NotificationBannerComposition, so extract it into a shared
SwiftPM target and have both call the same helper. Create a single shared
predicate function in the common module, move the trimming/JSONSerialization
check there, and update both call sites to use that shared symbol so CLI and app
stay consistent.

In `@cmuxTests/DescriptiveAgentNotificationCLITests.swift`:
- Around line 215-291: The Unix-socket client logic duplicated in
sendSocketCommands, writeSocketLine, readSocketLine, and socketTestError should
be moved into shared test support rather than kept as another private copy.
Extract the AF_UNIX connect/write/read/error-handling behavior into a reusable
helper used by this test and the existing
TerminalNotificationQueueTests/TerminalNotificationSocketActionTests codepaths,
then have sendSocketCommands delegate to that shared helper.

In `@Sources/NotificationBannerComposition.swift`:
- Around line 123-127: The fallback in feedNotificationSourceDisplayName is
incorrectly capitalizing opaque feed sources, which changes their original text.
Update the logic so RestorableAgentKind(rawValue:) still uses displayName for
known kinds, but the notificationBannerNonEmpty(source) fallback returns the
source verbatim without capitalizing it; only keep the localized Agent default
for empty or unknown inputs.

In `@Sources/TerminalNotification.swift`:
- Around line 3-17: Mark TerminalNotification as nonisolated since it is a pure
value Sendable model and should not inherit MainActor isolation by default.
Update the struct declaration itself, keeping the Identifiable, Hashable, and
Sendable conformance intact, so future consumers can use TerminalNotification
off-main without unnecessary actor hops.

---

Outside diff comments:
In `@Sources/TerminalController.swift`:
- Around line 12452-12481: `parseNotificationPayload` is incorrectly treating
legacy bodies that end with `|a=...` as metadata and stripping that text from
the body. Update the parsing in
`TerminalController.parseNotificationPayload(_:)` so the 4th segment is only
promoted to `AgentNotificationMeta` when the full grammar parses, otherwise it
stays in the body; also add a regression test next to the existing `|c=...`
legacy-body case to cover `|a=...`.

In `@Sources/TerminalNotificationStore.swift`:
- Around line 1636-1665: The rebind logic in rebindSurfaceNotifications keeps
the old workspaceTitle when moving a notification from sourceTabId to
destinationTabId, so the banner can show the wrong workspace name. Update the
TerminalNotification reconstruction to recompute workspaceTitle for the
destination tab/surface using the same resolution path the composer uses, and
ensure the moved notification reflects destinationTabId consistently while
preserving the other fields.
🪄 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: 06cf97ad-e37e-44d1-a90a-4a7d615caf0d

📥 Commits

Reviewing files that changed from the base of the PR and between a11910c and 6df8a54.

📒 Files selected for processing (20)
  • CLI/CMUXCLI+NotificationSummaryHelpers.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Resources/opencode-plugin.js
  • Sources/AgentNotificationGate.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/NotificationBannerComposition.swift
  • Sources/TerminalController+AgentNotifications.swift
  • Sources/TerminalController.swift
  • Sources/TerminalNotification.swift
  • Sources/TerminalNotificationQueue.swift
  • Sources/TerminalNotificationStore+BannerContent.swift
  • Sources/TerminalNotificationStore.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AgentNotificationGateTests.swift
  • cmuxTests/ClaudeBackgroundWorkNotifyTests.swift
  • cmuxTests/DescriptiveAgentNotificationCLITests.swift
  • cmuxTests/FeedNotificationContentTests.swift
  • cmuxTests/NotificationBannerCompositionTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift

Comment on lines +4 to +21
func claudeAssistantMessageFromHookPayload(_ object: [String: Any]?) -> String? {
guard let object else { return nil }
let keys = [
"last_assistant_message",
"lastAssistantMessage",
"assistantPreamble",
"assistant_preamble",
"assistant_response",
"assistantResponse",
]
let extra = (object["extra"] as? [String: Any]) ?? [:]
let message = firstString(in: object, keys: keys)
?? firstString(in: extra, keys: keys)
guard let message else { return nil }
let normalized = normalizedSingleLine(message)
guard !isJSONBlobAssistantMessage(normalized) else { return nil }
return normalized.isEmpty ? nil : normalized
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer non-optional [String: Any] with a default empty value.

SwiftLint flags the [String: Any]? parameter. Since the function immediately returns nil for a nil input and would equally return nil for an empty dictionary (no keys match), the optional adds no semantic value here.

🧹 Proposed fix
-    func claudeAssistantMessageFromHookPayload(_ object: [String: Any]?) -> String? {
-        guard let object else { return nil }
+    func claudeAssistantMessageFromHookPayload(_ object: [String: Any] = [:]) -> String? {
         let keys = [
📝 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
func claudeAssistantMessageFromHookPayload(_ object: [String: Any]?) -> String? {
guard let object else { return nil }
let keys = [
"last_assistant_message",
"lastAssistantMessage",
"assistantPreamble",
"assistant_preamble",
"assistant_response",
"assistantResponse",
]
let extra = (object["extra"] as? [String: Any]) ?? [:]
let message = firstString(in: object, keys: keys)
?? firstString(in: extra, keys: keys)
guard let message else { return nil }
let normalized = normalizedSingleLine(message)
guard !isJSONBlobAssistantMessage(normalized) else { return nil }
return normalized.isEmpty ? nil : normalized
}
func claudeAssistantMessageFromHookPayload(_ object: [String: Any] = [:]) -> String? {
let keys = [
"last_assistant_message",
"lastAssistantMessage",
"assistantPreamble",
"assistant_preamble",
"assistant_response",
"assistantResponse",
]
let extra = (object["extra"] as? [String: Any]) ?? [:]
let message = firstString(in: object, keys: keys)
?? firstString(in: extra, keys: keys)
guard let message else { return nil }
let normalized = normalizedSingleLine(message)
guard !isJSONBlobAssistantMessage(normalized) else { return nil }
return normalized.isEmpty ? nil : normalized
}
🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 4-4: Prefer empty collection over optional collection

(discouraged_optional_collection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+NotificationSummaryHelpers.swift around lines 4 - 21, Update
claudeAssistantMessageFromHookPayload to take a non-optional [String: Any] input
with a default empty dictionary at the call site, since the function’s nil case
is equivalent to no matching keys. Keep the existing lookup logic with
firstString(in:keys:) over the main object and its extra field, then preserve
the normalizedSingleLine and isJSONBlobAssistantMessage filtering before
returning the final message.

Source: Linters/SAST tools

Comment on lines +23 to +31
func isJSONBlobAssistantMessage(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.hasPrefix("{") || trimmed.hasPrefix("["),
let data = trimmed.data(using: .utf8),
(try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])) != nil else {
return false
}
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether NotificationBannerComposer defines its own copy of isJSONBlobAssistantMessage
fd -e swift . Sources | xargs rg -n 'func isJSONBlobAssistantMessage' -A5

Repository: manaflow-ai/cmux

Length of output: 848


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the surrounding implementations and call sites.
for f in CLI/CMUXCLI+NotificationSummaryHelpers.swift Sources/NotificationBannerComposition.swift; do
  echo "===== $f ====="
  nl -ba "$f" | sed -n '1,220p'
done

echo "===== call sites ====="
rg -n "isJSONBlobAssistantMessage" CLI Sources cmuxTests -A2 -B2

echo "===== package targets ====="
fd -a -t f Package.swift Packages . | sed -n '1,120p'

Repository: manaflow-ai/cmux

Length of output: 251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in CLI/CMUXCLI+NotificationSummaryHelpers.swift Sources/NotificationBannerComposition.swift; do
  echo "===== $f ====="
  sed -n '1,220p' "$f" | cat -n
done

echo "===== call sites ====="
rg -n "isJSONBlobAssistantMessage" CLI Sources cmuxTests -A2 -B2

echo "===== package targets ====="
find . -name Package.swift -o -path "./Packages/*/Package.swift" | sort

Repository: manaflow-ai/cmux

Length of output: 24373


Extract the JSON-blob predicate into shared code.
isJSONBlobAssistantMessage is duplicated in both CLI/CMUXCLI+NotificationSummaryHelpers.swift and Sources/NotificationBannerComposition.swift, so the CLI and app can drift on the same input. Move it into a shared SwiftPM target and call that from both sides.

🤖 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`+NotificationSummaryHelpers.swift around lines 23 - 31, The
JSON-blob predicate is duplicated between isJSONBlobAssistantMessage in
CMUXCLI+NotificationSummaryHelpers and the matching logic in
NotificationBannerComposition, so extract it into a shared SwiftPM target and
have both call the same helper. Create a single shared predicate function in the
common module, move the trimming/JSONSerialization check there, and update both
call sites to use that shared symbol so CLI and app stay consistent.

Source: Path instructions

Comment on lines +215 to +291
private nonisolated func sendSocketCommands(_ commands: [String], to socketPath: String) throws -> [String] {
let fd = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { throw socketTestError("socket(AF_UNIX)") }
defer { Darwin.close(fd) }

var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let bytes = Array(socketPath.utf8)
let maxPathLen = MemoryLayout.size(ofValue: addr.sun_path)
guard bytes.count < maxPathLen else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG))
}

withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in
let cPath = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self)
cPath.initialize(repeating: 0, count: maxPathLen)
for (index, byte) in bytes.enumerated() {
cPath[index] = CChar(bitPattern: byte)
}
}

let addrLen = socklen_t(MemoryLayout<sa_family_t>.size + bytes.count + 1)
let connectResult = withUnsafePointer(to: &addr) { ptr -> Int32 in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
Darwin.connect(fd, sockaddrPtr, addrLen)
}
}
guard connectResult == 0 else { throw socketTestError("connect(\(socketPath))") }

var responses: [String] = []
for command in commands {
try writeSocketLine(command, to: fd)
responses.append(try readSocketLine(from: fd))
}
return responses
}

private func codexLaunchEnvironment(context: ClaudeHookContext, sessionId _: String) -> [String: String] {
[
"CMUX_AGENT_LAUNCH_KIND": "codex",
"CMUX_AGENT_LAUNCH_EXECUTABLE": "/usr/local/bin/codex",
"CMUX_AGENT_LAUNCH_CWD": context.root.path,
"CMUX_AGENT_LAUNCH_ARGV_B64": base64NULSeparated(["/usr/local/bin/codex", "--model", "gpt-5.4"]),
]
}

private nonisolated func writeSocketLine(_ command: String, to fd: Int32) throws {
let payload = Array((command + "\n").utf8)
var offset = 0
while offset < payload.count {
let wrote = payload.withUnsafeBytes { raw in
Darwin.write(fd, raw.baseAddress!.advanced(by: offset), payload.count - offset)
}
guard wrote >= 0 else { throw socketTestError("write(\(command))") }
offset += wrote
}
}

private nonisolated func readSocketLine(from fd: Int32) throws -> String {
var data = Data()
var byte: UInt8 = 0
while true {
let count = Darwin.read(fd, &byte, 1)
guard count > 0 else { throw socketTestError("read") }
if byte == 0x0A { break }
data.append(byte)
}
return String(data: data, encoding: .utf8) ?? ""
}

private nonisolated func socketTestError(_ operation: String) -> NSError {
NSError(
domain: NSPOSIXErrorDomain,
code: Int(errno),
userInfo: [NSLocalizedDescriptionKey: "\(operation) failed: \(String(cString: strerror(errno)))"]
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for existing raw-socket write/connect helpers in cmuxTests to avoid duplication
rg -n --type=swift -C2 'AF_UNIX|sockaddr_un' cmuxTests | rg -v 'DescriptiveAgentNotificationCLITests'

Repository: manaflow-ai/cmux

Length of output: 48092


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Current file helpers ==\n'
sed -n '215,291p' cmuxTests/DescriptiveAgentNotificationCLITests.swift

printf '\n== Shared socket support ==\n'
sed -n '1,180p' cmuxTests/CLIMockSocketServerSupport.swift
printf '\n---\n'
sed -n '1,140p' cmuxTests/CLINotifyProcessTestSupport.swift

printf '\n== Closest existing client helpers ==\n'
sed -n '530,620p' cmuxTests/TerminalNotificationQueueTests.swift
printf '\n---\n'
sed -n '430,470p' cmuxTests/TerminalNotificationSocketActionTests.swift

Repository: manaflow-ai/cmux

Length of output: 20235


Extract the shared Unix-socket client helper
sendSocketCommands / writeSocketLine / readSocketLine / socketTestError repeat the same AF_UNIX connect/write/read/error-handling pattern already used in TerminalNotificationQueueTests.swift and TerminalNotificationSocketActionTests.swift; move that into shared test support instead of keeping another private copy 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 `@cmuxTests/DescriptiveAgentNotificationCLITests.swift` around lines 215 - 291,
The Unix-socket client logic duplicated in sendSocketCommands, writeSocketLine,
readSocketLine, and socketTestError should be moved into shared test support
rather than kept as another private copy. Extract the AF_UNIX
connect/write/read/error-handling behavior into a reusable helper used by this
test and the existing
TerminalNotificationQueueTests/TerminalNotificationSocketActionTests codepaths,
then have sendSocketCommands delegate to that shared helper.

Comment on lines +123 to +127
private nonisolated func feedNotificationSourceDisplayName(_ source: String) -> String {
RestorableAgentKind(rawValue: source)?.displayName
?? notificationBannerNonEmpty(source)?.capitalized
?? String(localized: "feed.notification.source.agent", defaultValue: "Agent")
}

@coderabbitai coderabbitai Bot Jul 4, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm RestorableAgentKind has no "custom" case and verify String.capitalized behavior assumption
rg -n "enum RestorableAgentKind" -A 30 --type=swift
rg -n "case custom" --type=swift -g '*RestorableAgentKind*'

Repository: manaflow-ai/cmux

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant symbols and inspect the helper/tests.
rg -n "enum RestorableAgentKind|struct RestorableAgentKind|typealias RestorableAgentKind|feedNotificationSourceDisplayName|permissionBodyUsesSingleToolApprovalForOpaqueInput" Sources cmuxTests -S

# Show the surrounding implementation in the reviewed file.
FILE="Sources/NotificationBannerComposition.swift"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '100,150p'
fi

# Show the enum definition if present.
rg -n "RestorableAgentKind" Sources cmuxTests -S -A 20 -B 5

Repository: manaflow-ai/cmux

Length of output: 671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focused search for the exact test expectations and any custom-source cases.
rg -n '"custom permission"|"custom · permission"|permissionBodyUsesSingleToolApprovalForOpaqueInput|source: "custom"|rawValue: source|capitalized' cmuxTests Sources -S -A 3 -B 3

Repository: manaflow-ai/cmux

Length of output: 6036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the definition by nearby names and inspect any helper that normalizes source strings.
rg -n "displayName.*source|source.*displayName|notificationBannerNonEmpty|feedNotificationSourceDisplayName|AgentKind|Restorable" Sources cmuxTests -S -A 4 -B 4

# If the reviewed file is present, dump the full helper region.
FILE="Sources/NotificationBannerComposition.swift"
[ -f "$FILE" ] && nl -ba "$FILE" | sed -n '1,220p'

Repository: manaflow-ai/cmux

Length of output: 50375


Keep opaque feed sources verbatim. notificationBannerNonEmpty(source)?.capitalized turns custom into Custom, but opaque-input banners expect the source text to stay lowercase; known agent kinds already go through displayName.

🤖 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/NotificationBannerComposition.swift` around lines 123 - 127, The
fallback in feedNotificationSourceDisplayName is incorrectly capitalizing opaque
feed sources, which changes their original text. Update the logic so
RestorableAgentKind(rawValue:) still uses displayName for known kinds, but the
notificationBannerNonEmpty(source) fallback returns the source verbatim without
capitalizing it; only keep the localized Agent default for empty or unknown
inputs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not a live bug: RestorableAgentKind(rawValue: "custom") resolves through the default branch to .custom("custom") (it's a valid vault-agent id), and .custom's displayName returns the id verbatim — lowercase, exactly what the tests assert. The .capitalized fallback is reachable only for sources that fail CmuxVaultAgentRegistration.isValidID (arbitrary non-id strings), where capitalizing is an intentional cosmetic fallback for a display label. Keeping as is.

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@lawrencecchen Thanks for the clarification — that makes sense. Since RestorableAgentKind(rawValue:) already routes valid vault-agent ids (like "custom") through .custom(id) with a verbatim lowercase displayName, the .capitalized fallback only ever applies to arbitrary non-id strings where a cosmetic capitalized label is the intended behavior. Withdrawing this comment.


🧠 Learnings used
Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-04T14:05:42.574Z
Learning: Guideline: In Swift files (cmux project), when handling pluralized strings, prefer using localization keys with the ICU-style plural forms .one and .other. For example, use keys like statusMenu.unreadCount.one for the singular case (1) and statusMenu.unreadCount.other for all other counts, and similarly for statusMenu.tooltip.unread.one/other. Rationale: ensures correct pluralization across locales and makes localization keys explicit. Review code to ensure any unread count strings and related tooltips follow this .one/.other key pattern and verify the correct value is chosen based on the count.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 954
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-05T22:04:34.712Z
Learning: Adopt the convention: for health/telemetry tri-state values in Swift, prefer Optionals (Bool?) over sentinel booleans. In TerminalController.swift, socketConnectable is Bool? and only set when socketProbePerformed is true; downstream logic must treat nil as 'not probed'. Ensure downstream code checks for nil before using a value and uses explicit non-nil checks to determine state, improving clarity and avoiding misinterpretation of default false.

Learnt from: moyashin63
Repo: manaflow-ai/cmux PR: 1074
File: Sources/AppDelegate.swift:7523-7545
Timestamp: 2026-03-09T01:38:24.337Z
Learning: When the command palette is visible (as in manaflow-ai/cmux Sources/AppDelegate.swift), ensure the shortcut handling consumes most Command shortcuts to protect the palette's text input. Specifically, do not allow UI zoom shortcuts (Cmd+Shift+= / Cmd+Shift+− / Cmd+Shift+0) to trigger while the palette is open. Do not reorder shortcut handlers (e.g., uiZoomShortcutAction(...)) to bypass this guard; users must close the palette before performing zoom actions. This guideline should apply to Swift source files handling global shortcuts within the app.

Learnt from: zlatkoc
Repo: manaflow-ai/cmux PR: 1368
File: Sources/Panels/BrowserPanel.swift:69-69
Timestamp: 2026-03-13T13:46:01.733Z
Learning: Do not wrap engine/brand name literals (e.g., displayName values such as Google, DuckDuckGo, Bing, Kagi, Startpage) in String(localized: ...). These are brand/product names that are not translatable UI text. Localization should apply to generic UI strings (labels, buttons, error messages, etc.). Apply this guideline across Swift source files under Sources/ (notably in BrowserPanel.swift and similar UI/engine-related strings) and flag only brand-name strings that are part of user-facing UI text appropriately for translation scope.

Learnt from: kjb0787
Repo: manaflow-ai/cmux PR: 1461
File: Sources/GhosttyTerminalView.swift:5904-5905
Timestamp: 2026-03-15T19:22:32.330Z
Learning: In Swift files under the Sources directory that manage terminal/scroll behavior, ensure the following: when preserving scroll across workspace switches, save savedScrollRow only if the scrollbar offset is greater than 0 (indicating the user has scrolled up). On restore, call scroll_to_row only if savedScrollRow is non-nil; if it is nil, rely on synchronizeScrollView() to keep bottom-pinned sessions following new output. This pattern should be applied wherever GhosttyTerminalView-like views implement setVisibleInUI(_:) to maintain consistent user scroll state across workspace switches.

Learnt from: MaTriXy
Repo: manaflow-ai/cmux PR: 1460
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-16T08:02:06.824Z
Learning: In Swift sources, for any panel_id-only route handling in v2PanelMarkBackground(params:) and v2PanelMarkForeground(params:), first attempt v2ResolveTabManager(params:). Use the manager only if it actually owns the panelId; otherwise fall back to AppDelegate.shared?.locateSurface(surfaceId:) to locate the correct TabManager across windows. Apply this pattern to all panel_id-only routes to avoid active-window bias.

Learnt from: pratikpakhale
Repo: manaflow-ai/cmux PR: 2011
File: Resources/Localizable.xcstrings:15256-15368
Timestamp: 2026-03-23T21:39:50.795Z
Learning: When reviewing this repo’s Swift localization usage, do not flag missing `String.localizedStringWithFormat` for calls that use the modern overload `String(localized: "key", defaultValue: "...\(variable)")` (where `defaultValue` is a `String.LocalizationValue` built with `\(…)`). That overload natively supports interpolation and the xcstrings/runtime substitution handles the resulting placeholders automatically. Only require `String.localizedStringWithFormat` when using the older `String(localized:)` overload that takes a plain `String` (i.e., where format arguments must be passed separately), such as for keys like `clipboard.sshError.single`.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: Sources/TerminalController.swift:3620-3622
Timestamp: 2026-03-25T00:32:54.735Z
Learning: When validating or reporting workspace/tab colors in this repo, only accept and use 6-digit hex colors in the form `#RRGGBB` (no alpha, i.e., do not allow `#RRGGBBAA`). Ensure validation logic matches the existing behavior (e.g., WorkspaceTabColorSettings.normalizedHex(...) and TabManager.setTabColor(tabId:color:) as well as CLI/cmux.swift). Update any error/help text for workspace color to reference only `#RRGGBB` (not `#RRGGBBAA`).

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/GhosttyTerminalView.swift:3891-3903
Timestamp: 2026-04-02T21:37:21.463Z
Learning: In Swift source files like Sources/GhosttyTerminalView.swift, avoid logging raw startup commands or initialInput even in DEBUG (to prevent leaking sensitive paths/tokens and multiline content). If you need to diagnose startup/input, log only non-sensitive metadata such as (1) presence flags (e.g., hasStartupCommand/hasInitialInput), (2) byte counts, and (3) the relevant surface id (so issues can be correlated without exposing the underlying strings).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/cmuxApp.swift:6439-6444
Timestamp: 2026-04-03T03:35:54.082Z
Learning: In this repo’s keyboard shortcut implementation, ensure `KeyboardShortcutSettings.setShortcut(...)` does nothing (no-op) when `KeyboardShortcutSettings.isManagedBySettingsFile(action)` returns `true` (i.e., the shortcut is managed via `settings.json`). This prevents writing back overrides into `UserDefaults` and keeps `settings.json` as the source of truth.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2964
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-17T21:35:25.493Z
Learning: In this repo’s shell session resume flow, always build resume commands using the cwd guard helper exposed by SessionEntry (e.g., resumeCommandWithCwd). The helper should produce a command of the form `cd <shell-quoted cwd> && <resumeCommand>`. Update all call sites that generate “resume” commands (e.g., clipboard actions, drag-drop terminal, and in-app resume) to use this helper so that rc files and newly spawned shells cannot start outside the intended directory. Avoid constructing resume commands directly without the guarded `cd` + shell-quoting + `&&` composition.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2978
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-22T08:13:36.833Z
Learning: In manaflow-ai/cmux, note that Sources/RestorableAgentSession.swift’s `SessionRestorableAgentSnapshot.resumeCommand` already includes a cwd guard when `workingDirectory` is present (it returns a string like `cd <shell-quoted cwd> && <resumeCommand>`). At call sites (e.g., `Workspace.createPanel(...)`), pass `.resumeCommand` through directly and do not prepend another `cd`/cwd guard or wrap it with an additional `cd <...> &&`—otherwise the working directory may be applied twice or incorrectly.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2978
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-22T08:14:04.901Z
Learning: In the cmux Swift sources, when restoring a restorable agent session, call sites should pass `resumeCommand` directly (ensuring it has the expected trailing newline if required) to `sendInputWhenReady`. Do not wrap `resumeCommand` with an additional `cd '<cwd>' && ...` guard, because `SessionRestorableAgentSnapshot.resumeCommand` already returns a `cwd`-guarded command; adding another guard can result in `double-cd`. Apply this especially along restore paths (e.g., `Sources/Workspace.swift` restore logic) whenever using `resumeCommand`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3084
File: Sources/AppDelegate.swift:4958-4975
Timestamp: 2026-04-22T11:37:36.238Z
Learning: In Swift code, when re-registering or updating an existing window/session context (e.g., in AppDelegate.registerMainWindow or similar flows), only update an existing *cmuxConfigStore* (or equivalent per-window configuration store) if the incoming configuration/store value is non-nil. Do not overwrite an existing per-window store with nil, so the previous per-window configuration is preserved across re-registration paths.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3128
File: Sources/Panels/BrowserPanelView.swift:4271-4290
Timestamp: 2026-04-23T11:23:49.934Z
Learning: In OmnibarSuggestionsView (and other omnibar-related debug/telemetry logging), never log raw omnibar suggestion content (e.g., URLs, titles, queries). Instead, log only non-sensitive metadata such as suggestion kind/category and the byte length of the text (e.g., "browser.suggestionClick kind=<kind> textBytes=<len>"). Apply this rule consistently to all omnibar-related debug logs to avoid leaking user/search data.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3046
File: Sources/TerminalController.swift:2096-2136
Timestamp: 2026-04-24T22:17:52.550Z
Learning: In Swift request/JSON handlers (e.g., v2 JSON-socket handlers) in Sources, prefer using the v2 helpers for numeric parsing—use `v2Int(params, "<key>")` for normal integer inputs and `v2StrictInt(...)` when strictness is required—rather than casting with `as? Int`. JSONSerialization may yield NSNumber/Double for numeric fields, so v2Int/v2StrictInt ensures correct extraction and type handling. If parsing is used for safety (e.g., timeouts), clamp/validate the parsed value as appropriate (as in `vm.exec` parsing `timeout_ms` and enforcing `>= 1`).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3166
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-27T06:58:21.434Z
Learning: In the Swift UI code (e.g., Sources/ContentView.swift), when `preferLiquidGlass`/`materialPolicy.preferLiquidGlass` is enabled for sidebar Liquid Glass, only omit the `Color` tint overlay if `NSGlassEffectView` (native Liquid Glass) is actually available. Compute `usingNativeLiquidGlass = materialPolicy.preferLiquidGlass && SidebarVisualEffectBackground.liquidGlassAvailable`, and when `usingNativeLiquidGlass` is false keep the overlay so the non-native `NSVisualEffectView` fallback still receives the configured tint.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3182
File: Sources/ContentView.swift:10850-10875
Timestamp: 2026-04-27T10:11:36.830Z
Learning: When computing NSTextView content height for NSTextView-based editors (e.g., a method like naturalDocumentHeight(...)), account for trailing newline layout. Specifically, include `layoutManager.extraLineFragmentRect.height` in the measured height only when `extraLineFragmentTextContainer == textContainer`. If you don’t, the caret on the final blank line can be clipped. Apply this rule to future NSTextView-based editors in this repo.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3139
File: Sources/Panels/FilePreviewPanel.swift:534-537
Timestamp: 2026-04-28T05:45:32.192Z
Learning: When implementing workspace/panel teardown or close-confirmation logic in Sources (e.g., close/collapse/workspace-close flows), rely on the shared dirty-state gate driven by the panel’s `isDirty` property rather than adding panel-specific teardown special-casing. Ensure each panel (including `FilePreviewPanel`) exposes and keeps its `isDirty` state up to date (e.g., via `Published private(set) var isDirty` and any subscriptions/synchronization logic), so the generic `panel.isDirty` check correctly covers all panel types during teardown.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3218
File: Sources/AppDelegate.swift:4800-4802
Timestamp: 2026-04-28T11:43:53.356Z
Learning: In Swift code that selects or activates the next main-window context (e.g., iterating window-context collections), avoid iterating `mainWindowContexts.values` directly while calling `resolvedWindow(for:)`. Since `resolvedWindow(for:)` may reindex/mutate `mainWindowContexts`, this can cause mutation-during-enumeration issues. Instead, snapshot first with `Array(mainWindowContexts.values)`, then resolve/reindex against that snapshot, and only then call `activateMainWindowContext(_:)` using the resolved result.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3247
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-29T01:08:39.652Z
Learning: Maintain the behavior contract for “Copy Workspace ID(s)”: when triggered from the sidebar context menu (e.g., in Sources/ContentView.swift TabItemView), the command must copy plain UUIDs (IDs-only), not references/refs. For command palette identifier-copy commands where refs are required, ensure the implementation explicitly passes includeRefs: true. This preserves backward compatibility for scripts expecting UUID-only output while allowing the palette to return richer payloads when needed.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3256
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-04-29T01:20:59.683Z
Learning: In this repo’s Swift implementation, keep browser-creation/open behavior consistent when `BrowserAvailabilitySettings` is disabled. For both V1 and V2, any command that includes a URL when creating/opening a browser (e.g., `open_browser` with a URL, `v2 surface.create` with `type=browser` and `url`, `browser.tab.new` with `url`, `v2 browser.open_split` with `url`) must open the URL externally using `NSWorkspace.shared.open(...)` and return appropriate success metadata. Only URL-less/blank browser creations should fail with the `browser_disabled` error.

Learnt from: pgbezerra
Repo: manaflow-ai/cmux PR: 3307
File: Sources/cmuxApp.swift:6413-6417
Timestamp: 2026-04-30T11:55:31.575Z
Learning: In this repo (manaflow-ai/cmux), when adding a new Settings section in SwiftUI (e.g., in Sources/cmuxApp.swift or related Views), don’t wire navigation/search with a raw anchor string alone. Instead: (1) create a corresponding SettingsNavigationTarget enum case (e.g., .workspaces); (2) provide the localized title, symbol, search text, and aliases for that case; (3) add/update the matching entry in SettingsSearchIndex so the sidebar/search can navigate to it; and (4) apply .settingsSearchAnchor(SettingsSearchIndex.sectionID(for: <target>)) to the section header. This prevents broken jump-to behavior by ensuring the navigation anchor and the search index stay consistent.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-02T06:07:12.997Z
Learning: In this repo’s SwiftUI views, any view placed under LazyVStack, LazyHStack, List, or ForEach must not capture or hold ObservableObject store instances (e.g., a TabManager). Instead, pass immutable value snapshots (e.g., currentSelectedTabId, sidebarIndexForTabId) plus action closures (e.g., moveToExistingWorkspace, moveToNewWorkspace). Prefer refactoring child view APIs to accept the needed values/closures rather than an ObservableObject reference (e.g., SidebarBonsplitTabWorkspaceDropOverlay should take closures instead of a TabManager).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3480
File: Sources/GhosttyTerminalView.swift:0-0
Timestamp: 2026-05-04T05:31:52.905Z
Learning: In this repo’s Swift sources, keep “surface-scoped” Ghostty config reloads strictly scoped to the target surface. Specifically, GhosttyApp.reloadSurfaceConfiguration(_:soft:source:) should update only the surface via ghostty_surface_update_config and invalidate GhosttyConfig’s load cache, but it must not replace or promote the per-surface config into GhosttyApp’s app-level config/cache (e.g., it must not overwrite GhosttyApp.config or modify app-level cached state). App-level helpers like scrollbarVisibility() and focusFollowsMouseEnabled() must continue to read GhosttyApp.config until a full app reload path is taken.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3502
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-04T11:09:27.707Z
Learning: This repo’s CI enforces a Swift file-length budget for large view files (e.g., Sources/ContentView.swift). When adding helper views/small components, avoid bloating the existing file: extract the subview into a dedicated Swift file under Sources (e.g., Sources/SidebarScrim.swift) and keep it under the CI length threshold. Use access control deliberately: if a extracted view/type must be referenced from other files, do not mark it `private` (use `internal` by omitting `private`); only use `private` for declarations that are truly local to the same file.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3471
File: Sources/CmuxTopSnapshotScopeCache.swift:1-54
Timestamp: 2026-05-05T02:19:22.055Z
Learning: In this repo (manafow-ai/cmux), `CmuxTopProcessSnapshot` and `CmuxTopProcessScope` are app-internal types defined under `Sources/`.

When reviewing Swift files under `Sources/`, do not recommend extracting/creating a separate SwiftPM package for extensions over these types (e.g., `CmuxTopSnapshotScopeCache.swift`). Only suggest SwiftPM extraction if the repo introduces a dedicated process-inspection package (e.g., a new `MuxCore`/`process-inspection`-style package) rather than trying to extract app-internal extensions in isolation.

Learnt from: psh4607
Repo: manaflow-ai/cmux PR: 3559
File: Sources/GhosttyTerminalView.swift:3824-3846
Timestamp: 2026-05-05T16:41:00.198Z
Learning: In cmux (Sources), the method `AppDelegate.shared?.workspaceContainingPanel(panelId:preferredWorkspaceId:)` returns an optional *named tuple* that includes a `workspace` field (not an optional `Workspace` directly). When using it, access the workspace through `.workspace`, e.g. `AppDelegate.shared?.workspaceContainingPanel(...)?.workspace ?? fallback`, and avoid treating the method’s return value as `Workspace?`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3582
File: Sources/SessionIndexModels.swift:261-276
Timestamp: 2026-05-06T07:12:22.050Z
Learning: In manaflow-ai/cmux, SessionEntry.resumeWorkingDirectory is the single source of truth for the working directory used in registered-agent resume commands. It must be passed as the 'workingDirectory' into AgentResumeCommandBuilder.resumeShellCommand, and resumeCommandWithCwd should consult it before prepending a 'cd <dir>' guard. Registrations with cwd: .ignore set resumeWorkingDirectory to nil, suppressing both the cwd guard in the resume command and the terminal working directory at placement time (ContentView resume, Workspace drag/drop). Apply this guidance to all Swift files under Sources (and related modules) to preserve consistent behavior.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3629
File: Sources/TerminalController.swift:8094-8105
Timestamp: 2026-05-06T10:18:57.314Z
Learning: Redact sensitive fields from all workstream event payloads before any persistence or streaming in the manaflow-ai/cmux project. Specifically redact tool_input, context, and extra in every encoded payload prior to emitting via events.stream or writing to ~/.cmuxterm/events.jsonl. Implement a centralized sanitizer in the encoding/transport stack (e.g., FeedSocketEncoding) and ensure TerminalController.v2FeedPush, CmuxEventBus.publishWorkstreamEvent, and all related encoding paths apply redaction. Add tests that verify sensitive strings do not appear in encoded event payloads. Use an allowlist approach for permitted fields and replace redacted fields with a stable placeholder (e.g., [REDACTED]). Ensure no redacted values are logged or persisted, and that CI checks enforce the rule.

Learnt from: ryouka0731
Repo: manaflow-ai/cmux PR: 3415
File: Sources/Worktree/WorktreeManager.swift:1-10
Timestamp: 2026-05-06T19:24:46.399Z
Learning: Do not recommend extracting Foundation-only Swift files into a new SwiftPM package target in this repo. The established pattern is to keep app-internal helper modules under Sources/ unless the repository introduces a dedicated package (e.g., CMUXAuthCore, CMUXDebugLog, CMUXWorkstream) with a clear architectural rationale. Speculative SwiftPM extractions for individual Foundation-only modules expand PR scope without documented justification. Apply this guideline when reviewing changes to Swift files under Sources/; require a documented package architecture justification before considering packaging changes.

Learnt from: say8425
Repo: manaflow-ai/cmux PR: 3680
File: Sources/cmuxApp.swift:0-0
Timestamp: 2026-05-07T05:01:28.476Z
Learning: In the cmux Swift app, the right sidebar’s visibility should come from `RightSidebarVisibilityTracker.shared` (an `ObservableObject`) and be bound to the active main window’s `FileExplorerState.isVisible`. Update/bind the UI state on window focus changes: `AppDelegate.activateMainWindowContext(...)` must call `bind(to:)` when the window changes so menu/command-palette titles reflect the focused window. Do not mirror these labels via a global `AppStorage("fileExplorer.isVisible")` (or other global storage) used as a label/visibility source; it should reflect the active window state instead.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3626
File: Sources/TabManager.swift:1817-1818
Timestamp: 2026-05-07T08:37:03.967Z
Learning: In this Swift repo (manafow-ai/cmux), when cleaning up stale agent process entries, use `Workspace.clearAgentPID(key:panelId:)` as the single cleanup path. Do not directly mutate `Workspace.statusEntries` or `Workspace.agentPIDs` from outside the dedicated helpers; for example, `TabManager.sweepStaleAgentPIDs` should only call `clearAgentPID` rather than performing its own mutations. This ensures panel-scoped side effects and port/refresh logic run consistently.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3626
File: Sources/TerminalController.swift:15973-15983
Timestamp: 2026-05-07T09:14:20.991Z
Learning: In the manaflow-ai/cmux Swift codebase, ensure option/argument parsing for panel/surface targeting handles explicitly provided but empty values as errors. For example, in Sources/TerminalController.swift, agentTrackingPanelTarget(options:) must treat an explicit empty value for --panel/--surface (e.g., `--panel ""`) as a parse error such as “Missing value for --panel”, rather than falling back to an unscoped/default target. This prevents unintended workspace-wide mutations when a scoped UUID/panel target is required. Apply the same validation rule to any related socket option parsers that support --panel/--surface targeting.

Learnt from: psh4607
Repo: manaflow-ai/cmux PR: 3696
File: cmuxTests/ShortcutAndCommandPaletteTests.swift:1716-1771
Timestamp: 2026-05-07T10:56:50.266Z
Learning: In the manaflow-ai/cmux repo, SwiftLint does not enforce a `required_deinit` rule (no project `.swiftlint.yml` in cmux itself, no `required_deinit` in `.github/review-bot-rules/`, and no SwiftLint CI run in `.github/workflows/`). During code reviews, do not raise findings for missing `deinit` on `XCTestCase` subclasses or other Swift classes based on a `required_deinit` rule.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3784
File: README.md:161-161
Timestamp: 2026-05-09T04:48:35.413Z
Learning: In the cmux project, the right-sidebar keyboard shortcut labels were intentionally swapped (per PR `#3784`). Reviewers should NOT flag the ⌘⇧E (Cmd+Shift+E) label as “Open file explorer.” Use these mappings consistently: ⌘⇧E → `focusRightSidebar` with the user-facing label “Toggle right sidebar focus”; ⌘⌥B (Cmd+Option+B) → `toggleFileExplorer` with the user-facing label “Open file explorer.”

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4353
File: Sources/AppIconDockTilePlugin.swift:91-97
Timestamp: 2026-05-19T07:50:03.218Z
Learning: When working with an `NSDockTilePlugIn`/dock tile plugin, remember it runs in the Dock process (`com.apple.dock`), not the main app process. Do not try to read app launch flags via `ProcessInfo.processInfo.arguments` inside the dock plugin—those arguments aren’t visible to the Dock process. For cross-process state, use a shared `UserDefaults` suite identified by the app’s bundle identifier (e.g., `UserDefaults(suiteName: appBundleIdentifier)`), since it’s accessible from both the Dock tile plugin and the main app. If you need deterministic behavior in smoke/UI tests, seed any relevant `UserDefaults` keys (e.g., via `defaults write "$BUNDLE_ID" ...`) before calling `open` so the dock plugin observes the flag before the app starts, and remove the key during cleanup after the smoke test.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4626
File: Sources/Workspace.swift:0-0
Timestamp: 2026-05-23T03:33:51.872Z
Learning: In this repo’s Swift code, the close-tab confirmation trigger must be explicit and wired through all close flows.

- For close operations, ensure the close-tab confirmation policy accepts and uses an explicit `CloseTabConfirmationTrigger` parameter.
- Specifically, `Workspace.markExplicitClose(surfaceId:trigger:)` and `TabManager.closeWorkspaceFromCloseTabGesture(_:trigger:)` must NOT rely on default parameter values for the trigger; each callsite must pass an appropriate trigger.
- At each callsite, pass the correct trigger: `.tabCloseButton` for the X-button, `.shortcut` for Cmd+W, and a purpose-specific value for programmatic/API-driven close paths.
- When touching the related trigger enums, keep them `nonisolated` and `Sendable` as required by the concurrency model.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4855
File: Sources/cmuxApp.swift:6302-6314
Timestamp: 2026-05-27T08:47:03.390Z
Learning: In the manaflow-ai/cmux settings UI, keep `app.menuBarOnly` / “Menu Bar Only” under the **App** settings bucket because it controls app presence behavior (Dock icon and Cmd+Tab visibility), not notification behavior. Ensure `SettingsNavigation` maps this setting to the `.app` section with the `menu-bar-only` anchor, and that command-palette settings descriptors report it as belonging to the **App** section. When reviewing, do not suggest moving it to **Notifications** solely because it sits near menu bar/Dock-related notification settings.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 6841
File: Sources/App/VSCodeServeWebSupport.swift:856-909
Timestamp: 2026-06-26T10:45:17.360Z
Learning: In manaflow-ai/cmux, when reviewing Swift production code for blocking synchronization (per .github/review-bot-rules/swift-blocking-runtime.md), do not flag issues if the PR only relocates existing blocking code verbatim (a move/rename/re-file move) without introducing new blocking synchronization and without worsening the blocking pattern (e.g., no additional call sites, no increased frequency/usage, and the moved code logic remains unchanged). For example, moving ServeWebOutputCollector from one Swift source to another should be treated as allowed under this exception when the diff contains only a verbatim relocation.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 6905
File: Sources/OfflineNotesStore.swift:0-0
Timestamp: 2026-06-26T12:31:31.190Z
Learning: When reviewing Swift code in this repo’s `Sources/`, do not automatically flag an app-global “persisted owner” as incorrect just because it uses a single shared instance (e.g., `static let shared`), if—and only if—the feature is backed by exactly one process-wide persistence (one store/file/DB) such that creating multiple per-window instances would cause races on the same persistence.

This singleton pattern is acceptable when the shared store is lazily created, the ownership model is truly app-global, and concurrency is handled appropriately (e.g., `MainActor` for `Observable` state or equivalent thread-safety). Also ensure test seams/injection are available so the persistence boundary can be controlled in tests.

Under these conditions, patterns like the ones used in `Sources/OfflineNotesStore.swift` (e.g., `MainActor Observable` + `static let shared`) are consistent with existing precedents; otherwise, prefer per-window instances or explicit coordination to prevent persistence races.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 6905
File: Sources/OfflineNotesStore.swift:0-0
Timestamp: 2026-06-26T12:31:53.114Z
Learning: When reviewing Swift code in this repo, an app-global `Observable` singleton (e.g., `static let shared`) can be acceptable if it truly *owns* a single persisted app-wide backing resource, so creating multiple window-scoped instances would otherwise race on the same store. Treat the pattern as non-problematic (don’t auto-flag) when all/most of these hold: (1) the singleton is the sole owner of one persisted resource (e.g., one JSON file / one shared store), (2) window-scoped instances would contend for the same underlying storage, (3) the singleton is annotated appropriately for concurrency (often `MainActor` when used on the main actor), (4) it supports testability (e.g., is injectable or can be swapped in tests), and (5) any reachability/background work is started lazily when the relevant UI/panel opens. Use the existing app-global precedents (e.g., `PaneMemoryGuardrail`, `DiffCommentStore.shared`) as confirmation of this intended pattern. Example: `Sources/OfflineNotesStore.swift` can remain `MainActor Observable static let shared` when it persists one offline-notes JSON file app-wide and lazily begins its work when the Notes panel opens.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4608
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-07-01T22:19:25.948Z
Learning: When reviewing Swift localization, do not require `String(localized:)` for v1 socket-protocol command reply strings used by the cmux CLI/hooks/tests (i.e., reply literals over the wire that are machine-parsed and must match exact text like `OK` / `ERROR: ...` or command “usage” text). Localizing these reply strings can break scripted clients and tests that match the literal replies. Only flag missing localization for genuine UI-visible copy (e.g., actual notification/display text), which should follow the project’s localization approach.

Comment on lines +3 to +17
struct TerminalNotification: Identifiable, Hashable, Sendable {
let id: UUID
let tabId: UUID
let surfaceId: UUID?
let panelId: UUID?
let title: String
let subtitle: String
let body: String
let agentId: String?
let workspaceTitle: String?
let createdAt: Date
var isRead: Bool
var paneFlash: Bool = true
var clickAction: TerminalNotificationClickAction?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider marking TerminalNotification nonisolated.

This is a newly introduced pure-value, Sendable model struct without an explicit nonisolated marker. If this target uses Swift 6 MainActor-by-default isolation (as referenced elsewhere in this repo's lint rules), the struct implicitly inherits MainActor isolation despite being a plain Sendable value type, which can force unnecessary actor hops for any future off-main consumer.

♻️ Suggested fix
-struct TerminalNotification: Identifiable, Hashable, Sendable {
+nonisolated struct TerminalNotification: Identifiable, Hashable, Sendable {
📝 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
struct TerminalNotification: Identifiable, Hashable, Sendable {
let id: UUID
let tabId: UUID
let surfaceId: UUID?
let panelId: UUID?
let title: String
let subtitle: String
let body: String
let agentId: String?
let workspaceTitle: String?
let createdAt: Date
var isRead: Bool
var paneFlash: Bool = true
var clickAction: TerminalNotificationClickAction?
nonisolated struct TerminalNotification: Identifiable, Hashable, Sendable {
let id: UUID
let tabId: UUID
let surfaceId: UUID?
let panelId: UUID?
let title: String
let subtitle: String
let body: String
let agentId: String?
let workspaceTitle: String?
let createdAt: Date
var isRead: Bool
var paneFlash: Bool = true
var clickAction: TerminalNotificationClickAction?
🤖 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/TerminalNotification.swift` around lines 3 - 17, Mark
TerminalNotification as nonisolated since it is a pure value Sendable model and
should not inherit MainActor isolation by default. Update the struct declaration
itself, keeping the Identifiable, Hashable, and Sendable conformance intact, so
future consumers can use TerminalNotification off-main without unnecessary actor
hops.

Source: Coding guidelines

…ifyProcessIntegrationRegressionTests

The budget code motion put this PR's new tests in a standalone XCTestCase that referenced ClaudeHookContext and the hook-run helpers, which are private to CLINotifyProcessIntegrationRegressionTests, so the test target failed to compile on CI. The moved tests now live in a cross-file extension of that class and the five shared helper declarations drop private.

Co-Authored-By: Claude Fable 5 <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: 1

Caution

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

⚠️ Outside diff range comments (2)
cmuxTests/DescriptiveAgentNotificationCLITests.swift (2)

252-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

codexLaunchEnvironment duplicates the helper already in CLINotifyProcessIntegrationRegressionTests.swift.

This is a near-identical private redeclaration of codexLaunchEnvironment(context:sessionId:) already defined in CLINotifyProcessIntegrationRegressionTests.swift. Since private is file-scoped, this extension file can't call the sibling's version, forcing a copy. The PR's commit message says shared helper declarations were "relaxed" so the extension could reach them — that relaxation covered ClaudeHookContext, runCodexHook, startAgentHookMockServerAccepting, makeClaudeHookContext, and runClaudeHook, but missed codexLaunchEnvironment (and agentLaunchEnvironment), leaving this duplicate. Consider dropping the private modifier on the original in CLINotifyProcessIntegrationRegressionTests.swift and deleting this copy.

🤖 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 `@cmuxTests/DescriptiveAgentNotificationCLITests.swift` around lines 252 - 259,
The helper codexLaunchEnvironment(context:sessionId:) is duplicated here because
the shared version in CLINotifyProcessIntegrationRegressionTests.swift is still
private and inaccessible from this extension. Relax the access level on the
original codexLaunchEnvironment helper (and the matching agentLaunchEnvironment
if needed) so this test file can reuse it, then remove the local duplicate from
DescriptiveAgentNotificationCLITests.

83-141: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the extra mock server here
runClaudeHook already starts its own accept loop, so startAgentHookMockServerAccepting just adds a second listener on the same socket and can make this test flaky. Keep the per-call server only, or switch the test to runClaudeHookWithoutServer if it really needs the shared agent handler.

🤖 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 `@cmuxTests/DescriptiveAgentNotificationCLITests.swift` around lines 83 - 141,
The test is starting an extra mock server listener even though runClaudeHook
already opens its own accept loop, which can create a second bind on the same
socket and make the test flaky. Remove the explicit
startAgentHookMockServerAccepting call from
testClaudeTranscriptJSONAssistantMessageDoesNotBecomeNotificationBody, or switch
the test to runClaudeHookWithoutServer if it truly needs shared agent handling.
Keep the rest of the transcript and notification assertions unchanged.
🤖 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 `@cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift`:
- Line 8622: Relax the visibility of codexLaunchEnvironment and
agentLaunchEnvironment to match the other shared harness helpers in
CLINotifyProcessIntegrationRegressionTests, so
DescriptiveAgentNotificationCLITests can reuse the same launch-environment setup
instead of duplicating it. Keep the change consistent with the
already-unprivatized symbols such as ClaudeHookContext, runCodexHook,
startAgentHookMockServerAccepting, makeClaudeHookContext, and runClaudeHook, and
update any call sites that rely on the shared environment helpers.

---

Outside diff comments:
In `@cmuxTests/DescriptiveAgentNotificationCLITests.swift`:
- Around line 252-259: The helper codexLaunchEnvironment(context:sessionId:) is
duplicated here because the shared version in
CLINotifyProcessIntegrationRegressionTests.swift is still private and
inaccessible from this extension. Relax the access level on the original
codexLaunchEnvironment helper (and the matching agentLaunchEnvironment if
needed) so this test file can reuse it, then remove the local duplicate from
DescriptiveAgentNotificationCLITests.
- Around line 83-141: The test is starting an extra mock server listener even
though runClaudeHook already opens its own accept loop, which can create a
second bind on the same socket and make the test flaky. Remove the explicit
startAgentHookMockServerAccepting call from
testClaudeTranscriptJSONAssistantMessageDoesNotBecomeNotificationBody, or switch
the test to runClaudeHookWithoutServer if it truly needs shared agent handling.
Keep the rest of the transcript and notification assertions 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: 1c34901a-969d-4ca0-b046-7445a575be7f

📥 Commits

Reviewing files that changed from the base of the PR and between 32af5e3 and c189040.

📒 Files selected for processing (2)
  • cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
  • cmuxTests/DescriptiveAgentNotificationCLITests.swift

}

private struct ClaudeHookContext {
struct ClaudeHookContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Visibility relaxation looks correct, but inconsistent with codexLaunchEnvironment.

Dropping private on ClaudeHookContext, runCodexHook, startAgentHookMockServerAccepting, makeClaudeHookContext, and runClaudeHook is a reasonable, minimal way to let the new DescriptiveAgentNotificationCLITests.swift extension reuse this file's mock-server harness. However, codexLaunchEnvironment/agentLaunchEnvironment were left private, so the other file had to redeclare its own copy of codexLaunchEnvironment — see the corresponding comment in DescriptiveAgentNotificationCLITests.swift. Consider relaxing those too for consistency.

Also applies to: 8675-8675, 8718-8718, 8787-8787, 8803-8803

🤖 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 `@cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift` at line 8622,
Relax the visibility of codexLaunchEnvironment and agentLaunchEnvironment to
match the other shared harness helpers in
CLINotifyProcessIntegrationRegressionTests, so
DescriptiveAgentNotificationCLITests can reuse the same launch-environment setup
instead of duplicating it. Keep the change consistent with the
already-unprivatized symbols such as ClaudeHookContext, runCodexHook,
startAgentHookMockServerAccepting, makeClaudeHookContext, and runClaudeHook, and
update any call sites that rely on the shared environment helpers.

… form

Codex review flagged that a bare a=<agent-id> 4th segment is easy for a
legacy pipe-containing body tail (e.g. "...|a=prod") to collide with. The
CLI now serializes uncategorized agent notifications as c=other;p=0;a=<id>
and the parser accepts agent identity only when paired with the category
grammar, so the reserved-segment surface stays the c=-anchored shape that
predates this PR. c=other without an agent id stays invalid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/TerminalNotificationStore.swift

@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

Caution

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

⚠️ Outside diff range comments (1)
cmuxTests/AgentNotificationGateTests.swift (1)

141-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise invalid IDs through the canonical meta form.

Lines 142-145 currently pass even if validAgentId accepts empty, underscore, or overlong values, because bare a= is rejected before validation. Use c=...;p=...;a=... for each invalid case.

Proposed test tightening
-        `#expect`(AgentNotificationMeta(meta: "a=") == nil)
-        `#expect`(AgentNotificationMeta(meta: "a=UPPER") == nil)
-        `#expect`(AgentNotificationMeta(meta: "a=has_underscore") == nil)
-        `#expect`(AgentNotificationMeta(meta: "a=abcdefghijklmnopqrstuvwxyzabcdefg") == nil)
+        `#expect`(AgentNotificationMeta(meta: "c=turn-complete;p=1;a=") == nil)
+        `#expect`(AgentNotificationMeta(meta: "c=turn-complete;p=1;a=UPPER") == nil)
+        `#expect`(AgentNotificationMeta(meta: "c=turn-complete;p=1;a=has_underscore") == nil)
+        `#expect`(AgentNotificationMeta(meta: "c=turn-complete;p=1;a=abcdefghijklmnopqrstuvwxyzabcdefg") == nil)
         `#expect`(AgentNotificationMeta(meta: "c=turn-complete;p=1;a=UPPER") == 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 `@cmuxTests/AgentNotificationGateTests.swift` around lines 141 - 146, The
invalid AgentNotificationMeta cases are not exercising validAgentId because bare
a= gets rejected before agent ID validation. Update the meta-based tests in
AgentNotificationGateTests to use the canonical c=...;p=...;a=... form for each
invalid agent ID case, including empty, uppercase, underscore, and overlong
values. Keep the existing meta parsing path but make sure the a= field is the
part under test so validAgentId is actually reached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/TerminalController.swift`:
- Around line 12467-12469: The metadata parsing in AgentNotificationMeta
handling is too permissive because the fourth segment is being trimmed before
strict parsing, which can incorrectly accept legacy tails as canonical metadata.
Update the parsing path in the related body-splitting logic to use the raw
fourth segment from parts[3] (without trimming) when constructing
AgentNotificationMeta, and only strip it from the body if that strict parse
succeeds.

---

Outside diff comments:
In `@cmuxTests/AgentNotificationGateTests.swift`:
- Around line 141-146: The invalid AgentNotificationMeta cases are not
exercising validAgentId because bare a= gets rejected before agent ID
validation. Update the meta-based tests in AgentNotificationGateTests to use the
canonical c=...;p=...;a=... form for each invalid agent ID case, including
empty, uppercase, underscore, and overlong values. Keep the existing meta
parsing path but make sure the a= field is the part under test so validAgentId
is actually reached.
🪄 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: 19398d88-2292-4905-8991-635894ff4a63

📥 Commits

Reviewing files that changed from the base of the PR and between c189040 and 0e5b11b.

📒 Files selected for processing (5)
  • CLI/CMUXCLI+NotificationSummaryHelpers.swift
  • Sources/AgentNotificationGate.swift
  • Sources/TerminalController.swift
  • cmuxTests/AgentNotificationGateTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +12467 to +12469
let candidate = parts[3].trimmingCharacters(in: .whitespacesAndNewlines)
if candidate.hasPrefix("c="), let parsed = AgentNotificationMeta(meta: candidate) {
if candidate.hasPrefix("c="),
let parsed = AgentNotificationMeta(meta: candidate) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not trim before strict meta parsing.

Line 12467 weakens the “exact canonical serialization” guarantee: a legacy body tail like | c=turn-complete;p=1 is trimmed, parsed as metadata, and removed from the body. Parse the raw fourth segment instead.

Proposed fix
-            let candidate = parts[3].trimmingCharacters(in: .whitespacesAndNewlines)
+            let candidate = parts[3]
             if candidate.hasPrefix("c="),
                let parsed = AgentNotificationMeta(meta: candidate) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let candidate = parts[3].trimmingCharacters(in: .whitespacesAndNewlines)
if candidate.hasPrefix("c="), let parsed = AgentNotificationMeta(meta: candidate) {
if candidate.hasPrefix("c="),
let parsed = AgentNotificationMeta(meta: candidate) {
let candidate = parts[3]
if candidate.hasPrefix("c="),
let parsed = AgentNotificationMeta(meta: candidate) {
🤖 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/TerminalController.swift` around lines 12467 - 12469, The metadata
parsing in AgentNotificationMeta handling is too permissive because the fourth
segment is being trimmed before strict parsing, which can incorrectly accept
legacy tails as canonical metadata. Update the parsing path in the related
body-splitting logic to use the raw fourth segment from parts[3] (without
trimming) when constructing AgentNotificationMeta, and only strip it from the
body if that strict parse succeeds.

lawrencecchen and others added 3 commits July 4, 2026 18:02
…sition

If a notification hook rewrote the title, subtitle, or body, the hook owns
the banner verbatim: the store drops the workspace title for that delivery
so agent recomposition never demotes a hook-provided title into the
subtitle. Untouched notifications keep the composed workspace-aware banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The opencode plugin maps every session.idle callback to a Stop feed event
with no turn identity, so a replayed idle for the same turn re-notified.
The app-side handler now records a session+message fingerprint per surface
in a bounded lock-protected map and drops repeats before enqueueing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lastPrompt is per-turn state. A prompt-submit without an extractable prompt
previously preserved the prior turn's prompt, so a later completion banner
could say "Finished: <previous prompt>". recordPromptSubmit now replaces
the field unconditionally; other session upserts still preserve it. Adds a
CLI process regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review exception (documented deliberately): the structured review flagged that permission banners now include the command text ("Allow Bash: "). Keeping as designed: showing the concrete command needing approval is the point of this PR's permission banners, the snippet is capped at 120 chars, it mirrors what the agent already prints in the visible terminal, and macOS notification previews remain OS-gated (Notification Center preview settings). If we later want redaction for tokens in commands, that's a follow-up across all permission surfaces, not just banners.

lawrencecchen and others added 3 commits July 4, 2026 18:25
Review flagged that a pure text fingerprint could permanently drop a later
turn whose banner text repeats an earlier one byte-for-byte. The plugin
pushes UserPromptSubmit through the same feed path, so the handler now
clears the surface's fingerprint on every user prompt: same-turn idle
replays stay suppressed, identical-text later turns still notify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The static property inherited TerminalController's main-actor isolation and
tripped the Swift warning budget when referenced from the nonisolated feed
path. The deduper is Sendable (lock-protected state), so nonisolated is
correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feed permission/question/plan banners quote raw tool input (shell commands,
question text), which can embed tokens, URL credentials, or key=value
secrets. Native banners escape the app via Notification Center and lock
screen previews, so the composed body now goes through CmuxFoundation's
SentryScrubber (secrets, URL credentials, emails, home paths) before
delivery. Adds a redaction test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Update on the review exception above: resolved in e258a5a instead of excepted. Feed banner bodies (permission commands, question text) now pass through CmuxFoundation's SentryScrubber before OS notification delivery, so tokens, URL credentials, key=value secrets, emails, and home paths are redacted while the command stays readable ("Allow Bash: API_TOKEN= ./deploy.sh"). Regression test added.

lawrencecchen and others added 3 commits July 5, 2026 16:18
Desktop banners route through NotificationBannerComposer's scrub at the
single materialization point, but PhonePushClient.forward() read the raw
stored TerminalNotification fields directly, bypassing it. Build the
phone payload from the same scrubbed banner content instead.

Also fixes the ordering in notificationBannerSnippet: scrub before
truncating, so a credential straddling the length boundary can't be cut
mid-pattern and evade the scrubber.
Two follow-on gaps in the banner scrubbing: (1) Path-A banners (agent stop
and idle bodies, prompt fallbacks, cmux notify, custom notification
commands) were not scrubbed at all; composeNotificationBannerContent now
redacts the body at the single materialization point every producer flows
through. (2) notificationBannerSnippet truncated before scrubbing, so a
credential straddling the length boundary could survive as a partial leak;
the helper now redacts first and truncates the redacted text. Tests cover
both, including a boundary-straddling token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e per turn

NotificationBannerComposer only scrubbed body. workspaceTitle (derived from
a path, prompt, or auto-name) now flows into native/feed banner titles and
subtitles too, so scrub every field at the single materialization point
before returning NotificationBannerContent.

Resources/opencode-plugin.js: state.assistantPreamble survived across turns,
so a session.idle firing before the new turn emitted any assistant text part
reused the previous turn's summary as the new completion banner body. Reset
it when a new user prompt arrives.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Final review-boundary note: structured review asked (pass 9) to also scrub workspace titles and subtitles in banner composition. Deliberately not doing that. Scrubbing is applied to every free-text field that carries agent/tool-produced content (permission commands, question/plan text, assistant messages, prompt fallbacks, and the phone-push forwarding path), redacted before truncation. Titles/subtitles carry the user's own workspace label, the agent display name, and localized status words; scrubbing them would redact directory-derived workspace names (the scrubber treats home paths as secrets), mangling the feature's primary output to hide the user's own path from themselves. Hook-rewritten titles are the user's own automation output by construction.

lawrencecchen and others added 3 commits July 5, 2026 16:36
UUID in the deduper tests needs it; all four app-host CI shards failed on
test-target compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OpenCode installs both the legacy CLI-subprocess plugin and the new
socket-RPC plugin, so every turn completion fired two notifications:
the CLI's generic notify_target_async path and the new app-side
v2PostOpenCodeStopNotificationIfNeeded. Set publishesStopNotification
to false for opencode (same pattern already used for grok) so only
the new descriptive path fires.

composeFeedNotificationContent scrubbed title and body but not
subtitle, leaving the source/kind label out of the redaction pass at
the same materialization point as the other fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The publishesStopNotification: false addition for opencode grew the
file to 783 lines, one over the checked-in budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

This PR's notify meta grammar now tags the Claude Stop-hook notification
with agentId "claude" (CLI/cmux.swift:23339), so the pre-existing
regression test's expected notify_target_async string needs the
;a=claude suffix to match.
Resolves conflicts from main's Kimi hook refactor (agentDefs extracted
to CMUXCLI+AgentHookCatalog.swift) intersecting with this PR's
notification descriptive-body work:

- CMUXCLI+AgentHookDefinitions.swift: took main's version (agentDefs
  moved to CMUXCLI+AgentHookCatalog.swift); reapplied this PR's
  opencode publishesStopNotification:false fix to the new file.
- CLI/cmux.swift: removed main's stale copy of notification helper
  functions this PR already relocated into
  CMUXCLI+NotificationSummaryHelpers.swift (with agentId support).
- Sources/TerminalController.swift: kept main's nonisolated annotation
  on parseNotificationPayload plus this PR's updated doc comment
  covering the a=<agent-id> meta segment.
- project.pbxproj: merged both branches' new file group/build-phase
  entries (NotificationSummaryHelpers.swift + ClaudePushNotificationHook.swift/
  AgentHookRestoreEvidence.swift).
- swift-file-length-budget.tsv: dropped the stale pre-refactor entry
  for CMUXCLI+AgentHookDefinitions.swift; main's correct 559-line entry
  already exists.

Brings in main's fix for the release-build compiler-timeout in
MobileTerminalRenderGridReplay+ModeReset.swift that was causing this
branch's stale, un-merged CI runs to fail.
…ift growth

CI's workflow-guard-tests job failed: threading agentId through the
notification queue (Sources/TerminalNotificationQueue.swift) grew the
file from 503 to 509 lines, past its tracked budget. Regenerated via
scripts/swift_file_length_budget.py --write-budget, which also tightens
a few entries that shrank since the last snapshot.
Resolve the current swift-file-length-budget and Xcode project conflicts. Tighten notification meta parsing so only byte-exact canonical c=... payload tails are stripped, and add regression coverage for whitespace-prefixed legacy body tails plus canonical invalid agent IDs.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7eadab5. Configure here.

Comment thread CLI/cmux.swift
?? parsedInput.transcriptPath
.flatMap({ readTranscriptSummary(path: $0)?.lastAssistantMessage })
.flatMap(normalizedHookValue) {
summary = (subtitle: summary.subtitle, body: truncate(replacementBody, maxLength: 180))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Idle nag prefers stale body

Medium Severity

When replacing Claude’s generic idle nag text, the handler uses the session’s saved lastBody before re-reading the transcript. If Stop stored a generic “session completed” fallback in lastBody, a later idle notification can show that boilerplate instead of a newer assistant message that exists in the transcript path supplied on the notification.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7eadab5. Configure here.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 7eadab51 Deployed Jul 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants