Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
6efaa2c
Move diff viewer status into React state
lawrencecchen Jun 3, 2026
8bb8098
Refactor diff viewer onto Pierre React primitives
lawrencecchen Jun 3, 2026
e32358b
Hide diff viewer lifecycle effects behind hooks
lawrencecchen Jun 3, 2026
2264854
Render diff viewer icons as React SVG
lawrencecchen Jun 3, 2026
1d0c0fe
Keep diff copy feedback out of status overlay
lawrencecchen Jun 3, 2026
438560d
Address diff viewer React review feedback
lawrencecchen Jun 3, 2026
a4624e5
Preserve clipboard fallback on write failure
lawrencecchen Jun 3, 2026
1aadcef
Use external Pierre diff worker asset
lawrencecchen Jun 3, 2026
846934a
Restore diff navigation select styling hooks
lawrencecchen Jun 3, 2026
4ba4b0c
Address diff viewer copy review feedback
lawrencecchen Jun 3, 2026
51ee179
Fix repeated-path diff item ids
lawrencecchen Jun 3, 2026
19d1351
Sync Pierre worker render options
lawrencecchen Jun 3, 2026
304cd79
Fix diff viewer navigation and CodeView viewport
lawrencecchen Jun 3, 2026
4d945c3
Address diff viewer menu accessibility feedback
lawrencecchen Jun 3, 2026
e8bd0d5
Fix appended Pierre tree git status fallback
lawrencecchen Jun 3, 2026
0ad95e9
Use exclusive Pierre tree selection
lawrencecchen Jun 3, 2026
d73885b
Sync Pierre worker asset version
lawrencecchen Jun 3, 2026
066c501
Copy JavaScript diff viewer worker assets
lawrencecchen Jun 3, 2026
9cf96b4
Allow diff viewer JavaScript worker assets
lawrencecchen Jun 3, 2026
ae6033e
Localize unnamed diff file fallback
lawrencecchen Jun 3, 2026
287d9ac
Allow diff viewer JS assets in custom scheme
lawrencecchen Jun 3, 2026
03dfd5b
Keep diff visible on copy failure
lawrencecchen Jun 3, 2026
74d571e
Avoid redundant diff worker option sync
lawrencecchen Jun 4, 2026
7454c5b
Avoid rebuilding diff file tree per batch
lawrencecchen Jun 4, 2026
ab900fd
Restore full file tree status after reset
lawrencecchen Jun 4, 2026
ffe6549
Improve diff viewer sidebar ergonomics
lawrencecchen Jun 4, 2026
a1148de
Add diff viewer stress samples
lawrencecchen Jun 4, 2026
e984010
Use local git for diff stress samples
lawrencecchen Jun 4, 2026
f1c0072
Localize diff viewer copy failure
lawrencecchen Jun 4, 2026
ac24014
Use historical bases for stress diffs
lawrencecchen Jun 4, 2026
01a7d6b
Reduce diff viewer hot-path churn
lawrencecchen Jun 4, 2026
b8d7c6b
Tighten diff viewer streaming updates
lawrencecchen Jun 4, 2026
8ea122d
Preserve diff tree append planning
lawrencecchen Jun 4, 2026
5a8a920
Bind diff viewer server reuse to current executable
lawrencecchen Jun 4, 2026
50e0d44
Fix diff viewer tree lanes and semantic colors
lawrencecchen Jun 4, 2026
62e8d51
Merge remote-tracking branch 'origin/main' into task-diff-viewer-reac…
lawrencecchen Jun 4, 2026
87f9558
Fix diff viewer toolbar overflow
lawrencecchen Jun 4, 2026
3213c1b
Make diff viewer code surface transparent
lawrencecchen Jun 4, 2026
dbe7678
Avoid full tree prep during diff streaming
lawrencecchen Jun 4, 2026
9b65c3c
Normalize diff separator backgrounds
lawrencecchen Jun 4, 2026
19351af
Fix diff stress sample setup and tree reset prep
lawrencecchen Jun 4, 2026
d648439
Use full tree status for coalesced stream updates
lawrencecchen Jun 4, 2026
86a5a07
Keep diff separator labels visible
lawrencecchen Jun 4, 2026
eeead53
Restore Pierre diff separator text and token colors
lawrencecchen Jun 4, 2026
9913209
Give diff file headers a readable surface
lawrencecchen Jun 4, 2026
c46565f
Use Pierre separator surface for file headers
lawrencecchen Jun 4, 2026
f7c1525
Force file header surface above Pierre defaults
lawrencecchen Jun 4, 2026
5b7f9ce
Bound diff worker pool size
lawrencecchen Jun 4, 2026
528fe3d
Tighten diff viewer React sync
lawrencecchen Jun 4, 2026
2ae83fc
Satisfy React Doctor on diff viewer sync
lawrencecchen Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ jobs:
- name: Verify generated diff viewer assets
run: ./scripts/build-diff-viewer-app.sh --check

