Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 39 additions & 20 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,19 +317,48 @@ jobs:
if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true')
run: |
set -euo pipefail
# Build with --asset-suffix so manifest download URLs point to
# immutable, build-specific asset names (e.g. cmuxd-remote-darwin-arm64-2362248028801).
# This prevents checksum mismatches when a newer nightly overwrites
# the shared "latest" assets on the release.
./scripts/build_remote_daemon_release_assets.sh \
--version "$NIGHTLY_REMOTE_DAEMON_VERSION" \
--release-tag "nightly" \
--repo "manaflow-ai/cmux" \
--output-dir "remote-daemon-assets"
MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' remote-daemon-assets/cmuxd-remote-manifest.json)"
--output-dir "remote-daemon-assets" \
--asset-suffix "$NIGHTLY_BUILD"
MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json")"
APP_PLIST="build-universal/Build/Products/Release/cmux NIGHTLY.app/Contents/Info.plist"
if [ ! -f "$APP_PLIST" ]; then
echo "Missing nightly app Info.plist at $APP_PLIST" >&2
exit 1
fi
plutil -remove CMUXRemoteDaemonManifestJSON "$APP_PLIST" >/dev/null 2>&1 || true
plutil -insert CMUXRemoteDaemonManifestJSON -string "$MANIFEST_JSON" "$APP_PLIST"
# Also create unsuffixed "latest" copies for the release page and
# any tooling that fetches the generic asset names. The manifest's
# downloadURLs still point to the versioned filenames (intentional:
# the live manifest is used by the client-side checksum fallback
# which only reads sha256, not downloadURL). The unsuffixed copies
# are convenience aliases and don't carry build-provenance
# attestation (attested versioned files are canonical).
for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do
cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \
"remote-daemon-assets/cmuxd-remote-${platform}"
done
# Regenerate unsuffixed checksums with generic filenames so
# `shasum -c cmuxd-remote-checksums.txt` works against the aliases.
(
cd remote-daemon-assets
shasum -a 256 \
cmuxd-remote-darwin-arm64 \
cmuxd-remote-darwin-amd64 \
cmuxd-remote-linux-arm64 \
cmuxd-remote-linux-amd64 \
> cmuxd-remote-checksums.txt
)
cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \
"remote-daemon-assets/cmuxd-remote-manifest.json"
Comment on lines +360 to +361

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unsuffixed manifest's downloadURL points to versioned assets

The unsuffixed cmuxd-remote-manifest.json is a verbatim copy of the suffixed manifest, so every downloadURL inside it references the versioned asset name (e.g. cmuxd-remote-darwin-arm64-${NIGHTLY_BUILD}), not the generic cmuxd-remote-darwin-arm64.

The Swift fallback in fetchRemoteManifestLocked is unaffected — it only reads the sha256 field from the live manifest, never downloadURL. However, any external tooling or users who fetch cmuxd-remote-manifest.json and follow its downloadURL fields to find the "latest" binary will be silently redirected to a versioned filename on every build. This may be intentional, but it's worth documenting in the comment on line 338 so future maintainers don't try to "fix" it.

Comment on lines 324 to +361

@coderabbitai coderabbitai Bot Mar 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't suffix remote-daemon URLs for non-publishing builds.

This block still runs on branch builds, but those runs only upload workflow artifacts. Since scripts/build_remote_daemon_release_assets.sh bakes --asset-suffix into the manifest's assetName and downloadURL, the app in a branch artifact will try to fetch cmuxd-remote-…-$NIGHTLY_BUILD from the nightly release even though that upload never happens. That turns cmux ssh into a 404 path, and the new fallback here won't help because it only re-validates checksum mismatches. Lines 511-516 need the same publish-only split so attestation matches whichever naming mode is used.

Possible fix
-          ./scripts/build_remote_daemon_release_assets.sh \
-            --version "$NIGHTLY_REMOTE_DAEMON_VERSION" \
-            --release-tag "nightly" \
-            --repo "manaflow-ai/cmux" \
-            --output-dir "remote-daemon-assets" \
-            --asset-suffix "$NIGHTLY_BUILD"
-          MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json")"
+          BUILD_REMOTE_DAEMON_ARGS=(
+            --version "$NIGHTLY_REMOTE_DAEMON_VERSION"
+            --release-tag "nightly"
+            --repo "manaflow-ai/cmux"
+            --output-dir "remote-daemon-assets"
+          )
+          MANIFEST_PATH="remote-daemon-assets/cmuxd-remote-manifest.json"
+          if [ "${{ needs.decide.outputs.should_publish }}" = "true" ]; then
+            BUILD_REMOTE_DAEMON_ARGS+=(--asset-suffix "$NIGHTLY_BUILD")
+            MANIFEST_PATH="remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json"
+          fi
+          ./scripts/build_remote_daemon_release_assets.sh "${BUILD_REMOTE_DAEMON_ARGS[@]}"
+          MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "$MANIFEST_PATH")"
@@
-          for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do
-            cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \
-               "remote-daemon-assets/cmuxd-remote-${platform}"
-          done
-          (
-            cd remote-daemon-assets
-            shasum -a 256 \
-              cmuxd-remote-darwin-arm64 \
-              cmuxd-remote-darwin-amd64 \
-              cmuxd-remote-linux-arm64 \
-              cmuxd-remote-linux-amd64 \
-              > cmuxd-remote-checksums.txt
-          )
-          cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \
-             "remote-daemon-assets/cmuxd-remote-manifest.json"
+          if [ "${{ needs.decide.outputs.should_publish }}" = "true" ]; then
+            for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do
+              cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \
+                 "remote-daemon-assets/cmuxd-remote-${platform}"
+            done
+            (
+              cd remote-daemon-assets
+              shasum -a 256 \
+                cmuxd-remote-darwin-arm64 \
+                cmuxd-remote-darwin-amd64 \
+                cmuxd-remote-linux-arm64 \
+                cmuxd-remote-linux-amd64 \
+                > cmuxd-remote-checksums.txt
+            )
+            cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \
+               "remote-daemon-assets/cmuxd-remote-manifest.json"
+          fi

Also applies to: 511-516

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/nightly.yml around lines 324 - 361, The workflow is always
inserting the $NIGHTLY_BUILD suffix into the manifest via
scripts/build_remote_daemon_release_assets.sh and then creating suffixed
filenames (cmuxd-remote-…-${NIGHTLY_BUILD}) but the upload only happens for
publish runs, causing branch builds to reference non-existent suffixed assets;
wrap the suffixing/aliasing steps in a publish-only conditional: only pass
--asset-suffix to scripts/build_remote_daemon_release_assets.sh and only run the
for-loop that copies cmuxd-remote-${platform}-${NIGHTLY_BUILD} to unsuffixed
names, the shasum regeneration block, and the cp of
cmuxd-remote-manifest-${NIGHTLY_BUILD}.json to cmuxd-remote-manifest.json when a
PUBLISH/RELEASE flag is true (otherwise build without --asset-suffix so manifest
downloadURL/assetName are unsuffixed); apply the same guard to the other
identical block that manipulates the cmuxd-remote-* files referenced in the
review (the block around the alternate lines 511-516).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not actionable. Branch builds only upload to workflow artifacts for CI validation, never installed by users or distributed via Sparkle. The manifest URLs don't matter for those artifacts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@lawrencecchen, understood — since branch artifacts are purely for CI validation and are never distributed to or installed by real users, the suffixed manifest URLs in those builds have no practical impact. Fair point, I'll drop the concern.


🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2225
File: Sources/Workspace.swift:4149-4151
Timestamp: 2026-03-27T00:15:38.194Z
Learning: Repo: manaflow-ai/cmux — WorkspaceRemoteDaemonManifest.releaseURL is guaranteed to be the GitHub asset download base "https://github.com/{owner}/{repo}/releases/download/{tag}" (not the tag page). Therefore, Sources/Workspace.swift should (and does) build the live-manifest URL as releaseURL + "/cmuxd-remote-manifest.json". Do not suggest deriving it from checksumsURL or entry.downloadURL; test fixtures that reference "/releases/tag/..." are from a different context and shouldn’t drive code changes.

Learnt from: outoftime
Repo: manaflow-ai/cmux PR: 1528
File: Resources/shell-integration/fish/vendor_conf.d/cmux-fish-integration.fish:541-546
Timestamp: 2026-03-17T13:59:10.665Z
Learning: Repo: manaflow-ai/cmux — The socket command `report_git_branch` (parsed in Sources/TerminalController.swift) expects the branch name as an **unquoted, bare token**. Wrapping the branch name in double quotes causes it to be silently discarded by the parser. This matches the bash/zsh shell integration convention. Do not suggest quoting the branch argument for this command; the fix was attempted in commit 829437c7 and immediately reverted because it broke branch reporting.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 1926
File: scripts/build-sign-upload.sh:8-10
Timestamp: 2026-03-22T00:14:23.473Z
Learning: Repo: manaflow-ai/cmux — `scripts/lib/cmux-paths.sh` `cmux_paths_init()` intentionally preserves any pre-set `CMUX_*` environment variable overrides verbatim (does not canonicalize relative paths to absolute). This is a deliberate Stage 1 design choice; do not flag relative-override canonicalization as a bug unless a concrete reproducer is provided or a later stage explicitly tightens the override-semantics contract.


- name: Import signing cert
if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true')
Expand Down Expand Up @@ -479,12 +508,12 @@ jobs:
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
remote-daemon-assets/cmuxd-remote-darwin-arm64
remote-daemon-assets/cmuxd-remote-darwin-amd64
remote-daemon-assets/cmuxd-remote-linux-arm64
remote-daemon-assets/cmuxd-remote-linux-amd64
remote-daemon-assets/cmuxd-remote-checksums.txt
remote-daemon-assets/cmuxd-remote-manifest.json
remote-daemon-assets/cmuxd-remote-darwin-arm64-${{ env.NIGHTLY_BUILD }}
remote-daemon-assets/cmuxd-remote-darwin-amd64-${{ env.NIGHTLY_BUILD }}
remote-daemon-assets/cmuxd-remote-linux-arm64-${{ env.NIGHTLY_BUILD }}
remote-daemon-assets/cmuxd-remote-linux-amd64-${{ env.NIGHTLY_BUILD }}
remote-daemon-assets/cmuxd-remote-checksums-${{ env.NIGHTLY_BUILD }}.txt
remote-daemon-assets/cmuxd-remote-manifest-${{ env.NIGHTLY_BUILD }}.json
Comment on lines 510 to +516

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unsuffixed "latest" copies lack build-provenance attestation

The attest-build-provenance step was updated to attest only the suffixed files. The unsuffixed copies created by the cp loop (e.g. cmuxd-remote-darwin-arm64, cmuxd-remote-checksums.txt, cmuxd-remote-manifest.json) are uploaded to the release but carry no attestation, so gh attestation verify will fail for those assets.

If build provenance isn't required for the "latest" aliases (and users who care about provenance are expected to use the versioned filenames), this is fine — just worth an explicit comment so it doesn't look like an oversight. If provenance on the generic names is desired, the unsuffixed copies should be added to subject-path here as well.


- name: Upload branch nightly artifacts
if: needs.decide.outputs.should_publish != 'true'
Expand All @@ -494,12 +523,7 @@ jobs:
path: |
cmux-nightly-macos*.dmg
appcast.xml
remote-daemon-assets/cmuxd-remote-darwin-arm64
remote-daemon-assets/cmuxd-remote-darwin-amd64
remote-daemon-assets/cmuxd-remote-linux-arm64
remote-daemon-assets/cmuxd-remote-linux-amd64
remote-daemon-assets/cmuxd-remote-checksums.txt
remote-daemon-assets/cmuxd-remote-manifest.json
remote-daemon-assets/cmuxd-remote-*
appcast-universal.xml
if-no-files-found: error

Expand Down Expand Up @@ -533,12 +557,7 @@ jobs:
cmux-nightly-macos-${{ github.run_id }}*.dmg
cmux-nightly-macos.dmg
appcast.xml
remote-daemon-assets/cmuxd-remote-darwin-arm64
remote-daemon-assets/cmuxd-remote-darwin-amd64
remote-daemon-assets/cmuxd-remote-linux-arm64
remote-daemon-assets/cmuxd-remote-linux-amd64
remote-daemon-assets/cmuxd-remote-checksums.txt
remote-daemon-assets/cmuxd-remote-manifest.json
remote-daemon-assets/cmuxd-remote-*
appcast-universal.xml
overwrite_files: true

Expand Down
45 changes: 39 additions & 6 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4146,7 +4146,29 @@ final class WorkspaceRemoteSessionController {
.appendingPathComponent("cmuxd-remote", isDirectory: false)
}

private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String) throws -> URL {
/// Fetch the live manifest JSON from the release, returning nil on any failure.
private static func fetchRemoteManifestLocked(releaseURL: String, version: String) -> WorkspaceRemoteDaemonManifest? {
guard let manifestURL = URL(string: "\(releaseURL)/cmuxd-remote-manifest.json") else { return nil }
Comment on lines +4149 to +4151

@coderabbitai coderabbitai Bot Mar 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 'releaseURL|checksumsURL|downloadURL|cmuxd-remote-manifest\.json|releases/(tag|download)' \
  -g '!**/.build/**' \
  -g '!**/DerivedData/**' \
  -g '!**/node_modules/**'

Repository: manaflow-ai/cmux

Length of output: 29894


Derive the live-manifest URL from a confirmed asset URL.

The manifest builder correctly sets releaseURL to the asset download base (https://github.com/{repo}/releases/download/{tag}), but the test data in cmuxTests/GhosttyConfigTests.swift incorrectly uses the release page URL (/releases/tag/...). Using checksumsURL or entry.downloadURL as the seed and replacing only the filename is more robust and avoids ambiguity:

-    guard let manifestURL = URL(string: "\(releaseURL)/cmuxd-remote-manifest.json") else { return nil }
+    guard let assetURL = URL(string: releaseURL) else { return nil }
+    let manifestURL = assetURL.deletingLastPathComponent()
+        .appendingPathComponent("cmuxd-remote-manifest.json")

At the call site, pass checksumsURL (a confirmed asset URL) instead of releaseURL:

-            let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion, releaseURL: manifest.releaseURL)
+            let downloadedURL = try downloadRemoteDaemonBinaryLocked(
+                entry: entry,
+                version: manifest.appVersion,
+                releaseURL: manifest.checksumsURL
+            )

Also applies to: 4171, 4218–4225, 4265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 4149 - 4151, The code currently builds
the live manifest URL from releaseURL (the release page) in
fetchRemoteManifestLocked; instead derive the manifest URL from a confirmed
asset URL (like checksumsURL or entry.downloadURL) by replacing only the final
path component with "cmuxd-remote-manifest.json" so you always start from an
actual download base (https://github.com/{repo}/releases/download/{tag}) rather
than the release page; update calls that pass releaseURL to instead pass
checksumsURL (or entry.downloadURL) and adjust the logic in
fetchRemoteManifestLocked (and the other similar call sites referenced) to
construct the manifest URL by dropping the last path component of the asset URL
and appending "cmuxd-remote-manifest.json".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not a real issue. releaseURL in the manifest is always https://github.com/{repo}/releases/download/{tag} (the asset download base), not the release page URL. The code constructs releaseURL + "/cmuxd-remote-manifest.json" which correctly resolves to https://github.com/manaflow-ai/cmux/releases/download/nightly/cmuxd-remote-manifest.json. The test data CodeRabbit flagged is in a different context.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@lawrencecchen Thanks for the clarification—agreed. In this repo the manifest’s releaseURL is always the asset download base (…/releases/download/{tag}), so constructing releaseURL + "/cmuxd-remote-manifest.json" in fetchRemoteManifestLocked is correct. I’ve retracted the earlier suggestion; no changes needed here.


✏️ Learnings added
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2225
File: Sources/Workspace.swift:4149-4151
Timestamp: 2026-03-27T00:15:38.194Z
Learning: Repo: manaflow-ai/cmux — WorkspaceRemoteDaemonManifest.releaseURL is guaranteed to be the GitHub asset download base "https://github.com/{owner}/{repo}/releases/download/{tag}" (not the tag page). Therefore, Sources/Workspace.swift should (and does) build the live-manifest URL as releaseURL + "/cmuxd-remote-manifest.json". Do not suggest deriving it from checksumsURL or entry.downloadURL; test fixtures that reference "/releases/tag/..." are from a different context and shouldn’t drive code changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: outoftime
Repo: manaflow-ai/cmux PR: 1528
File: Resources/shell-integration/fish/vendor_conf.d/cmux-fish-integration.fish:541-546
Timestamp: 2026-03-17T13:59:10.665Z
Learning: Repo: manaflow-ai/cmux — The socket command `report_git_branch` (parsed in Sources/TerminalController.swift) expects the branch name as an **unquoted, bare token**. Wrapping the branch name in double quotes causes it to be silently discarded by the parser. This matches the bash/zsh shell integration convention. Do not suggest quoting the branch argument for this command; the fix was attempted in commit 829437c7 and immediately reverted because it broke branch reporting.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 1926
File: scripts/build-sign-upload.sh:8-10
Timestamp: 2026-03-22T00:14:23.473Z
Learning: Repo: manaflow-ai/cmux — `scripts/lib/cmux-paths.sh` `cmux_paths_init()` intentionally preserves any pre-set `CMUX_*` environment variable overrides verbatim (does not canonicalize relative paths to absolute). This is a deliberate Stage 1 design choice; do not flag relative-override canonicalization as a bug unless a concrete reproducer is provided or a later stage explicitly tightens the override-semantics contract.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/TerminalController.swift:3180-3193
Timestamp: 2026-03-09T02:08:14.574Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceClearTags(params:) must only clear all tags when the "source" key is absent. If "source" is present but blank or non-string (v2String(...) returns nil), the API should return invalid_params. Current implementation uses hasSourceKey = params.keys.contains("source") and guards with if hasSourceKey && source == nil { return .err(...)}.

Learnt from: arieltobiana
Repo: manaflow-ai/cmux PR: 1873
File: Sources/TerminalController.swift:4071-4087
Timestamp: 2026-03-20T17:18:30.333Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceAction(params:) -> case "set_color": palette names are resolved via WorkspaceTabColorSettings.defaultPaletteWithOverrides(), whose entries are always valid hex (validated by the UI). Therefore, additional normalization of entry.hex is unnecessary.

Learnt from: MaTriXy
Repo: manaflow-ai/cmux PR: 1460
File: Sources/TerminalController.swift:4012-4023
Timestamp: 2026-03-16T08:05:21.899Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2SurfaceSplitSized(params:) must validate "ratio" as follows: if the "ratio" key is present, it must be numeric (NSNumber/Double-coercible) and strictly 0 < ratio < 1, otherwise return invalid_params; when "ratio" is absent, use default 0.6. This mirrors the general pattern that a present-but-invalid param should yield invalid_params rather than falling back.

Learnt from: outoftime
Repo: manaflow-ai/cmux PR: 1528
File: Sources/GhosttyTerminalView.swift:3220-3228
Timestamp: 2026-03-17T18:25:33.286Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift within TerminalSurface.createSurface(for:), when constructing XDG_DATA_DIRS for Fish vendor_conf.d auto-sourcing, treat empty or whitespace-only values from initialEnvironmentOverrides, env, getenv, and ProcessInfo as unset before prefixing the integrationDir. This avoids producing a trailing colon. Keep XDG_DATA_DIRS in protectedStartupEnvironmentKeys so initialEnvironmentOverrides cannot overwrite the prefixed value.

Learnt from: homanp
Repo: manaflow-ai/cmux PR: 883
File: CLI/cmux.swift:1569-1586
Timestamp: 2026-03-04T22:05:07.913Z
Learning: In manaflow-ai/cmux CLI (CLI/cmux.swift), for parity with existing browser commands, markdown.open intentionally normalizes --surface before resolving --workspace/--window, relying on server-side resolution to disambiguate final routing. Avoid one-off reordering in markdown; consider any change only as a coordinated, cross-command refactor.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:12:42.553Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/BrowserPanel.swift, BrowserPanel.updateWorkspaceId(_:) is a dedicated method (var workspaceId) that updates both BrowserPanel.workspaceId and pickerMessageHandler?.updateWorkspaceId(_:) atomically. It is called from BrowserPanel.reattachToWorkspace(_:) when a panel moves between workspaces. This ensures BrowserPickerMessageHandler always posts notifications with the current workspaceId, not a stale one from panel initialization.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2034
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-25T07:14:56.211Z
Learning: Repo: manaflow-ai/cmux — Sources/AppDelegate.swift — Pattern for “Open Folder”: In AppDelegate.showOpenFolderPanel(), seed NSOpenPanel.directoryURL using preferredMainWindowContextForWorkspaceCreation(debugSource: …) rather than NSApp.keyWindow, and on selection delegate to openWorkspaceForExternalDirectory(workingDirectory:…, debugSource: …). Rationale: handles auxiliary-key-window cases, ensures shouldBringToFront = true, and unifies menu/shortcut behavior with a consistent fallback to createMainWindow when workspace creation returns nil.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: Sources/TerminalController.swift:3607-3645
Timestamp: 2026-03-25T00:32:48.115Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceSetColor(params:) must validate the provided color with WorkspaceTabColorSettings.normalizedHex. If the color parameter is present but invalid, return invalid_params (no fallback). On success, apply the color and return the normalized (uppercase) `#RRGGBB` in the response payload.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:01:33.134Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, sendPickedElementToTerminal(workspaceId:summary:) resolves the target workspace via AppDelegate.shared?.workspaceFor(tabId: workspaceId), which searches across all mainWindowContexts (not self.tabManager which is active-window only). It first injects text into the focused terminal panel if it's a terminal, then falls back to iterating all panels in the workspace.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 1909
File: Sources/ContentView.swift:2156-2171
Timestamp: 2026-03-23T06:08:14.740Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, openFileInTextEditor(_:) must attempt workspace.newTextEditorSplit(from:orientation:filePath:focus:) and, if that returns nil, fall back to workspace.newTextEditorSurface(inPane:filePath:focus:) using bonsplitController.focusedPaneId. Rationale: avoid dropping file-open requests when the focused panel is not pane-backed.

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

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: CLI/cmux.swift:1948-1978
Timestamp: 2026-03-25T00:33:26.452Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, the set-workspace-color command requires exactly one trailing <hex> argument and enforces a strict 6-digit hex format (`#RRGGBB`, optional leading '#'); clear-workspace-color rejects any unexpected positional args beyond --workspace. This matches server-side normalization and prevents malformed inputs.

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

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

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

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

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

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

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

let request = NSMutableURLRequest(url: manifestURL)
request.timeoutInterval = 15
request.setValue("cmux/\(version)", forHTTPHeaderField: "User-Agent")
let session = URLSession(configuration: .ephemeral)
let semaphore = DispatchSemaphore(value: 0)
var resultData: Data?
session.dataTask(with: request as URLRequest) { data, response, error in
defer { semaphore.signal() }
guard error == nil,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else { return }
resultData = data
}.resume()
_ = semaphore.wait(timeout: .now() + 20.0)
session.finishTasksAndInvalidate()
guard let data = resultData else { return nil }
return try? JSONDecoder().decode(WorkspaceRemoteDaemonManifest.self, from: data)
}

private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String, releaseURL: String? = nil) throws -> URL {
guard let url = URL(string: entry.downloadURL) else {
throw NSError(domain: "cmux.remote.daemon", code: 25, userInfo: [
NSLocalizedDescriptionKey: "remote daemon manifest has an invalid download URL",
Expand Down Expand Up @@ -4193,10 +4215,21 @@ final class WorkspaceRemoteSessionController {
}

let downloadedSHA = try Self.sha256Hex(forFile: downloadedURL)
guard downloadedSHA == entry.sha256.lowercased() else {
throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [
NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)",
])
if downloadedSHA != entry.sha256.lowercased() {
// The embedded manifest's checksum doesn't match the downloaded binary.
// This can happen when a newer nightly overwrites the shared release
// asset after this build's manifest was embedded. As a fallback, fetch
// the live manifest from the release and verify against that.
if let releaseURL,
let liveManifest = Self.fetchRemoteManifestLocked(releaseURL: releaseURL, version: version),
let liveEntry = liveManifest.entry(goOS: entry.goOS, goArch: entry.goArch),
downloadedSHA == liveEntry.sha256.lowercased() {
debugLog("remote.download.checksum-fallback: embedded manifest checksum stale, live manifest matched for \(entry.assetName)")
} else {
throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [
NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)",
])
}
}

let tempURL = cacheURL.deletingLastPathComponent()
Expand Down Expand Up @@ -4229,7 +4262,7 @@ final class WorkspaceRemoteSessionController {
}
try? FileManager.default.removeItem(at: cacheURL)
}
let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion)
let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion, releaseURL: manifest.releaseURL)
debugLog("remote.build.downloaded path=\(downloadedURL.path)")
return downloadedURL
}
Expand Down
35 changes: 27 additions & 8 deletions scripts/build_remote_daemon_release_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ Usage: scripts/build_remote_daemon_release_assets.sh \
--version <app-version> \
--release-tag <tag> \
--repo <owner/repo> \
--output-dir <dir>
--output-dir <dir> \
[--asset-suffix <suffix>]

Builds cmuxd-remote release assets for the supported remote platforms and emits:
cmuxd-remote-<goos>-<goarch>
cmuxd-remote-checksums.txt
cmuxd-remote-manifest.json
cmuxd-remote-<goos>-<goarch>[-<suffix>]
cmuxd-remote-checksums[-<suffix>].txt
cmuxd-remote-manifest[-<suffix>].json

When --asset-suffix is provided, all output filenames and manifest download URLs
include the suffix, making each build's assets immutable (used by nightly builds
to avoid checksum mismatches when assets are overwritten by later builds).
EOF
}

VERSION=""
RELEASE_TAG=""
REPO=""
OUTPUT_DIR=""
ASSET_SUFFIX=""

while [[ $# -gt 0 ]]; do
case "$1" in
Expand All @@ -39,6 +45,10 @@ while [[ $# -gt 0 ]]; do
OUTPUT_DIR="${2:-}"
shift 2
;;
--asset-suffix)
ASSET_SUFFIX="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
Expand Down Expand Up @@ -77,9 +87,14 @@ DAEMON_GO_BUILD_ARGS=(
-ldflags "$DAEMON_GO_LDFLAGS"
)

CHECKSUMS_ASSET_NAME="cmuxd-remote-checksums.txt"
SUFFIX_TAG=""
if [[ -n "$ASSET_SUFFIX" ]]; then
SUFFIX_TAG="-${ASSET_SUFFIX}"
fi

CHECKSUMS_ASSET_NAME="cmuxd-remote-checksums${SUFFIX_TAG}.txt"
CHECKSUMS_PATH="${OUTPUT_DIR}/${CHECKSUMS_ASSET_NAME}"
MANIFEST_PATH="${OUTPUT_DIR}/cmuxd-remote-manifest.json"
MANIFEST_PATH="${OUTPUT_DIR}/cmuxd-remote-manifest${SUFFIX_TAG}.json"

TARGETS=(
"darwin arm64"
Expand All @@ -95,18 +110,22 @@ trap 'rm -f "$ENTRIES_FILE"' EXIT

for target in "${TARGETS[@]}"; do
read -r GOOS GOARCH <<<"$target"
ASSET_NAME="cmuxd-remote-${GOOS}-${GOARCH}"
ASSET_NAME="cmuxd-remote-${GOOS}-${GOARCH}${SUFFIX_TAG}"
OUTPUT_PATH="${OUTPUT_DIR}/${ASSET_NAME}"

# Build into a temp path first, then rename (the binary content is the same
# regardless of suffix, so we build once and move).
BUILD_PATH="${OUTPUT_DIR}/cmuxd-remote-${GOOS}-${GOARCH}.build"
(
cd "$DAEMON_ROOT"
GOOS="$GOOS" \
GOARCH="$GOARCH" \
CGO_ENABLED=0 \
go "${DAEMON_GO_BUILD_ARGS[@]}" \
-o "$OUTPUT_PATH" \
-o "$BUILD_PATH" \
./cmd/cmuxd-remote
)
mv "$BUILD_PATH" "$OUTPUT_PATH"
chmod 755 "$OUTPUT_PATH"

SHA256="$(shasum -a 256 "$OUTPUT_PATH" | awk '{print $1}')"
Expand Down
43 changes: 43 additions & 0 deletions tests/test_remote_daemon_release_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,46 @@ for entry in manifest["entries"]:

print("PASS: remote daemon release assets include all targets and manifest entries")
PY

# ------------------------------------------------------------------
# Test with --asset-suffix (nightly-style immutable asset names)
# ------------------------------------------------------------------
SUFFIX_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cmux-remote-assets-suffix-test.XXXXXX")"
trap 'rm -rf "$OUTPUT_DIR" "$SUFFIX_DIR"' EXIT

"$ROOT_DIR/scripts/build_remote_daemon_release_assets.sh" \
--version "0.62.0-nightly.123456" \
--release-tag "nightly" \
--repo "manaflow-ai/cmux" \
--output-dir "$SUFFIX_DIR" \
--asset-suffix "123456" >/dev/null

for asset in \
cmuxd-remote-darwin-arm64-123456 \
cmuxd-remote-darwin-amd64-123456 \
cmuxd-remote-linux-arm64-123456 \
cmuxd-remote-linux-amd64-123456 \
cmuxd-remote-checksums-123456.txt \
cmuxd-remote-manifest-123456.json
do
if [[ ! -f "$SUFFIX_DIR/$asset" ]]; then
echo "FAIL: missing suffixed asset $asset" >&2
exit 1
fi
done

python3 - <<'PY' "$SUFFIX_DIR/cmuxd-remote-manifest-123456.json"
import json
import sys
from pathlib import Path

manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))

for entry in manifest["entries"]:
if not entry["assetName"].endswith("-123456"):
raise SystemExit(f"FAIL: suffixed asset name missing suffix: {entry['assetName']}")
if not entry["downloadURL"].endswith("/" + entry["assetName"]):
raise SystemExit(f"FAIL: downloadURL mismatch for {entry['assetName']}")

print("PASS: --asset-suffix produces correctly suffixed assets and manifest entries")
PY
Loading