Skip to content
Open
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
139 changes: 139 additions & 0 deletions .github/workflows/cmux-remote-ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: cmux-remote iOS

on:
push:
branches:
- main
paths:
- ".github/workflows/cmux-remote-ios.yml"
- "iOS/cmux-remote/**"
- "CLI/**"
- "Packages/CMUXWorkstream/**"
- "Resources/Localizable.xcstrings"
- "Sources/AppDelegate.swift"
- "Sources/Feed/**"
- "Sources/TerminalController.swift"
- "cmuxTests/FeedCoordinatorTests.swift"
- "cmuxTests/CLIGenericHookPersistenceTests.swift"
pull_request:
paths:
- ".github/workflows/cmux-remote-ios.yml"
- "iOS/cmux-remote/**"
- "CLI/**"
- "Packages/CMUXWorkstream/**"
- "Resources/Localizable.xcstrings"
- "Sources/AppDelegate.swift"
- "Sources/Feed/**"
- "Sources/TerminalController.swift"
- "cmuxTests/FeedCoordinatorTests.swift"
- "cmuxTests/CLIGenericHookPersistenceTests.swift"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
runs-on: warp-macos-26-arm64-6x
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
if [ -z "$XCODE_APP" ]; then
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
XCODE_DIR="$XCODE_APP/Contents/Developer"
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-path

- name: Lint checked-in metadata
run: |
set -euo pipefail
python3 -m json.tool Resources/Localizable.xcstrings >/dev/null
python3 -m json.tool iOS/cmux-remote/App/Resources/Localizable.xcstrings >/dev/null
plutil -lint \
iOS/cmux-remote/App/Configuration/Info.plist \
iOS/cmux-remote/App/Configuration/CmuxRemoteWidgets-Info.plist \
iOS/cmux-remote/App/Resources/PrivacyInfo.xcprivacy
if rg -n \
"processingIdentifier|scheduleProcessing|BGProcessing|com.cmuxterm.remote.processing|NSSupportsLiveActivitiesFrequentUpdates|processing</string>|BGProcessingTask|pushTokenHex|pushTokenUpdates|pushType: \\.token" \
iOS/cmux-remote; then
echo "Found stale BGProcessing/frequent-update/APNs-token declarations." >&2
exit 1
fi

