Skip to content

Diff viewer: review comments with per-repo persistence and TextBox attach - #5768

Merged
lawrencecchen merged 13 commits into
mainfrom
feat-diff-comments
Jun 11, 2026
Merged

lawrencecchen merged 13 commits into
mainfrom
feat-diff-comments

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Review comments on the diff viewer, Pierre/DiffHub-style: hover the line-number gutter and click the utility button (or drag a gutter selection) to open a draft composer; saved comments render inline under their line and in a Comments section at the bottom of the files sidebar.
  • Comments persist per repository on disk (Application Support/cmux/diff-comments/<repo-hash>.json) keyed by the canonical repo root, so regenerating a diff for the same repo shows the same comments. Each comment stores its anchor line's text and re-anchors by content when line numbers shift; comments whose line is gone or ambiguous show as Outdated in the sidebar only.
  • Saving a comment auto-attaches it to a terminal TextBox as a non-file attachment chip (file:line display name, submission text carrying the file path, a line-numbered excerpt, and the comment). The composer has an "Attach to terminal TextBox" checkbox (persisted, default on) as the opt-out.
  • Attach targeting is opener-first: cmux diff now embeds the opener workspace/surface (commentTarget) in the viewer payload, including for piped patches and deferred git-source pages. If the opener terminal is gone, it falls back to the only terminal with an open TextBox, then the only terminal in the diff viewer's workspace; anything ambiguous shows an in-page terminal picker. Insertion never steals focus, and attachments queue until the TextBox view mounts when the target panel is hidden.
  • New native bridge cmuxDiffComments (comments.list/save/delete/attach), installed on browser webviews but rejecting every frame that is not a main-frame page of a registered diff viewer session token.
  • New labels localized in English and Japanese; older generated diff pages fall back to inline English.

Testing

  • bun test in webviews/ (134 pass, including new anchor/re-anchor, excerpt, formatting, annotation-derivation, and sidebar-entry tests), bun run typecheck, bun run lint:ci all clean.
  • New cmuxTests/DiffCommentStoreTests.swift (store CRUD round-trip, createdAt preservation, repo isolation, key canonicalization) and DiffCommentsAttachResolutionTests (opener-first/fallback/picker matrix), wired into project.pbxproj; runs in CI tests.
  • Full app builds clean via cloud builder (tag diffc).

Issues


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


Note

Medium Risk
Touches webview↔native IPC, on-disk comment storage, and TextBox submission merging; bridge is session-gated but incorrect handling could leak or drop user review data.

Overview
Adds review comments to the diff viewer (gutter composer, inline annotations, sidebar) with per-repository JSON persistence and a workspace pending-comments pool that surfaces as a chip above terminal TextBoxes and is appended on the next submit.

cmux diff / CLI wires repoRoot into viewer pages (including piped patches via the CLI’s cwd), adds localized comment UI labels, and content-keys shared /tmp diff-viewer web assets with SHA-256 so parallel app builds don’t clobber each other’s bundles. cmuxDiffComments is the gated native bridge for list/save/delete and pool updates; new textbox.diffComments.* strings cover the chip UX.

CI Swift file-length budgets are bumped for touched large files.

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


Summary by cubic

Adds review comments to the diff viewer with per‑repo persistence and a workspace pending‑comments pool that appends to the next TextBox submission. Submission text renders as fenced diffs; a pending chip sits above the input with preview/dismiss, and per‑terminal attach targeting plus the “Attach to” dropdown are removed.

  • New Features

    • Inline gutter composer; saved comments render inline and in a Comments sidebar.
    • Per‑repo JSON persistence at Application Support/cmux/diff-comments/<repo-hash>.json; anchor by line text; outdated in the sidebar only.
    • Pending pool: saving registers precomputed submission text; one “[N comments]” chip per TextBox; first submit appends and consumes; failed submits restore; chips survive reloads.
    • Native bridge cmuxDiffComments (comments.list/save/delete) on browser webviews, gated to main‑frame pages of registered diff viewer sessions; cmux diff embeds repoRoot; labels localized (EN/JA with EN fallbacks).
  • Bug Fixes

    • Content‑key the shared webview asset cache to avoid cross‑build clobbering.
    • Trust gate accepts router‑rewritten diff viewer URLs; fragment‑insensitive token extraction; trust‑gate helpers marked MainActor; CI budgets refreshed.
    • Hide “Outdated” while files stream; show a loading state until anchoring resolves.
    • Fix double‑spaced excerpts in submissions and the chip popover; add hover‑grace so the popover is scrollable; Cmd+Enter saves in the composer.
    • Chip popover also opens on click and has extra top padding.
    • Initialize DiffCommentStore.shared’s default argument on MainActor to avoid init-time issues.

Written for commit 02dab1d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • First-class diff review comments: compose, edit, drafts, per-repo persistence, inline annotations, sidebar, gutter-created drafts, and native/webview bridge for save/list/delete/attach. Queued programmatic attachments apply when inputs mount; attach-on-save toggle supported.
  • Localization

    • Added localized comment UI labels and messages.
  • Style

    • New styles for comments sidebar, composer, entries, attach picker and attachment UI.
  • Tests

    • Expanded tests for anchoring, formatting, attach resolution, bridge tokens, and UI flows.
  • Bug Fixes

    • More reliable comment-target metadata for piped patches and improved diff viewer asset caching.

lawrencecchen and others added 2 commits June 9, 2026 23:30
Per-repo DiffCommentStore under Application Support, a cmuxDiffComments
WKScriptMessageHandlerWithReply gated to registered diff viewer pages,
TerminalPanel.insertTextBoxAttachments with a pending queue that never
steals focus, and a commentTarget (opener workspace/surface) embedded in
the diff viewer payload by the CLI for opener-first attach targeting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pierre-style line annotations: gutter utility opens a draft composer,
saved comments render inline and in a sidebar list, comments re-anchor
by line text across regenerated diffs (outdated ones stay sidebar-only),
and saving auto-attaches to the opener terminal's TextBox with an
opt-out toggle and an in-page terminal picker for ambiguous targets.

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

vercel Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 11, 2026 7:19am
cmux-staging Building Building Preview, Comment Jun 11, 2026 7:19am

@coderabbitai

coderabbitai Bot commented Jun 10, 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

Adds end-to-end diff comment support: CLI comment-target and asset-key wiring, a persistent DiffCommentStore, a WK bridge for list/save/delete/attach with trusted-session checks, terminal attach buffering and non-focusing insertion, webview types/bridge client/anchoring/formatting, React UI/components/styles, and unit tests.

Changes

Diff Comments System for Diff Viewer