- name: Verify React Compiler for diff viewer
run: ./scripts/check-diff-viewer-react-compiler.mjs

- name: Typecheck diff viewer
working-directory: diff-viewer
run: bun run typecheck
Expand Down
50 changes: 43 additions & 7 deletions CLI/cmux_open.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,10 @@ extension CMUXCLI {
var pid: Int32
var rootPath: String
var protocolVersion: String?
var executablePath: String?
}

private static let diffViewerHTTPServerProtocolVersion = "wait-v2 remote-stream manifest-refresh react-app-v1"
private static let diffViewerHTTPServerProtocolVersion = "wait-v2 remote-stream manifest-refresh react-app-v2 executable-bound"
private static let diffViewerHTTPServerHealthResponse = Data("ok \(diffViewerHTTPServerProtocolVersion)\n".utf8)

private struct DiffViewerLabels {
Expand All @@ -402,6 +403,7 @@ extension CMUXCLI {
"commit": CMUXDiffViewerLocalization.string("about.commit", defaultValue: "Commit"),
"collapseAllDiffs": CMUXDiffViewerLocalization.string("diffViewer.collapseAllDiffs", defaultValue: "Collapse all diffs"),
"collapseUnchangedContext": CMUXDiffViewerLocalization.string("diffViewer.collapseUnchangedContext", defaultValue: "Collapse unchanged context"),
"copyFailedGitApplyCommand": CMUXDiffViewerLocalization.string("diffViewer.copyFailedGitApplyCommand", defaultValue: "Could not copy git apply command."),
"copiedGitApplyCommand": CMUXDiffViewerLocalization.string("diffViewer.copiedGitApplyCommand", defaultValue: "Copied git apply command"),
"copyGitApplyCommand": CMUXDiffViewerLocalization.string("diffViewer.copyGitApplyCommand", defaultValue: "Copy git apply command"),
"deletions": CMUXDiffViewerLocalization.string("diffViewer.deletions", defaultValue: "Deletions"),
Expand Down Expand Up @@ -4105,6 +4107,7 @@ extension CMUXCLI {
state.rootPath == rootDirectory.path,
state.protocolVersion == Self.diffViewerHTTPServerProtocolVersion,
(1...65535).contains(state.port),
diffViewerHTTPServerStateMatchesCurrentExecutable(state),
diffViewerHTTPServerIsReachable(port: state.port) {
guard let url = URL(string: "http://127.0.0.1:\(state.port)") else {
throw CLIError(message: "Failed to build diff viewer server URL")
Expand All @@ -4128,6 +4131,38 @@ extension CMUXCLI {
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}

private func diffViewerHTTPServerStateMatchesCurrentExecutable(_ state: DiffViewerHTTPServerState) -> Bool {
guard state.pid > 0,
let currentExecutablePath = resolvedExecutableURL()?.path,
let serverExecutablePath = diffViewerHTTPServerExecutablePath(pid: state.pid),
serverExecutablePath == currentExecutablePath else {
return false
}

guard let recordedExecutablePath = state.executablePath else {
return true
}
return recordedExecutablePath == currentExecutablePath
}

private func diffViewerHTTPServerExecutablePath(pid: Int32) -> String? {
var buffer = [CChar](repeating: 0, count: 4096)
let count = buffer.withUnsafeMutableBufferPointer { pointer -> Int32 in
guard let baseAddress = pointer.baseAddress else { return 0 }
return proc_pidpath(pid, baseAddress, UInt32(pointer.count))
}
guard count > 0 else {
return nil
}

let rawPath = String(cString: buffer)
if let resolvedPath = realpath(rawPath, nil) {
defer { free(resolvedPath) }
return URL(fileURLWithPath: String(cString: resolvedPath)).standardizedFileURL.path
}
return URL(fileURLWithPath: rawPath).standardizedFileURL.path
}

private func startDiffViewerHTTPServer(rootDirectory: URL) throws -> URL {
guard let executableURL = resolvedExecutableURL() else {
throw CLIError(message: "Failed to resolve cmux executable for diff viewer server")
Expand Down Expand Up @@ -4265,7 +4300,8 @@ extension CMUXCLI {
port: port,
pid: getpid(),
rootPath: rootDirectory.path,
protocolVersion: Self.diffViewerHTTPServerProtocolVersion
protocolVersion: Self.diffViewerHTTPServerProtocolVersion,
executablePath: resolvedExecutableURL()?.path
),
rootDirectory: rootDirectory
)
Expand Down Expand Up @@ -5037,7 +5073,7 @@ extension CMUXCLI {
return path.hasSuffix(".html")
}
if mimeType == "text/javascript" {
return path.hasSuffix(".mjs")
return path.hasSuffix(".mjs") || path.hasSuffix(".js")
}
if mimeType == "text/x-diff" {
return path.hasSuffix(".patch")
Expand Down Expand Up @@ -5461,7 +5497,7 @@ extension CMUXCLI {

private func ensureDiffViewerAssets(nextTo viewerURL: URL) throws -> DiffViewerAssets {
let sourceDirectory = try diffViewerBundledAssetDirectory()
let assetDirectoryName = "pierre-diffs-1.2.1-trees-1.0.0-beta.4"
let assetDirectoryName = "pierre-diffs-1.2.7-trees-1.0.0-beta.4"
let targetDirectory = viewerURL.deletingLastPathComponent()
.appendingPathComponent("assets", isDirectory: true)
.appendingPathComponent(assetDirectoryName, isDirectory: true)
Expand All @@ -5478,7 +5514,7 @@ extension CMUXCLI {
guard assetPaths.contains("diffs.mjs"),
assetPaths.contains("trees.mjs"),
assetPaths.contains("worker-pool/worker-pool.mjs"),
assetPaths.contains("worker-pool/worker-portable.mjs") else {
assetPaths.contains("worker-pool/worker-portable.js") else {
throw CLIError(message: "Bundled diff viewer entry assets not found")
}
for assetPath in assetPaths {
Expand All @@ -5498,7 +5534,7 @@ extension CMUXCLI {
diffsModuleURL: "./assets/\(assetDirectoryName)/diffs.mjs",
treesModuleURL: "./assets/\(assetDirectoryName)/trees.mjs",
workerPoolModuleURL: "./assets/\(assetDirectoryName)/worker-pool/worker-pool.mjs",
workerModuleURL: "./assets/\(assetDirectoryName)/worker-pool/worker-portable.mjs",
workerModuleURL: "./assets/\(assetDirectoryName)/worker-pool/worker-portable.js",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow the .js worker in the custom-scheme allowlist

When the diff viewer is restored through cmux-diff-viewer://, this .js worker URL is resolved to a custom-scheme asset, but the app-side scheme handler still rejects text/javascript allowlist entries unless the request path ends in .mjs (Sources/Panels/BrowserPanel.swift:2777-2783). Since the manifest now includes worker-portable.js, registerFromManifest fails and restored/local custom-scheme diff viewers cannot load the bundled worker; update the scheme handler’s extension check alongside the CLI/server change.

Useful? React with 👍 / 👎.

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 current head. The custom-scheme handler now accepts text/javascript for both .mjs and .js paths, so worker-portable.js can load after restore.

— Claude Code

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.

Already covered in the current code: BrowserPanel.pathExtensionMatchesMimeType accepts text/javascript for both .mjs and .js, so worker-portable.js is allowed by the custom-scheme handler.

— Claude Code

files: assetPaths.map { targetDirectory.appendingPathComponent($0, isDirectory: false) }
+ appAssetPaths.map { targetAppDirectory.appendingPathComponent($0, isDirectory: false) }
)
Expand Down Expand Up @@ -5567,7 +5603,7 @@ extension CMUXCLI {

var relativePaths: [String] = []
for case let fileURL as URL in enumerator {
guard fileURL.pathExtension == "mjs",
guard ["js", "mjs"].contains(fileURL.pathExtension),
let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey]),
values.isRegularFile == true else {
continue
Expand Down
125 changes: 125 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,131 @@
}
}
},
"diffViewer.copyFailedGitApplyCommand": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "تعذر نسخ أمر git apply."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Nije moguće kopirati naredbu git apply."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Kunne ikke kopiere git apply-kommandoen."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Der git apply-Befehl konnte nicht kopiert werden."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Could not copy git apply command."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "No se pudo copiar el comando git apply."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Impossible de copier la commande git apply."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Impossibile copiare il comando git apply."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "git apply コマンドをコピーできませんでした。"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "មិនអាចចម្លងពាក្យបញ្ជា git apply បានទេ។"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "git apply 명령을 복사할 수 없습니다."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Kunne ikke kopiere git apply-kommandoen."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Nie udało się skopiować polecenia git apply."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Não foi possível copiar o comando git apply."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Не удалось скопировать команду git apply."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "ไม่สามารถคัดลอกคำสั่ง git apply ได้"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "git apply komutu kopyalanamadı."
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Не вдалося скопіювати команду git apply."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法复制 git apply 命令。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "無法複製 git apply 指令。"
}
}
}
},
"diffViewer.copyGitApplyCommand": {
"extractionState": "manual",
"localizations": {
Expand Down
Loading
Loading