- name: Cache Swift packages
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: iOS/cmux-remote/.ci-source-packages
key: cmux-remote-spm-${{ runner.os }}-${{ hashFiles('iOS/cmux-remote/cmux-remote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: cmux-remote-spm-${{ runner.os }}-

- name: Resolve Swift packages
working-directory: iOS/cmux-remote
run: |
set -euo pipefail
mkdir -p .ci-source-packages
for attempt in 1 2 3; do
if xcodebuild -project cmux-remote.xcodeproj -scheme CmuxRemote -configuration Debug \
-clonedSourcePackagesDirPath "$PWD/.ci-source-packages" \
-resolvePackageDependencies; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to resolve Swift packages after 3 attempts" >&2
exit 1
fi
echo "Package resolution failed on attempt $attempt, retrying..."
sleep $((attempt * 5))
done

- name: Build iOS app
working-directory: iOS/cmux-remote
run: |
set -euo pipefail
xcodebuild -quiet \
-project cmux-remote.xcodeproj \
-scheme CmuxRemote \
-configuration Debug \
-destination "generic/platform=iOS" \
-clonedSourcePackagesDirPath "$PWD/.ci-source-packages" \
-disableAutomaticPackageResolution \
-derivedDataPath "$RUNNER_TEMP/cmux-remote-ios-app" \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
build

- name: Build iOS test bundle
working-directory: iOS/cmux-remote
run: |
set -euo pipefail
xcodebuild -quiet \
-project cmux-remote.xcodeproj \
-scheme CmuxRemote \
-configuration Debug \
-destination "generic/platform=iOS" \
-clonedSourcePackagesDirPath "$PWD/.ci-source-packages" \
-disableAutomaticPackageResolution \
-derivedDataPath "$RUNNER_TEMP/cmux-remote-ios-tests" \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
build-for-testing
2 changes: 1 addition & 1 deletion CLI/CMUXCLI+AgentHookDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ extension CMUXCLI {
.init(agentEvent: "UserPromptSubmit", cmuxSubcommand: "prompt-submit"),
.init(agentEvent: "Stop", cmuxSubcommand: "stop"),
],
feedHookEvents: ["PreToolUse", "PermissionRequest"],
feedHookEvents: ["PreToolUse", "PermissionRequest", "DiffApprovalRequest"],
postInstallAction: .codexConfigToml
),
AgentHookDef(
Expand Down
34 changes: 23 additions & 11 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27674,7 +27674,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
_ = try client.sendV2(
method: "feed.permission.reply",
params: ["request_id": requestId, "mode": mode]
params: ["request_id": requestId, "item_id": item.id, "mode": mode]
)
return "Permission \(mode) sent"
case "exitPlan":
Expand All @@ -27683,7 +27683,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
guard !feedback.isEmpty else { return "Replan cancelled" }
_ = try client.sendV2(
method: "feed.exit_plan.reply",
params: ["request_id": requestId, "mode": "deny", "feedback": feedback]
params: ["request_id": requestId, "item_id": item.id, "mode": "deny", "feedback": feedback]
)
return "Replan feedback sent"
}
Expand All @@ -27707,7 +27707,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
_ = try client.sendV2(
method: "feed.exit_plan.reply",
params: ["request_id": requestId, "mode": mode]
params: ["request_id": requestId, "item_id": item.id, "mode": mode]
)
return "Plan \(mode) sent"
case "question":
Expand All @@ -27720,7 +27720,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
_ = try client.sendV2(
method: "feed.question.reply",
params: ["request_id": requestId, "selections": [] as [String]]
params: ["request_id": requestId, "item_id": item.id, "selections": [] as [String]]
)
return "Question answer sent"
}
Expand All @@ -27731,7 +27731,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
_ = try client.sendV2(
method: "feed.question.reply",
params: ["request_id": requestId, "selections": selections]
params: ["request_id": requestId, "item_id": item.id, "selections": selections]
)
return "Question answer sent"
}
Expand All @@ -27758,7 +27758,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
.map(\.label)
_ = try client.sendV2(
method: "feed.question.reply",
params: ["request_id": requestId, "selections": selections]
params: ["request_id": requestId, "item_id": item.id, "selections": selections]
)
selectedQuestionOptions.removeValue(forKey: requestId)
return selections.isEmpty ? "Question answer sent with no selections" : "Question answer sent"
Expand All @@ -27781,7 +27781,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
_ = try client.sendV2(
method: "feed.question.reply",
params: ["request_id": requestId, "selections": [option.label]]
params: ["request_id": requestId, "item_id": item.id, "selections": [option.label]]
)
return "Question answer sent: \(option.label)"
default:
Expand Down Expand Up @@ -28364,6 +28364,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
event: String,
toolName: String
) -> (String, Bool) {
if event == "DiffApprovalRequest" || toolName == "DiffApprovalRequest" {
return ("DiffApprovalRequest", true)
}

if source == "claude" {
switch event {
case "PermissionRequest":
Expand Down Expand Up @@ -28514,7 +28518,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
behavior: String,
message: String? = nil,
updatedInput: [String: Any]? = nil,
updatedPermissions: [[String: Any]]? = nil
updatedPermissions: [[String: Any]]? = nil,
outputHookEventName: String = "PermissionRequest"
) -> [String: Any] {
var inner: [String: Any] = ["behavior": behavior]
if behavior == "deny" {
Expand All @@ -28528,7 +28533,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
return [
"hookSpecificOutput": [
"hookEventName": "PermissionRequest",
"hookEventName": outputHookEventName,
"decision": inner,
]
]
Expand Down Expand Up @@ -28597,13 +28602,20 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
))
}
if source == "codex" {
let outputHookEventName = hookEventName == "DiffApprovalRequest"
? "DiffApprovalRequest"
: "PermissionRequest"
if mode == "deny" {
return encode(permissionRequestHookDecision(
behavior: "deny",
message: "User denied permission via cmux Feed."
message: "User denied permission via cmux Feed.",
outputHookEventName: outputHookEventName
))
}
return encode(permissionRequestHookDecision(behavior: "allow"))
return encode(permissionRequestHookDecision(
behavior: "allow",
outputHookEventName: outputHookEventName
))
}
if source == "hermes-agent" {
if mode == "deny" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public struct WorkstreamEvent: Codable, Sendable, Equatable {
case preToolUse = "PreToolUse"
case postToolUse = "PostToolUse"
case permissionRequest = "PermissionRequest"
case diffApprovalRequest = "DiffApprovalRequest"
case askUserQuestion = "AskUserQuestion"
case exitPlanMode = "ExitPlanMode"
case todoWrite = "TodoWrite"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,14 @@ public final class WorkstreamStore {
) -> (WorkstreamKind, WorkstreamPayload) {
let toolInput = event.toolInputJSON ?? "{}"
switch event.hookEventName {
case .permissionRequest:
case .permissionRequest, .diffApprovalRequest:
return (
.permissionRequest,
.permissionRequest(
requestId: event.requestId ?? event.sessionId,
toolName: event.toolName ?? "unknown",
toolName: event.hookEventName == .diffApprovalRequest
? event.hookEventName.rawValue
: (event.toolName ?? "unknown"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
toolInputJSON: toolInput,
pattern: nil
)
Expand Down
Loading