Layer / File(s) Summary
CLI payload and localization wiring
CLI/cmux_open.swift, Resources/Localizable.xcstrings
Resolve repo root/opener for piped diffs, extend writeDiffViewer payload with optional commentTarget fields, content-address diff viewer app asset directory, and add localized diff comment UI strings.
Comment Data Model & Persistence
Sources/DiffCommentStore.swift, cmuxTests/DiffCommentStoreTests.swift
DiffComment model and @MainActor singleton DiffCommentStore with per-repo SHA-256 keys, JSON-backed persistence (atomic writes), in-memory test mode, and upsert/delete semantics preserving createdAt.
Native WK Bridge & Browser wiring
Sources/Panels/DiffCommentsBridge.swift, Sources/Panels/BrowserPanel.swift
DiffCommentsBridge installs on webviews, validates trusted main-frame sessions/tokens, dispatches comments.list/save/delete/targets/attach, maps JSON, resolves attach targets, and BrowserPanel installs/associates the bridge.
Terminal Delivery & TextBox Integration
Sources/Panels/TerminalPanel.swift, Sources/TextBoxInput.swift
Queue programmatic TextBox attachments while unmounted, drain pending attachments on register/move-to-window, clear pending on discard/restore, and append attachments without stealing focus.
Webview Contracts & Bridge Client
webviews/src/comments/types.ts, webviews/src/comments/bridge.ts, webviews/src/comments/labels.ts
Type definitions for comments/attachments/targets/attach-state, a WebKit bridge client (list/save/delete/attach) with error wrapper, and label fallback/resolution utilities.
Comment Anchoring & Formatting
webviews/src/comments/anchor.ts, webviews/src/comments/format.ts
Anchor detection (anchored/moved/outdated), line-index/text lookup, excerpt generation with capped lines, display-name and attachment formatting including old/new mapping for deletions.
Annotations & Sidebar Entries
webviews/src/comments/annotations.ts, webviews/src/diff-stream.ts
Compute per-item comment annotations (skip outdated), equality helpers to avoid needless updates, applyCommentAnnotations for versioned item updates, and sidebarCommentEntries including fallback anchors.
React UI Components & Integration
webviews/src/App.tsx, webviews/src/comments/*, webviews/src/styles.css, Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjs
Add CommentComposer, SavedComment, CommentsSidebarSection, useCommentsBootstrap, integrate comments into app state/reducer and useDiffComments hook, render annotations and sidebar, persist attachOnSave, manage attach phases, add CSS, and add a crypto-backed UUID export in the webview chunk.
Tests & Project Wiring
cmuxTests/DiffCommentStoreTests.swift, webviews/test/comments.test.ts, cmux.xcodeproj/project.pbxproj
Unit tests for store persistence, attach-resolution, token extraction, webview anchoring/formatting/annotation logic; Xcode project entries for new Swift sources.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5016: Modifies writeDiffViewerHTML/viewer payload construction in CLI/cmux_open.swift, overlapping viewer payload changes.
  • manaflow-ai/cmux#4451: Changes cmux diff/runDiffCommand pipeline that this PR further adjusts for source-context and opener wiring.

🐰 "I stitched the hunks with gentle paws,
saving lines and anchoring their cause.
A bridge, a store, a sidebar bright,
comments bloom in diffing light. ✨"

✨ 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-diff-comments

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2982d18c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/DiffCommentsBridge.swift Outdated
) else {
return false
}
return CmuxDiffViewerURLSchemeHandler.shared.hasActiveSession(token: components.token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow remote-patch diff viewers to use comments

When a diff viewer is served from the local HTTP URL but its manifest contains a streamed remote patch (remote_url with an empty file_path, the remote PR-diff case called out in BrowserPanel.localManifestFiles), there is no custom-scheme registration from browser.open_split, and registerFromManifest refuses that manifest. This trust check therefore returns false for an otherwise valid diff viewer frame, so every comments.list/comments.save bridge call gets not_allowed and the new comments UI is unusable for remote-streamed diffs.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds Pierre/DiffHub-style review comments to the diff viewer: gutter composer, inline annotations, sidebar, per-repo JSON persistence (Application Support/cmux/diff-comments/<repo-hash>.json), content-based re-anchoring, and a workspace-scoped pending pool that attaches saved comments to the next TextBox submission. A gated cmuxDiffComments native bridge handles list/save/delete; cmux diff now embeds repoRoot for piped patches and content-keys the shared asset cache to prevent cross-build clobbering.

  • Core data flow: saved comments register as DiffCommentSubmissionPool.Entry objects; the first TextBox to submit consumes the pool, failed submits restore it, and successful submits call DiffCommentStore.markConsumed so page reloads don't resurrect already-delivered comments.
  • Trust model: DiffCommentsBridge rejects every frame whose URL does not match a live, registered diff-viewer session token, making it safe to install the handler on all browser webviews.
  • Known open items from prior threads: synchronous disk I/O in DiffCommentStore on @MainActor, ISO8601DateFormatter allocated per comment in commentJSON, double-click/cancel-during-save race in saveDraft, and 16 of 18 xcstrings locales carrying English placeholders.

Confidence Score: 4/5

Safe to merge once the open items from prior review threads are resolved; the new code in this pass is well-structured and the trust gate is sound.

The prior review threads surfaced two substantive concerns that remain open: synchronous disk I/O in DiffCommentStore on the main thread (every save/load blocks the main runloop), and a double-click race in saveDraft that can create duplicate comments. Until those are addressed the feature has known correctness and responsiveness gaps on the save path.

Sources/DiffCommentStore.swift (main-thread disk I/O) and webviews/src/App.tsx / webviews/src/comments/CommentComposer.tsx (saveDraft in-flight guard) are the files most worth re-examining before merge.

Important Files Changed

Filename Overview
Sources/DiffCommentStore.swift New @mainactor store with per-repo JSON persistence; synchronous disk reads/writes on the main thread (flagged in a prior review thread) are the main concern.
Sources/DiffCommentSubmissionPool.swift New workspace-scoped pending-pool; uses ObservableObject/@published on a shared singleton instead of @observable, causing broad SwiftUI invalidation across all terminal TextBoxes.
Sources/Panels/DiffCommentsBridge.swift New WKScriptMessageHandlerWithReply bridge; trust-gate via session-token validation is solid; ISO8601DateFormatter allocated per comment in commentJSON (flagged in prior thread).
Sources/TextBoxInput.swift Adds pending-comments chip and submit/rollback logic; consume-then-restore pattern is correct; @ObservedObject on the shared pool causes unnecessary invalidations.
webviews/src/App.tsx Integrates comment state into the diff viewer reducer and hooks; double-click/cancel-during-save race in saveDraft was flagged in a prior thread.
webviews/src/comments/format.ts Computes submission text; format strings ("Review comment on…", "old/new version", "lines X-Y") are hardcoded English and bypass the payload label system.
webviews/src/comments/anchor.ts Anchoring, re-anchoring, and excerpt logic; well-tested; trailing-newline stripping handled correctly for line content.
webviews/src/comments/bridge.ts Thin webview-to-native bridge wrapper; trust gating deferred to Swift side; error propagation is clean.
Sources/Panels/BrowserPanel.swift Wires DiffCommentsBridge into browser panel setup; handler install and panel association look correct.
CLI/cmux_open.swift Adds repoRoot propagation for piped patches and content-keyed asset cache directories; synchronous asset hashing in CLI path is acceptable.

Sequence Diagram

sequenceDiagram
    participant W as Webview (JS)
    participant B as DiffCommentsBridge (Swift)
    participant S as DiffCommentStore
    participant P as DiffCommentSubmissionPool
    participant T as TextBoxInputContainer

    W->>B: comments.list(repoRoot)
    B->>S: comments(repoRoot:)
    S-->>B: [DiffComment]
    B->>P: setPending(entry, workspaceId) for each unconsumed
    B-->>W: "{ comments: [...] }"

    W->>B: comments.save(repoRoot, comment)
    B->>S: upsert(comment, repoRoot:)
    S-->>B: DiffComment (saved)
    B->>P: setPending(entry, workspaceId)
    B-->>W: "{ comment: saved }"

    Note over T: chip shows pending count
    T->>P: consumeAll(workspaceId)
    P-->>T: [Entry]
    T->>T: append bundle to partsToSend
    T->>T: TextBoxSubmit.send(partsToSend)
    alt submit succeeded
        T->>S: markConsumed(ids, repoRoot)
    else submit failed
        T->>P: restorePending(entries, workspaceId)
    end

    W->>B: comments.delete(repoRoot, id)
    B->>P: removePending(commentId)
    B->>S: delete(id, repoRoot)
    B-->>W: "{ deleted: true }"
Loading

Reviews (12): Last reviewed commit: "Resolve DiffCommentStore.shared default ..." | Re-trigger Greptile

Comment on lines 10626 to +10630
}
},
"diffViewer.addComment": {
"extractionState": "manual",
"localizations": {

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 16 of 18 supported locales lack actual translations

All 14 new diffViewer.* comment UI strings and diffComments.bridge.notAllowed are added for 18 locales (ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk). Only en and ja have real translations; the remaining 16 locales all carry the English source string as the "translated" value — users in Arabic, German, Spanish, French, Korean, etc. will see English text in the comment UI.

The full-internationalization rule requires catalog additions to include translated entries for every locale already supported by the touched catalog. The 16 non-Japanese, non-English locales need actual translations (or "state": "needs_review" placeholders the translation toolchain can pick up) before this merges.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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/DiffCommentStoreTests.swift`:
- Around line 155-172: Add a new unit test function named
testMultipleActiveTextBoxesWithOpenerGoneShowsPicker that calls
DiffCommentsBridge.resolveAttachTarget with a non-nil requested UUID that is not
present in the candidates list and with two candidates created via
candidate(UUID(), textBox: true); assert the result matches the .picker case
(guard case .picker(let candidates) = resolution) and
XCTAssertEqual(candidates.count, 2) to verify a picker is returned when the
requested opener is gone but multiple active text boxes exist.

In `@Sources/DiffCommentStore.swift`:
- Around line 47-58: The upsert/delete flow currently calls saveFile(...) which
updates cacheByRepoKey and swallows disk errors, causing in-memory success when
persistence failed; modify saveFile to propagate errors (make it throwable) or,
if keeping non-throwing API, detect write failures and roll back cacheByRepoKey
before returning; update callers (upsert(_:repoRoot:), delete(...), and any
other uses around lines noted) to handle the throwable saveFile
(propagate/return failure) or to check save result and revert the cached File
state so callers only report success when the disk write actually succeeded.
- Around line 37-57: The cache (cacheByRepoKey) can be stale and upsert(_:,
repoRoot:) (and the delete path) currently mutates the in-memory
RepoCommentsFile without re-reading the on-disk JSON, risking overwriting newer
changes from other processes; update upsert(_:, repoRoot:) and the delete
handler to reload the authoritative file from disk via loadFile(repoRoot:) or a
new loadFromDisk(repoRoot:) just before merging, perform an id-based merge of
comments (preserving existing createdAt for matched ids and replacing/adding
others), resolve conflicts deterministically (e.g., last-write-wins or reject on
mtime/version), and only then call saveFile(file, repoRoot:) so you never write
from a stale cached snapshot.

In `@Sources/Panels/BrowserPanel.swift`:
- Line 4219: DiffCommentsBridge.associate(panelId: id, workspaceId: workspaceId,
with: webView) is only invoked when the web view is first bound, so later
workspace changes (via updateWorkspaceId(_:) or the no-store-swap path in
reattachToWorkspace(...)) leave the bridge pointing at the old workspace; ensure
you refresh the bridge metadata by calling DiffCommentsBridge.associate with the
current panel id, the new workspaceId, and the webView whenever the workspace
changes—add that call into updateWorkspaceId(_:) and into the
reattachToWorkspace(...) no-store-swap path so comment attach resolution uses
the updated workspace context.

In `@Sources/Panels/DiffCommentsBridge.swift`:
- Around line 296-317: The comment(fromJSON:) function currently coerces unknown
anchor metadata and invalid ranges; change it to validate inputs strictly by
returning nil on bad data: verify "side" is exactly "additions" or "deletions"
(do not default unknowns to "additions"), if "endSide" is present ensure it is
also exactly "additions" or "deletions" (otherwise treat as nil/reject), and
validate that startLine and endLine are positive and endLine >= startLine
(reject otherwise) before constructing the DiffComment so upsert receives only
supported anchor values and valid ranges.
- Around line 124-150: Replace hard-coded English messages passed to
BridgeError.invalidRequest with localized strings using the same localization
API used for .notAllowed (e.g., NSLocalizedString or a project wrapper) so
userMessage is localized; update all occurrences in DiffCommentsBridge.swift
where BridgeError.invalidRequest(...) is thrown (including the blocks handling
repoRoot, comments.save malformed comment, comments.delete missing id,
comments.attach failures and the default unsupported method branch, and the
similar strings referenced around lines 169-204) to pass a localized key and
comment instead of raw English text, ensuring keys are added to the string
catalog for each message.
- Around line 121-150: The handler currently trusts params["repoRoot"]; instead,
resolve the repoRoot from the authorized diff-viewer session associated with the
incoming webView/frame and use that canonical root for all operations
(comments.list, comments.save, comments.delete, comments.attach). In practice,
inside handle(body:webView:) replace the params-derived repoRoot check with a
lookup of the registered viewer session for the provided webView (or frame),
obtain its canonical repo root, and if params["repoRoot"] exists and does not
equal the session root, throw BridgeError.invalidRequest("repoRoot mismatch");
then pass the session-derived repoRoot into store.comments(...),
store.upsert(...), store.delete(...), and into handleAttach(params:webView:) so
all persistence is scoped to the authorized session. Ensure unsupported or
missing session lookups also throw BridgeError.invalidRequest.
🪄 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: 20429d75-6615-4e27-ab28-d6ee2d42cfd5

📥 Commits

Reviewing files that changed from the base of the PR and between 2c56ef6 and a2982d1.

📒 Files selected for processing (26)
  • CLI/cmux_open.swift
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/chunks/agentSessionSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjs
  • Sources/DiffCommentStore.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/DiffCommentsBridge.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/TextBoxInput.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/DiffCommentStoreTests.swift
  • webviews/src/App.tsx
  • webviews/src/comments/CommentComposer.tsx
  • webviews/src/comments/CommentsSection.tsx
  • webviews/src/comments/SavedComment.tsx
  • webviews/src/comments/anchor.ts
  • webviews/src/comments/annotations.ts
  • webviews/src/comments/bridge.ts
  • webviews/src/comments/format.ts
  • webviews/src/comments/labels.ts
  • webviews/src/comments/types.ts
  • webviews/src/comments/useCommentsBootstrap.ts
  • webviews/src/diff-stream.ts
  • webviews/src/styles.css
  • webviews/test/comments.test.ts

Comment thread cmuxTests/DiffCommentStoreTests.swift Outdated
Comment on lines +155 to +172
func testAmbiguousTerminalsBecomePicker() {
let resolution = DiffCommentsBridge.resolveAttachTarget(
requested: nil,
candidates: [candidate(UUID(), textBox: true), candidate(UUID(), textBox: true)]
)
guard case .picker(let candidates) = resolution else {
return XCTFail("Expected picker, got \(resolution)")
}
XCTAssertEqual(candidates.count, 2)
}

func testNoTerminalsIsUnavailable() {
let resolution = DiffCommentsBridge.resolveAttachTarget(requested: nil, candidates: [])
guard case .unavailable = resolution else {
return XCTFail("Expected unavailable, got \(resolution)")
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optional: Consider adding edge-case test for requested opener gone with multiple active text boxes.

The current tests cover the case where the requested opener is gone and a single active text box wins (line 131), and where no opener is requested (nil) with multiple terminals (line 155). An additional edge case would be: requested opener is gone (not nil, but not in candidates) AND multiple active text boxes exist. This should likely resolve to a picker, similar to the nil-opener case.

However, the core attach resolution paths are well-covered, so this is a nice-to-have rather than critical.

Example edge-case test
func testMultipleActiveTextBoxesWithOpenerGoneShowsPicker() {
    let resolution = DiffCommentsBridge.resolveAttachTarget(
        requested: UUID(),  // Requested opener not in candidates
        candidates: [candidate(UUID(), textBox: true), candidate(UUID(), textBox: true)]
    )
    guard case .picker(let candidates) = resolution else {
        return XCTFail("Expected picker, got \(resolution)")
    }
    XCTAssertEqual(candidates.count, 2)
}
🤖 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/DiffCommentStoreTests.swift` around lines 155 - 172, Add a new unit
test function named testMultipleActiveTextBoxesWithOpenerGoneShowsPicker that
calls DiffCommentsBridge.resolveAttachTarget with a non-nil requested UUID that
is not present in the candidates list and with two candidates created via
candidate(UUID(), textBox: true); assert the result matches the .picker case
(guard case .picker(let candidates) = resolution) and
XCTAssertEqual(candidates.count, 2) to verify a picker is returned when the
requested opener is gone but multiple active text boxes exist.

Comment on lines +37 to +57
private var cacheByRepoKey: [String: RepoCommentsFile] = [:]

init(directoryURL: URL? = DiffCommentStore.defaultDirectoryURL()) {
self.directoryURL = directoryURL
}

func comments(repoRoot: String) -> [DiffComment] {
loadFile(repoRoot: repoRoot).comments
}

@discardableResult
func upsert(_ comment: DiffComment, repoRoot: String) -> DiffComment {
var file = loadFile(repoRoot: repoRoot)
var stored = comment
if let index = file.comments.firstIndex(where: { $0.id == comment.id }) {
stored.createdAt = file.comments[index].createdAt
file.comments[index] = stored
} else {
file.comments.append(stored)
}
saveFile(file, repoRoot: repoRoot)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reload or merge the on-disk JSON before mutating cached comments.

After the first loadFile(...), cacheByRepoKey becomes the source of truth for that repo. A later upsert/delete in this process will therefore overwrite newer comments written by another cmux process/session from a stale in-memory snapshot, which is data loss on a persisted history path. Re-read and merge by id (or reject on version/mtime mismatch) before writing.

As per coding guidelines, persistence/history paths that substitute cached values for authoritative reads must handle cold and stale caches explicitly.

Also applies to: 71-89

🤖 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/DiffCommentStore.swift` around lines 37 - 57, The cache
(cacheByRepoKey) can be stale and upsert(_:, repoRoot:) (and the delete path)
currently mutates the in-memory RepoCommentsFile without re-reading the on-disk
JSON, risking overwriting newer changes from other processes; update upsert(_:,
repoRoot:) and the delete handler to reload the authoritative file from disk via
loadFile(repoRoot:) or a new loadFromDisk(repoRoot:) just before merging,
perform an id-based merge of comments (preserving existing createdAt for matched
ids and replacing/adding others), resolve conflicts deterministically (e.g.,
last-write-wins or reject on mtime/version), and only then call saveFile(file,
repoRoot:) so you never write from a stale cached snapshot.

Source: Coding guidelines

Comment on lines +47 to +58
@discardableResult
func upsert(_ comment: DiffComment, repoRoot: String) -> DiffComment {
var file = loadFile(repoRoot: repoRoot)
var stored = comment
if let index = file.comments.firstIndex(where: { $0.id == comment.id }) {
stored.createdAt = file.comments[index].createdAt
file.comments[index] = stored
} else {
file.comments.append(stored)
}
saveFile(file, repoRoot: repoRoot)
return stored

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not return success when the disk write failed.

saveFile(...) updates cacheByRepoKey before encoding/writing and then swallows any error. That means upsert(...) and delete(...) can report success to the bridge even though nothing was persisted, so comments appear saved for the session and disappear on restart. Please make the write path throwable or roll back the cache when persistence fails.

Also applies to: 61-68, 87-102

🤖 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/DiffCommentStore.swift` around lines 47 - 58, The upsert/delete flow
currently calls saveFile(...) which updates cacheByRepoKey and swallows disk
errors, causing in-memory success when persistence failed; modify saveFile to
propagate errors (make it throwable) or, if keeping non-throwing API, detect
write failures and roll back cacheByRepoKey before returning; update callers
(upsert(_:repoRoot:), delete(...), and any other uses around lines noted) to
handle the throwable saveFile (propagate/return failure) or to check save result
and revert the cached File state so callers only report success when the disk
write actually succeeded.

}

private func bindWebView(_ webView: CmuxWebView) {
DiffCommentsBridge.associate(panelId: id, workspaceId: workspaceId, with: webView)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh the diff-comments bridge metadata on workspace moves.

Line 4219 only captures workspaceId when the web view is first bound. updateWorkspaceId(_:) and the no-store-swap path in reattachToWorkspace(...) can change the panel’s workspace later without rebinding, so comment attach resolution can keep using the old workspace and target the wrong terminal after a move.

Suggested fix
 func updateWorkspaceId(_ newWorkspaceId: UUID) {
     workspaceId = newWorkspaceId
+    DiffCommentsBridge.associate(panelId: id, workspaceId: newWorkspaceId, with: webView)
 }

 func reattachToWorkspace(
     _ newWorkspaceId: UUID,
     isRemoteWorkspace: Bool,
@@
 ) {
     workspaceId = newWorkspaceId
+    DiffCommentsBridge.associate(panelId: id, workspaceId: newWorkspaceId, with: webView)
     usesRemoteWorkspaceProxy = isRemoteWorkspace && !bypassesRemoteWorkspaceProxy
     let targetStore = isRemoteWorkspace
         ? WKWebsiteDataStore(forIdentifier: remoteWebsiteDataStoreIdentifier ?? newWorkspaceId)
         : BrowserProfileStore.shared.websiteDataStore(for: profileID)

Based on the PR objective, attach fallback is scoped to the diff viewer workspace.

🤖 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/Panels/BrowserPanel.swift` at line 4219,
DiffCommentsBridge.associate(panelId: id, workspaceId: workspaceId, with:
webView) is only invoked when the web view is first bound, so later workspace
changes (via updateWorkspaceId(_:) or the no-store-swap path in
reattachToWorkspace(...)) leave the bridge pointing at the old workspace; ensure
you refresh the bridge metadata by calling DiffCommentsBridge.associate with the
current panel id, the new workspaceId, and the webView whenever the workspace
changes—add that call into updateWorkspaceId(_:) and into the
reattachToWorkspace(...) no-store-swap path so comment attach resolution uses
the updated workspace context.

Comment thread Sources/Panels/DiffCommentsBridge.swift
Comment on lines +124 to +150
throw BridgeError.invalidRequest("Malformed bridge request")
}
let params = body["params"] as? [String: Any] ?? [:]
guard let repoRoot = (params["repoRoot"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!repoRoot.isEmpty else {
throw BridgeError.invalidRequest("Missing repoRoot")
}

switch method {
case "comments.list":
return ["comments": store.comments(repoRoot: repoRoot).map(Self.commentJSON)]
case "comments.save":
guard let commentParams = params["comment"] as? [String: Any],
let comment = Self.comment(fromJSON: commentParams) else {
throw BridgeError.invalidRequest("Malformed comment")
}
let saved = store.upsert(comment, repoRoot: repoRoot)
return ["comment": Self.commentJSON(saved)]
case "comments.delete":
guard let rawId = params["id"] as? String, let id = UUID(uuidString: rawId) else {
throw BridgeError.invalidRequest("Missing comment id")
}
return ["deleted": store.delete(id: id, repoRoot: repoRoot)]
case "comments.attach":
return try handleAttach(params: params, webView: webView)
default:
throw BridgeError.invalidRequest("Unsupported method '\(method)'")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Localize the invalidRequest reply messages.

These detail strings are returned as error.userMessage, so failures like “Missing repoRoot”, “Malformed attachment”, and “Terminal no longer exists” will surface in English even when the app is running in Japanese. Route them through localization keys the same way .notAllowed already does.

As per coding guidelines, "All user-facing strings must be localized" and Swift text must use localized APIs backed by translated string-catalog entries for every supported locale.

Also applies to: 169-204

🤖 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/Panels/DiffCommentsBridge.swift` around lines 124 - 150, Replace
hard-coded English messages passed to BridgeError.invalidRequest with localized
strings using the same localization API used for .notAllowed (e.g.,
NSLocalizedString or a project wrapper) so userMessage is localized; update all
occurrences in DiffCommentsBridge.swift where BridgeError.invalidRequest(...) is
thrown (including the blocks handling repoRoot, comments.save malformed comment,
comments.delete missing id, comments.attach failures and the default unsupported
method branch, and the similar strings referenced around lines 169-204) to pass
a localized key and comment instead of raw English text, ensuring keys are added
to the string catalog for each message.

Source: Coding guidelines

Comment on lines +296 to +317
nonisolated private static func comment(fromJSON json: [String: Any]) -> DiffComment? {
guard let filePath = json["filePath"] as? String, !filePath.isEmpty,
let side = json["side"] as? String,
let startLine = json["startLine"] as? Int,
let endLine = json["endLine"] as? Int,
let message = json["message"] as? String else {
return nil
}
let id = (json["id"] as? String).flatMap(UUID.init(uuidString:)) ?? UUID()
let now = Date()
return DiffComment(
id: id,
filePath: filePath,
side: side == "deletions" ? "deletions" : "additions",
startLine: startLine,
endLine: endLine,
endSide: json["endSide"] as? String,
lineText: json["lineText"] as? String ?? "",
message: message,
createdAt: now,
updatedAt: now
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unsupported anchor metadata instead of coercing it.

Line 309 maps any unknown side to "additions", Line 312 accepts any endSide, and this path never checks that the line range is valid. If the web client ever sends a stale enum value or impossible range, the bridge will persist a comment against the wrong anchor instead of failing fast. Validate side/endSide against the supported set and reject non-positive or inverted ranges before calling upsert.

🤖 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/Panels/DiffCommentsBridge.swift` around lines 296 - 317, The
comment(fromJSON:) function currently coerces unknown anchor metadata and
invalid ranges; change it to validate inputs strictly by returning nil on bad
data: verify "side" is exactly "additions" or "deletions" (do not default
unknowns to "additions"), if "endSide" is present ensure it is also exactly
"additions" or "deletions" (otherwise treat as nil/reject), and validate that
startLine and endLine are positive and endLine >= startLine (reject otherwise)
before constructing the DiffComment so upsert receives only supported anchor
values and valid ranges.

Every running cmux build wrote its webview bundle to the same
/tmp/cmux-diff-viewer-<uid>/assets/cmux-webviews-app directory, so two
concurrent builds with different bundles clobbered each other's chunks
and broke pages whose per-token allowlist no longer matched the files on
disk (blank diff pane, 'Importing a module script failed'). The target
directory is now cmux-webviews-app-<sha256(bundle)> so distinct bundles
coexist.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 954ec0de8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/DiffCommentsBridge.swift Outdated
Comment on lines +192 to +194
let targetParams = params["target"] as? [String: Any]
let requestedSurfaceId = (targetParams?["surfaceId"] as? String).flatMap(UUID.init(uuidString:))
let candidates = Self.attachCandidates(in: workspace)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the opener workspace when resolving attach targets

When the diff viewer has been moved/restored into a different workspace, the saved commentTarget can still identify the opener via both workspaceId and surfaceId, but the bridge only reads surfaceId and builds candidates from the workspace that currently contains the browser panel. In that case the requested opener surface is filtered out even though it still exists, so auto-attach falls through to the wrong terminal/picker instead of the opener-first behavior described by the payload.

Useful? React with 👍 / 👎.

Comment thread Sources/Panels/TerminalPanel.swift Outdated
Comment on lines +275 to +278
pendingProgrammaticTextBoxAttachments.append(contentsOf: attachments)
// Keep the published mirror truthful so session snapshots taken before
// the view mounts still include the queued attachments.
textBoxAttachments.append(contentsOf: attachments)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve queued attachments in off-window TextBox snapshots

In the branch where a TextBox view exists but is not in a window, the attachment is only queued and mirrored into textBoxAttachments; however sessionTextBoxDraftSnapshot() still returns textBoxInputView.sessionDraftSnapshot(...) whenever textBoxInputView is non-nil, so it ignores this mirror. If a session snapshot is taken before the view moves to a window, the queued diff-comment attachment is lost on restore despite this path intending to make snapshots include it.

Useful? React with 👍 / 👎.

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

Caution

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

⚠️ Outside diff range comments (1)
CLI/cmux_open.swift (1)

850-857: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate the repo-root lookup to git-backed diffs.

Line 850 now throws whenever --cwd/--repo points outside a git repo, even for patch/stdin/URL inputs. That regresses commands like cmux diff some.patch --repo /tmp: the repo root here is only needed for git sources or best-effort comment persistence, but this path now hard-fails before the patch is opened.

Suggested fix
-        if let cwd = parsedArgs.cwd {
-            diffSourceContext.repoRoot = try gitRepoRoot(startingAt: resolvePath(cwd))
-        } else if parsedArgs.source == nil {
+        if let cwd = parsedArgs.cwd, parsedArgs.source != nil {
+            diffSourceContext.repoRoot = try gitRepoRoot(startingAt: resolvePath(cwd))
+        } else if parsedArgs.source == nil {
             // Piped patches get a best-effort repo root from the CLI's cwd so
             // diff comments can persist per repository.
             diffSourceContext.repoRoot = try? gitRepoRoot(
-                startingAt: FileManager.default.currentDirectoryPath
+                startingAt: parsedArgs.cwd.map(resolvePath) ?? FileManager.default.currentDirectoryPath
             )
         }
🤖 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/cmux_open.swift` around lines 850 - 857, When computing
diffSourceContext.repoRoot from parsedArgs.cwd using gitRepoRoot(startingAt:
resolvePath(cwd)), don't let a missing/non-git repo throw for non-git-backed
inputs; change the lookup to a non-throwing best-effort call (use try? instead
of try) so parsedArgs.cwd is resolved but failure to find a git repo yields nil
instead of crashing; update the assignment to diffSourceContext.repoRoot = try?
gitRepoRoot(startingAt: resolvePath(cwd)) so behavior matches the piped/URL
branch which already uses try?.
🤖 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/cmux_open.swift`:
- Around line 5858-5866: diffViewerAppAssetContentKey(directory:) currently
re-reads and rehashes the entire bundle on every call; modify it to memoize the
computed hash per source directory (keyed by directory URL or by directory path
plus mtime snapshot) and return the cached value on subsequent calls to avoid
repeated I/O; update or add a shared cache (e.g., a static [URL: (hash:String,
mtime:Date)] dictionary) used by diffViewerAppAssetContentKey and
invalidate/update the entry when the directory mtime changes; ensure callers
like ensureDiffViewerAssets and writeCompleteGitDiffViewerHTMLSet continue to
call diffViewerAppAssetContentKey unchanged so they benefit from the cache.
- Around line 5846-5852: pruneDiffViewerFiles(in:) currently only removes stale
HTML/patch/manifest files but not the per-bundle asset directories created by
using diffViewerAppAssetContentKey(directory:), so add directory pruning logic:
in pruneDiffViewerFiles(in:) scan the assets directory for subdirectories named
with the cmux-webviews-app-<hash> prefix (or otherwise matching the targetName
pattern used when building targetName), and remove any directories whose content
key/hash does not match the currently active one (or are older than retention
policy); ensure you reference and use the same naming convention as
diffViewerAppAssetContentKey(directory:) and the targetName variable when
deciding which directories are stale so full copied app bundles are reclaimed
during diff-viewer cleanup.

---

Outside diff comments:
In `@CLI/cmux_open.swift`:
- Around line 850-857: When computing diffSourceContext.repoRoot from
parsedArgs.cwd using gitRepoRoot(startingAt: resolvePath(cwd)), don't let a
missing/non-git repo throw for non-git-backed inputs; change the lookup to a
non-throwing best-effort call (use try? instead of try) so parsedArgs.cwd is
resolved but failure to find a git repo yields nil instead of crashing; update
the assignment to diffSourceContext.repoRoot = try? gitRepoRoot(startingAt:
resolvePath(cwd)) so behavior matches the piped/URL branch which already uses
try?.
🪄 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: e3eb0c84-550e-4c29-8e6d-4ae1121abe30

📥 Commits

Reviewing files that changed from the base of the PR and between a2982d1 and 954ec0d.

📒 Files selected for processing (1)
  • CLI/cmux_open.swift

Comment thread CLI/cmux_open.swift
Comment on lines +5846 to +5852
// The shared /tmp asset cache is written by every running cmux
// build (stable, nightly, each tagged dev app). Content-key the
// directory so builds with different webview bundles coexist
// instead of clobbering each other's chunks, which broke pages
// whose per-token allowlist no longer matched the files on disk.
let targetName = "\(candidate.targetName)-\(try diffViewerAppAssetContentKey(directory: appDirectory))"
return (sourceDirectory: appDirectory, targetDirectoryName: targetName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prune stale hashed asset directories as part of diff-viewer cleanup.

This change starts creating a new assets/cmux-webviews-app-<hash> directory for each bundle revision, but pruneDiffViewerFiles(in:) only deletes stale HTML, patch, and manifest files. Old hashed asset trees are never reclaimed, so switching between tagged builds/nightlies will accumulate full copied app bundles under /tmp/cmux-diff-viewer-<uid>/assets until manual cleanup or reboot.

🤖 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/cmux_open.swift` around lines 5846 - 5852, pruneDiffViewerFiles(in:)
currently only removes stale HTML/patch/manifest files but not the per-bundle
asset directories created by using diffViewerAppAssetContentKey(directory:), so
add directory pruning logic: in pruneDiffViewerFiles(in:) scan the assets
directory for subdirectories named with the cmux-webviews-app-<hash> prefix (or
otherwise matching the targetName pattern used when building targetName), and
remove any directories whose content key/hash does not match the currently
active one (or are older than retention policy); ensure you reference and use
the same naming convention as diffViewerAppAssetContentKey(directory:) and the
targetName variable when deciding which directories are stale so full copied app
bundles are reclaimed during diff-viewer cleanup.

Comment thread CLI/cmux_open.swift
Comment on lines +5858 to +5866
private func diffViewerAppAssetContentKey(directory: URL) throws -> String {
var hasher = SHA256()
for relativePath in try diffViewerBundledAssetRelativePaths(in: directory).sorted() {
hasher.update(data: Data(relativePath.utf8))
let fileURL = directory.appendingPathComponent(relativePath, isDirectory: false)
hasher.update(data: try Data(contentsOf: fileURL, options: .mappedIfSafe))
}
let digest = hasher.finalize()
return digest.map { String(format: "%02x", $0) }.joined().prefix(12).lowercased()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Memoize the app-bundle hash instead of rehashing on every page write.

diffViewerAppAssetContentKey(directory:) re-enumerates and re-reads the entire webview bundle each time ensureDiffViewerAssets runs. writeCompleteGitDiffViewerHTMLSet can emit many pages per invocation, so one diff render now multiplies that full-bundle I/O by page count for a value that is constant within the process. Cache the hash per source directory (or per mtime snapshot) and reuse it. As per coding guidelines, .github/review-bot-rules/algorithmic-complexity.md asks us to flag repeated full-collection scans and repeated work in hot paths.

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

In `@CLI/cmux_open.swift` around lines 5858 - 5866,
diffViewerAppAssetContentKey(directory:) currently re-reads and rehashes the
entire bundle on every call; modify it to memoize the computed hash per source
directory (keyed by directory URL or by directory path plus mtime snapshot) and
return the cached value on subsequent calls to avoid repeated I/O; update or add
a shared cache (e.g., a static [URL: (hash:String, mtime:Date)] dictionary) used
by diffViewerAppAssetContentKey and invalidate/update the entry when the
directory mtime changes; ensure callers like ensureDiffViewerAssets and
writeCompleteGitDiffViewerHTMLSet continue to call diffViewerAppAssetContentKey
unchanged so they benefit from the cache.

Source: Coding guidelines

Live diff viewer pages carry '#/cmux-diff-viewer' (the in-page router
rewrites the original '#cmux-diff-viewer' fragment), so the bridge
rejected every comments call with not_allowed: saving did nothing and
attach never ran. Token extraction is now fragment-insensitive for
loopback page URLs; the registered-token check stays the trust
authority. Adds parser regression tests and a Pierre-style polish pass
(blue rounded gutter + button, cleaner comment cards).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread webviews/src/App.tsx
Comment thread CLI/cmux_open.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 710e45554c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +26
const lineRef = comment.endLine > comment.startLine
? `lines ${comment.startLine}-${comment.endLine}`
: `line ${comment.startLine}`;
const version = comment.side === "deletions" ? "old" : "new";
const excerpt = excerptFor(fileDiff, comment.side, comment.startLine, comment.endLine);
const sections = [`Review comment on ${comment.filePath} ${lineRef} (${version} version):`];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Localize the attached comment text

When a non-English user attaches a saved comment to a terminal TextBox, the inserted attachment text is always English (Review comment on ..., line(s), old/new version) because this formatter builds the user-facing text from hard-coded literals instead of the localized label payload/strings. The repo instructions require user-facing strings to be localized, so the new attach flow is partially untranslated in Japanese and any other supported locale.

Useful? React with 👍 / 👎.

…tdated badge, Pierre UI pass

A closed-TextBox opener no longer silently wins over terminals with
visibly open TextBoxes (picker instead, open-TextBox candidates listed
first). Sidebar comments whose file has not streamed in yet show as
loading instead of flashing an Outdated badge before converging. Second
Pierre styling pass: borderless composer field with a card-level focus
ring, hover-revealed card actions, softer elevation.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed02a990d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/DiffCommentsBridge.swift Outdated
Comment on lines +218 to +221
let resolution = Self.resolveAttachTarget(
requested: requestedSurfaceId,
candidates: candidates
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor explicit picker selections

When the attach flow returns a picker and the user clicks a specific terminal, the webview sends explicit: true, but this bridge ignores that flag and runs the same opener-first heuristic again. If the selected terminal does not currently have an active TextBox while any other candidate does, resolveAttachTarget returns .picker(candidates) again, so that picker item can never be chosen even though the user explicitly selected it.

Useful? React with 👍 / 👎.

The composer's bare attach checkbox becomes an 'Attach to' dropdown
listing every terminal in the workspace (open-TextBox terminals first,
opener-first default, titles disambiguated by directory and ordinal)
plus a Don't attach option, backed by a new comments.targets bridge
method. Lets the user aim a comment at any terminal when several
TextBoxes are open instead of relying on the opener heuristic.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 098bc33aeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread webviews/src/App.tsx
return;
}
const input = {
filePath: fileName(item.fileDiff, ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the commit prefix in saved comment anchors

When a streamed patch contains multiple commits that touch the same file, appendFileDiffToModel gives those entries distinct tree paths by prefixing the commit label, but the saved comment anchor keeps only fileName(item.fileDiff, ""). The annotation/sidebar code later matches comments by this filePath, so a comment left on src/foo.ts in one commit can render under every other src/foo.ts entry (or jump to the first one) in the same multi-commit diff. Persist enough of the item/tree path to distinguish prefixed entries before saving.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
webviews/src/comments/CommentComposer.tsx (1)

1-91: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider memoizing targetLabels computation.

The attachTargetOptionLabels(candidates) call on line 38 runs on every render. For typical use (few terminals) this is fine, but if a workspace has many terminals, the three-pass disambiguation logic could become a hot path. Consider wrapping it in useMemo([candidates], ...) to avoid recomputing unchanged candidate lists.

🤖 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 `@webviews/src/comments/CommentComposer.tsx` around lines 1 - 91, The
computation of targetLabels via attachTargetOptionLabels(candidates) is run on
every render and should be memoized to avoid repeated work; wrap the call in a
useMemo that depends on candidates (e.g., const targetLabels = useMemo(() =>
attachTargetOptionLabels(candidates), [candidates])) so
attachTargetOptionLabels, targetLabels and candidates are only recomputed when
candidates changes.
CLI/cmux_open.swift (2)

852-860: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't key every non-git diff to the caller's cwd repo.

Lines 852-860 now assign repoRoot from FileManager.default.currentDirectoryPath for every non-source input. That misfiles comments for remote PR URLs and patch files outside the cwd repo under the wrong repository hash, so persisted comments can bleed into unrelated repos later. Restrict the cwd fallback to stdin/piped patches, and derive local patch files from the patch file’s directory instead.

Suggested fix
-        if let cwd = parsedArgs.cwd {
-            diffSourceContext.repoRoot = try gitRepoRoot(startingAt: resolvePath(cwd))
-        } else if parsedArgs.source == nil {
-            // Piped patches get a best-effort repo root from the CLI's cwd so
-            // diff comments can persist per repository.
-            diffSourceContext.repoRoot = try? gitRepoRoot(
-                startingAt: FileManager.default.currentDirectoryPath
-            )
-        }
+        if let cwd = parsedArgs.cwd {
+            diffSourceContext.repoRoot = try gitRepoRoot(startingAt: resolvePath(cwd))
+        } else if parsedArgs.source == nil {
+            let rawInput = parsedArgs.inputs.first
+            if rawInput == nil || rawInput == "-" {
+                diffSourceContext.repoRoot = try? gitRepoRoot(
+                    startingAt: FileManager.default.currentDirectoryPath
+                )
+            } else if let rawInput,
+                      diffInputPatchURL(rawInput) == nil,
+                      diffInputTrustedRemotePatchURL(rawInput) == nil {
+                let patchPath = resolvePath(rawInput)
+                diffSourceContext.repoRoot = try? gitRepoRoot(
+                    startingAt: URL(fileURLWithPath: patchPath).deletingLastPathComponent().path
+                )
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux_open.swift` around lines 852 - 860, The current logic sets
diffSourceContext.repoRoot to FileManager.default.currentDirectoryPath for any
non-source input, which misattributes remote PR URLs and local patch files;
change the branching in the parsedArgs handling so the cwd fallback (using
gitRepoRoot(startingAt: FileManager.default.currentDirectoryPath)) is only used
when the input is piped/stdin (i.e., when parsedArgs indicates piped content),
while for a local patch file (parsedArgs.source points to a file path) derive
repoRoot from the patch file’s directory via resolvePath(parsedArgs.source) and
gitRepoRoot(startingAt: that directory); update the branches around
parsedArgs.cwd, parsedArgs.source, diffSourceContext.repoRoot and calls to
gitRepoRoot/resolvePath accordingly.

861-875: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

commentTarget is following the split target, not the opener terminal.

The new payload is built from sourceContext after resolveTargetIfNeeded(), and that resolver prefers --workspace / --surface / --window over the opener env. If I run cmux diff --workspace ... from terminal A, the saved comment will bias toward the destination workspace instead of A. Downstream, DiffCommentsBridge.handleTargets resolves candidates from the viewer workspace and only uses the requested surface as an in-workspace bias, so the real opener cannot be recovered later. Capture opener ids before applying target overrides, or update the bridge contract to honor cross-workspace opener metadata.

🤖 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/cmux_open.swift` around lines 861 - 875, The payload's commentTarget is
being set from sourceContext after
resolveTargetIfNeeded()/canonicalDiffSourceContext, which means CLI flags
(--workspace/--surface/--window) override the original opener terminal; before
calling resolveTargetIfNeeded() and canonicalDiffSourceContext(), capture the
original opener identifiers (e.g.,
openerWorkspaceId/openerSurfaceId/openerWindowId from diffSourceContext or the
opener env) and preserve them on the payload (or on diffSourceContext.opener*
fields) so commentTarget can be built from the opener IDs rather than the
post-resolution sourceContext; update places that consume these fields (notably
DiffCommentsBridge.handleTargets) to prefer the explicit opener* metadata when
present so the opener terminal can be recovered even if the user specified
workspace/surface/window overrides.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@CLI/cmux_open.swift`:
- Around line 852-860: The current logic sets diffSourceContext.repoRoot to
FileManager.default.currentDirectoryPath for any non-source input, which
misattributes remote PR URLs and local patch files; change the branching in the
parsedArgs handling so the cwd fallback (using gitRepoRoot(startingAt:
FileManager.default.currentDirectoryPath)) is only used when the input is
piped/stdin (i.e., when parsedArgs indicates piped content), while for a local
patch file (parsedArgs.source points to a file path) derive repoRoot from the
patch file’s directory via resolvePath(parsedArgs.source) and
gitRepoRoot(startingAt: that directory); update the branches around
parsedArgs.cwd, parsedArgs.source, diffSourceContext.repoRoot and calls to
gitRepoRoot/resolvePath accordingly.
- Around line 861-875: The payload's commentTarget is being set from
sourceContext after resolveTargetIfNeeded()/canonicalDiffSourceContext, which
means CLI flags (--workspace/--surface/--window) override the original opener
terminal; before calling resolveTargetIfNeeded() and
canonicalDiffSourceContext(), capture the original opener identifiers (e.g.,
openerWorkspaceId/openerSurfaceId/openerWindowId from diffSourceContext or the
opener env) and preserve them on the payload (or on diffSourceContext.opener*
fields) so commentTarget can be built from the opener IDs rather than the
post-resolution sourceContext; update places that consume these fields (notably
DiffCommentsBridge.handleTargets) to prefer the explicit opener* metadata when
present so the opener terminal can be recovered even if the user specified
workspace/surface/window overrides.

In `@webviews/src/comments/CommentComposer.tsx`:
- Around line 1-91: The computation of targetLabels via
attachTargetOptionLabels(candidates) is run on every render and should be
memoized to avoid repeated work; wrap the call in a useMemo that depends on
candidates (e.g., const targetLabels = useMemo(() =>
attachTargetOptionLabels(candidates), [candidates])) so
attachTargetOptionLabels, targetLabels and candidates are only recomputed when
candidates changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 113a8272-0980-4166-853f-38bea3d818d9

📥 Commits

Reviewing files that changed from the base of the PR and between ed02a99 and 098bc33.

📒 Files selected for processing (12)
  • CLI/cmux_open.swift
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Sources/Panels/DiffCommentsBridge.swift
  • webviews/src/App.tsx
  • webviews/src/comments/CommentComposer.tsx
  • webviews/src/comments/bridge.ts
  • webviews/src/comments/format.ts
  • webviews/src/comments/labels.ts
  • webviews/src/comments/types.ts
  • webviews/src/styles.css
  • webviews/test/comments.test.ts

Design pivot from dogfood: saved diff comments no longer attach to one
chosen terminal. Every comment is saved with a precomputed submission
text and registered in a workspace-scoped pending pool; every open
TextBox in the workspace shows one '[N comments]' chip, whichever
TextBox submits first appends the formatted comments to its submission
and marks them consumed (chips clear everywhere; failed submits restore
the pool). Comments re-register on viewer load so chips survive app
restarts. Deletes the opener heuristic, picker, composer dropdown, and
the TerminalPanel attachment-insert machinery.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 026cb9311c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2948 to +2951
if !pendingComments.isEmpty {
for (repoRoot, entries) in Dictionary(grouping: pendingComments, by: \.repoRoot) {
DiffCommentStore.shared.markConsumed(ids: entries.map(\.commentId), repoRoot: repoRoot)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop consumed comments from every workspace pool

When the same repo diff is open in more than one workspace, comments.list can register the same unconsumed comment into multiple workspace pools. A successful submit here only clears the current workspace via consumeAll and marks the store record consumed, but it leaves the same comment ID pending in the other workspace pools, so a later TextBox submit there appends and sends an already-consumed review comment again. Remove the consumed IDs from DiffCommentSubmissionPool globally when marking them consumed.

Useful? React with 👍 / 👎.

Comment thread webviews/src/App.tsx
Comment on lines +428 to +430
side,
startLine: Math.min(range.start, range.end),
endLine: Math.max(range.start, range.end),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the range end side when creating drafts

For split-diff selections that start on one side and end on the other, SelectedLineRange carries endSide, but the draft stores only side and normalizes the two line numbers with Math.min/Math.max. That turns a cross-side selection into a same-side range, so the saved excerpt/anchor can refer to unrelated old or new lines instead of the selected range. Carry endSide through the draft/comment path (or reject cross-side selections) instead of collapsing everything onto range.side.

Useful? React with 👍 / 👎.

Comment thread webviews/src/App.tsx
Comment on lines +453 to +470
.then((saved) => {
dispatch({ type: "upsert-comment", comment: saved });
dispatch({ type: "set-draft", draft: null });
})
.catch((error) => console.warn("cmux diff comment save failed", error));
};

const editMessage = (
comment: DiffCommentRecord,
message: string,
fileDiff: CommentFileDiff | null | undefined,
) => {
if (message.trim() === "") {
return;
}
const edited = { ...comment, message, updatedAt: new Date().toISOString() };
const updated = { ...edited, submissionText: commentSubmissionText(edited, fileDiff) };
const save = bridgeAvailable && repoRoot != null

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 Double-click duplicates and cancel-during-save commits comments

saveDraft starts an async bridge call and returns immediately; there is no in-flight guard on the CommentComposer during that window. Two separate paths produce incorrect behavior:

  1. Double-click: the "Comment" button is only disabled when message.trim() === "", so a second click before the first bridgeSaveComment resolves enters saveDraft again — latestState.current.draft is still set, no id is provided, and the native layer generates a fresh UUID for each call, yielding two identical saved comments.

  2. Cancel-during-save: clicking "Cancel" dispatches set-draft: null immediately, which unmounts the composer, but the in-flight save.then() still runs to completion and dispatches upsert-comment, committing the comment the user intended to discard.

A simple fix is to track an isSaving flag in CommentComposer (or as a ref in saveDraft) and disabled-gate the footer buttons until the save settles.

Comment submission text now carries a ```diff fence with paired -/+
rows and trimmed context built from the hunk content (numbered-line
fallback when hunk content is unavailable). The pending-comments chip
moves above the TextBox input styled like an attachment chip (bubble
icon, count, X to dismiss without sending) and shows the formatted
comments in a popover on hover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pierre's line arrays keep each line's trailing newline, which
double-spaced the diff excerpts in submissions and the chip popover;
line content is now stripped in one helper (also dedups anchor
matching). The chip popover survives the cursor traveling into it
(bounded cancellable grace task) so it can be scrolled, gets real top
padding, and Cmd+Enter in the composer saves the comment.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 297c537bcb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +188 to +199
private func registerPending(_ comment: DiffComment, repoRoot: String, workspaceId: UUID) {
guard comment.consumedAt == nil,
let submissionText = comment.submissionText,
!submissionText.isEmpty else {
return
}
DiffCommentSubmissionPool.shared.setPending(
DiffCommentSubmissionPool.Entry(
commentId: comment.id,
repoRoot: DiffCommentStore.canonicalRepoRoot(repoRoot),
submissionText: submissionText
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recompute queued text after re-anchoring comments

When an unsent comment is persisted and the diff is regenerated with shifted line numbers, the web UI re-anchors it from lineText, but the TextBox pool still queues the old persisted submissionText here. In that scenario the sidebar/inline marker can point at the moved line while the eventual agent submission still says the original line range/excerpt, sending stale review context. Queue text should be refreshed from the current diff/anchor on load or save before registering it pending.

Useful? React with 👍 / 👎.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b236900199

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

lineText: json["lineText"] as? String ?? "",
message: message,
submissionText: json["submissionText"] as? String,
consumedAt: nil,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve consumed state when saving edits

When an already-submitted or dismissed comment is edited, the web client saves the existing id back through comments.save, but the bridge reconstructs it with consumedAt: nil and upsert only preserves createdAt. Because comments.save immediately calls registerPending(saved, ...), editing a previously consumed sidebar comment re-queues it and the next TextBox submission sends the same review comment again. Preserve the stored consumedAt for existing ids unless the user explicitly asks to re-submit it.

Useful? React with 👍 / 👎.

The nonisolated token/trust helpers called MainActor scheme-handler APIs
(6 new warnings); they are main-thread-only callers so they become
MainActor, and the token tests follow. File length budget refreshed for
the feature growth in cmux_open/BrowserPanel/TerminalPanel.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: feecfad740

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to +32
if (anchor.state === "outdated") {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep actions available for outdated comments

When a saved comment becomes outdated after the diff is regenerated, this branch prevents rendering the only SavedComment card that exposes edit/delete actions. I checked the sidebar path in webviews/src/comments/CommentsSection.tsx; it only renders a selectable row with an outdated badge, so these persisted comments can be viewed in the list but cannot be deleted or edited and will remain stuck in the per-repo store unless they happen to re-anchor later.

Useful? React with 👍 / 👎.

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	cmux.xcodeproj/project.pbxproj

@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 80149ac. Configure here.

Comment thread Sources/Panels/DiffCommentsBridge.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80149ac454

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

guard TextBoxSubmissionFormatter.hasSubmittableContent(submittedParts) else {
let poolWorkspaceId = surface.owningWorkspace()?.id
let hasTypedContent = TextBoxSubmissionFormatter.hasSubmittableContent(submittedParts)
guard hasTypedContent || pendingCommentCount > 0 else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recheck the pool before allowing comment-only sends

When another TextBox in the same workspace (or the dismiss button) consumes the shared comment pool after this view last rendered, pendingCommentCount can still be nonzero here, so an otherwise empty TextBox passes this guard; consumeAll then returns no entries and partsToSend remains the empty submittedParts. TextBoxSubmit.dispatchEvents(for:) turns empty parts into a bare Return key, so the stale enabled send button can submit a blank enter to the terminal instead of beeping. Base the guard on the entries claimed from the pool, or re-check after consumeAll, before calling TextBoxSubmit.send.

Useful? React with 👍 / 👎.

…t body

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

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant