Skip to content

feat: add built-in OpenCode status integration#1426

Open
coygeek wants to merge 16 commits intomanaflow-ai:mainfrom
coygeek:feat/opencode-status-integration
Open

feat: add built-in OpenCode status integration#1426
coygeek wants to merge 16 commits intomanaflow-ai:mainfrom
coygeek:feat/opencode-status-integration

Conversation

@coygeek
Copy link
Copy Markdown

@coygeek coygeek commented Mar 14, 2026

Summary

  • add a bundled opencode wrapper that injects a local cmux integration plugin when OpenCode is launched inside cmux
  • map OpenCode session / permission / question / error events to cmux sidebar status pills and notifications
  • extend cmux set-status with --pid so OpenCode statuses participate in stale-process cleanup

Testing

  • python3 tests/test_opencode_wrapper_hooks.py
  • node tests/test_opencode_plugin_runtime.js
  • python3 tests/test_claude_wrapper_hooks.py
  • inside Tart VM: bash -n Resources/bin/opencode && python3 tests/test_opencode_wrapper_hooks.py && node tests/test_opencode_plugin_runtime.js && python3 tests/test_claude_wrapper_hooks.py

Demo Video

  • Video URL or attachment: not recorded

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Related


Summary by cubic

Adds built-in OpenCode status integration with an auto-injected plugin that maps session events to sidebar status and notifications. When work finishes, it shows “Idle” with an attention badge (blue ring) until new work starts. Works automatically when opencode runs inside cmux; set CMUX_OPENCODE_INTEGRATION_DISABLED=1 to opt out.

  • New Features

    • Bundled opencode wrapper injects opencode-cmux-plugin.js when a live cmux socket is detected; falls back to plain opencode on any setup failure.
    • Maps events to status: Running, Retrying, Needs input, Idle (attention on completion), Error; sends notifications for permission, question, and error; clears on resume and on exit.
    • Extends cmux set-status with --pid (validated positive Int32) and adds sidebar commands: clear-status, list-status, set-progress/clear-progress, log/clear-log/list-log, sidebar-state; CLI help updated. Xcode project ships opencode and the plugin; docs add a manual example with osascript fallback and a reference to the bundled plugin.
  • Bug Fixes

    • Stabilizes completion flow: detect busy→idle, add cooldown, keep completion attention until new work; reset completed and waiting text on new work.
    • Suppresses cmux command output via .quiet() to prevent “OK” text from leaking into terminals.

Written for commit f007166. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Full CLI support for sidebar metadata commands (status/progress/log/notifications) with optional PID scoping and improved log message handling.
    • OpenCode wrapper and CMUX integration plugin to maintain per-session status, surface notifications, and inject the plugin when available.
  • Documentation

    • Renamed OpenCode docs and added environment variable aliases (CMUX_WORKSPACE_ID, CMUX_SURFACE_ID) with backward-compat notes.
  • Tests

    • Added runtime and wrapper regression tests for plugin behavior and wrapper injection.
  • Chores

    • Expanded .gitignore and registered new CLI integration resources.

Ship a bundled opencode wrapper that injects a local cmux plugin when OpenCode runs inside a cmux terminal. Translate OpenCode session, permission, question, retry, idle, and error events into sidebar status pills and notifications so OpenCode sessions expose the same working-state cues as Claude Code.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 14, 2026

@coygeek is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

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 a built-in OpenCode cmux integration: a wrapper injects a bundled OpenCode plugin when a live cmux socket is detected; the plugin maps OpenCode events to cmux set-status/notify/clear (PID-scoped), and tests + project packaging include the new artifacts. (≤50 words)

Changes

Cohort / File(s) Summary
CLI
CLI/cmux.swift
Adds explicit parsing/handling for sidebar-metadata v1 socket commands (set-status, clear-status, list-status, set-progress, clear-progress, log, clear-log, list-log, sidebar-state); set-status accepts --pid <pid> validated as positive Int32; log supports -- separator; builds v1 command strings and sends via sendV1Command.
Build / Packaging
GhosttyTabs.xcodeproj/project.pbxproj
Registers two new runtime artifacts (Resources/bin/opencode, Resources/bin/opencode-cmux-plugin.js) and adds them to the Copy CLI build phase.
Wrapper script
Resources/bin/opencode
New shell wrapper that finds real opencode, probes cmux socket, installs a temp OPENCODE_CONFIG_DIR overlay (symlinks existing config, preserves plugin subtrees), copies the cmux plugin into plugins dir, exports OPENCODE_CONFIG_DIR, attempts to clear status/notifications before/after, and cleans up on exit/signals; falls back to real binary if integration unavailable.
Plugin implementation
Resources/bin/opencode-cmux-plugin.js
New CmuxIntegrationPlugin export: maintains per-session state, derives workspace desiredStatus, issues cmux set-status (uses --pid when available), cmux notify, and cmux clear-notifications/clear-status; caches applied payloads and wraps cmux calls in try/catch.
Runtime tests (JS)
tests/test_opencode_plugin_runtime.js
Node test imports the plugin, simulates session/permission/question/error events, captures rendered $ commands, and asserts expected cmux calls (status, notify, clear) and PID inclusion.
Wrapper integration tests (Py)
tests/test_opencode_wrapper_hooks.py
Python regression tests that sandbox the wrapper with fake opencode/cmux binaries to validate plugin injection for live/missing/stale socket states, overlay creation, preservation of existing config, cleanup behavior, and argument passthrough.
Docs & task state
docs/notifications.md, TODO.md
Docs updated for OpenCode (header/example shape + env var aliases CMUX_WORKSPACE_ID/CMUX_SURFACE_ID); TODO marks OpenCode integration complete.
Tests & CI helpers
tests/*
New test artifacts for runtime and wrapper behavior verification added to repo.
Ignore patterns
.gitignore
Expanded ignore list covering editor/OS/build/test/tooling artifacts.
App & portal behavior
Sources/AppDelegate.swift, Sources/TerminalWindowPortal.swift, cmuxTests/TerminalAndGhosttyTests.swift
UI-test launch stabilization logic expanded with retrying activation and diagnostics; TerminalWindowPortal.bind schedules an extra external-geometry synchronize; new lifecycle test added to validate queued layout-shift behavior.
CI workflow
.github/workflows/ci.yml
xcodebuild UI test invocation wrapped to capture output via tee, save exit code, and conditionally retry a flaky display-resolution test when specific failure patterns appear.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Wrapper as OpenCode Wrapper
    participant OC as OpenCode App
    participant Plugin as Cmux Plugin
    participant Cmux as Cmux Socket
    participant Sidebar as Sidebar UI

    User->>Wrapper: run `opencode`
    Wrapper->>Wrapper: probe cmux socket
    alt socket live
        Wrapper->>Wrapper: create temp OPENCODE_CONFIG_DIR (symlink existing config), copy plugin
    else socket missing/stale
        Wrapper->>Wrapper: skip injection
    end
    Wrapper->>OC: exec with OPENCODE_CONFIG_DIR
    OC->>Plugin: emit session.*, permission.*, question.*, error events
    Plugin->>Cmux: cmux set-status / cmux notify / cmux clear-notifications
    Cmux->>Sidebar: update sidebar status & notifications
    OC->>Plugin: emit session.deleted
    Plugin->>Cmux: cmux clear-status
    Wrapper->>Wrapper: cleanup temp config
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped into configs, quiet and fleet,
injected a plugin, made statuses meet,
sessions now chatter, badges gleam bright,
Sidebar wakes up to running light,
Cmux and OpenCode, a nimble delight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The PR includes some changes beyond the core OpenCode integration: updates to CI workflow retry logic, window stability improvements, and portal geometry synchronization are tangential to the main objective. Clarify whether the window stability and CI changes in AppDelegate.swift, TerminalWindowPortal.swift, TerminalAndGhosttyTests.swift, and ci.yml are necessary preconditions for the OpenCode integration or represent separate bug fixes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main change: adding built-in OpenCode status integration to cmux.
Description check ✅ Passed The PR description covers summary, testing, and verification steps; however, the demo video field is empty ('not recorded') and bot review requests appear unchecked.
Linked Issues check ✅ Passed The PR fully addresses all requirements from issue #1425: implements sidebar status pills (Running, Retrying, Needs input, Idle, Error), maps OpenCode events to status updates, provides automatic opt-out integration via bundled wrapper and plugin, and extends cmux set-status with --pid for stale-process cleanup.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/opencode-status-integration

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

❤️ Share

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

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/notifications.md">

<violation number="1" location="docs/notifications.md:124">
P2: The manual plugin example can throw when `cmux` is unavailable because it lacks a fallback path. Add the documented macOS fallback so the event hook remains resilient outside cmux.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread docs/notifications.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
CLI/cmux.swift (1)

1736-1751: Validate --pid as a positive integer before sending.

Right now --pid is accepted as any string and forwarded. Invalid values will silently bypass PID tracking semantics.

Suggested patch
-            let (pid, r3) = parseOption(r2, name: "--pid")
+            let (pidRaw, r3) = parseOption(r2, name: "--pid")
+            let pid: Int?
+            if let pidRaw {
+                guard let parsedPid = Int(pidRaw), parsedPid > 0 else {
+                    throw CLIError(message: "--pid must be a positive integer")
+                }
+                pid = parsedPid
+            } else {
+                pid = nil
+            }
             let (wsFlag, r4) = parseOption(r3, name: "--workspace")
@@
-            if let pid { socketCmd += " --pid=\(socketQuote(pid))" }
+            if let pid { socketCmd += " --pid=\(pid)" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 1736 - 1751, The --pid option is currently
forwarded as any string; update the parsing/validation after let (pid, r3) =
parseOption(r2, name: "--pid") to ensure, when pid is non-nil, it represents a
positive integer (e.g., attempt to convert to Int and check > 0) and throw a
CLIError with a clear message if it’s invalid; after validation, continue using
the validated pid value when appending to socketCmd (the set_status command) so
only a valid PID is included, keeping existing flows for workspace resolution
via resolveWorkspaceId and socketQuote usage for other fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 5257-5258: The top-level usage() output and the set-status
subcommand help are inconsistent because usage() omits the newly added --pid
flag; update the usage() function to include the --pid <pid> description
(matching the subcommand help) so both help surfaces present the same options,
locating the usage() method and the set-status subcommand definition in
CLI/cmux.swift to mirror the exact text "--pid <pid>            Track a process
for stale-pill cleanup".

In `@docs/notifications.md`:
- Around line 117-127: The provided manual fallback only emits session.idle and
misquotes the shell body; update the example so it matches the full integration
behavior (emit Running/Retrying/Needs input/Error mappings) by either replacing
the snippet with the full CmuxNotificationPlugin implementation from
Resources/bin/opencode-cmux-plugin.js or inlining the complete plugin logic
(keep the exported symbol CmuxNotificationPlugin and its event handler), and
ensure shell invocations quote the body (e.g., wrap the --body argument in
quotes) so multi-word messages aren’t split.

In `@Resources/bin/opencode`:
- Around line 73-98: Check that CMUX_OPENCODE_CONFIG_DIR is non-empty and a
valid directory immediately after the mktemp -d call (and before any
overlays/links are created); if mktemp failed, exec the real opencode binary
(use ${OPENCODE_BIN:-opencode}) to bail out rather than continuing. Likewise,
verify mkdir -p "$CMUX_OPENCODE_CONFIG_DIR/plugins" and cp "$PLUGIN_SOURCE" ...
succeed; on any failure restore/cleanup and exec the real binary instead of
proceeding. Keep the existing cleanup() and trap behavior so any created temp
dir is removed on exit, and make these checks around the
CMUX_OPENCODE_CONFIG_DIR, mkdir, and cp steps (reference symbols:
CMUX_OPENCODE_CONFIG_DIR, cleanup(), mkdir, cp, PLUGIN_SOURCE, OPENCODE_BIN).

In `@Resources/bin/opencode-cmux-plugin.js`:
- Around line 90-118: The cleanup helpers currently call global clears and can
erase other sessions; change clearNotifications() and clearStatus() to only
clear items scoped to this process by using the pid used in setStatus()
(parse/store the pid from the applied value or keep a currentPid variable) and
call cmux with the same --pid option (e.g. `cmux clear-notifications --pid
<pid>` and `cmux clear-status opencode --pid <pid>`), and also guard clears so
they run only when the stored pid matches process.pid and attention/applied
correspond to this process; remove any global clear paths in wrapper cleanup
that don't supply or check the pid.

---

Nitpick comments:
In `@CLI/cmux.swift`:
- Around line 1736-1751: The --pid option is currently forwarded as any string;
update the parsing/validation after let (pid, r3) = parseOption(r2, name:
"--pid") to ensure, when pid is non-nil, it represents a positive integer (e.g.,
attempt to convert to Int and check > 0) and throw a CLIError with a clear
message if it’s invalid; after validation, continue using the validated pid
value when appending to socketCmd (the set_status command) so only a valid PID
is included, keeping existing flows for workspace resolution via
resolveWorkspaceId and socketQuote usage for other fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39e7f30b-4f83-411a-9772-43d11ae144a4

📥 Commits

Reviewing files that changed from the base of the PR and between 6c644d9 and b9ebd9a.

📒 Files selected for processing (8)
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/bin/opencode
  • Resources/bin/opencode-cmux-plugin.js
  • TODO.md
  • docs/notifications.md
  • tests/test_opencode_plugin_runtime.js
  • tests/test_opencode_wrapper_hooks.py

Comment thread CLI/cmux.swift
Comment thread docs/notifications.md
Comment thread Resources/bin/opencode Outdated
Comment on lines +90 to +118
async function clearNotifications() {
if (!attention) return
try {
await $`cmux clear-notifications`
attention = false
} catch {}
}

async function setStatus(value, icon, color) {
const pid = typeof process.pid === "number" && process.pid > 0 ? process.pid : 0
const next = `${value}\u0000${icon}\u0000${color}\u0000${pid}`
if (applied === next) return
try {
if (pid > 0) {
await $`cmux set-status opencode ${value} --icon ${icon} --color ${color} --pid ${pid}`
} else {
await $`cmux set-status opencode ${value} --icon ${icon} --color ${color}`
}
applied = next
} catch {}
}

async function clearStatus() {
if (!applied) return
try {
await $`cmux clear-status opencode`
applied = ""
} catch {}
}
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

These cleanup helpers can erase another session’s UI state.

clearNotifications() wipes the whole workspace, and clearStatus() is not tied to the --pid used in setStatus(). That means one OpenCode process resuming or exiting can clear another live OpenCode session’s pill or another agent’s notification in the same workspace. The wrapper cleanup path has the same side effect. Please switch to source/pid-scoped cleanup, or rely on stale-PID cleanup instead of issuing global clears.

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

In `@Resources/bin/opencode-cmux-plugin.js` around lines 90 - 118, The cleanup
helpers currently call global clears and can erase other sessions; change
clearNotifications() and clearStatus() to only clear items scoped to this
process by using the pid used in setStatus() (parse/store the pid from the
applied value or keep a currentPid variable) and call cmux with the same --pid
option (e.g. `cmux clear-notifications --pid <pid>` and `cmux clear-status
opencode --pid <pid>`), and also guard clears so they run only when the stored
pid matches process.pid and attention/applied correspond to this process; remove
any global clear paths in wrapper cleanup that don't supply or check the pid.

- Validate --pid as a positive integer in set-status CLI command
- Add --pid to top-level usage() help to match subcommand help
- Guard mktemp/mkdir/cp failures in opencode wrapper with fallback
- Fix shell quoting and add osascript fallback in docs manual example
- Add reference to bundled plugin for full integration in docs
- Document workspace-global clear limitation in plugin comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coygeek
Copy link
Copy Markdown
Author

coygeek commented Mar 15, 2026

Review feedback addressed in ece7e89

Investigated all bot review comments. Here's what was fixed vs. what was intentionally left as-is.

Fixed

# Source File Finding Fix
1 CodeRabbit CLI/cmux.swift:9560 Top-level usage() omits --pid from set-status Added [--pid <pid>] to match subcommand help
2 CodeRabbit CLI/cmux.swift:1736 --pid accepted as any string, forwarded without validation Parse as Int, reject non-positive values with CLIError. Emit raw integer (no socketQuote) since it's now validated
3 CodeRabbit Resources/bin/opencode:73-98 mktemp -d failure leaves CMUX_OPENCODE_CONFIG_DIR empty; mkdir/cp could target wrong paths Added `
4 cubic + CodeRabbit docs/notifications.md:124 Manual plugin example: --body Session idle shell-splits; no osascript fallback; no pointer to full plugin Quoted the body, added &>/dev/null, added osascript fallback, added sentence pointing to Resources/bin/opencode-cmux-plugin.js for full integration

Not fixed (by design)

# Source File Finding Rationale
5 CodeRabbit opencode-cmux-plugin.js:90-118 clearNotifications() and clearStatus() are workspace-global, not scoped by --pid cmux clear-notifications and cmux clear-status don't support --pid — that would require server-side changes out of scope for this PR. In practice each surface runs one OpenCode instance. Added comments documenting the limitation.
6 CodeRabbit docs/notifications.md:117-127 Manual example only shows session.idle, doesn't match full integration Intentionally minimal — the docs show a starting-point example. Inlining the 260-line plugin would be impractical. Added a sentence pointing to the bundled plugin source for the full integration.

Test results

All three test suites pass after the changes:

  • bash -n Resources/bin/opencode — syntax OK
  • python3 tests/test_opencode_wrapper_hooks.py — PASS
  • node tests/test_opencode_plugin_runtime.js — PASS
  • python3 tests/test_claude_wrapper_hooks.py — PASS (no regressions)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Resources/bin/opencode-cmux-plugin.js (1)

90-98: ⚠️ Potential issue | 🟠 Major

Line 95 and Line 119 still clear workspace-global UI state.

setStatus() is pid-scoped, but clear-notifications and clear-status opencode are not. When this process leaves an attention state or drops its last session, it can erase another live OpenCode process's notification or status in the same workspace. Please keep cleanup pid/source-scoped, or skip the global clears and rely on stale-PID cleanup instead.

Also applies to: 114-122

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

In `@Resources/bin/opencode-cmux-plugin.js` around lines 90 - 98, The current
cleanup commands (in clearNotifications() and the similar cleanup block around
the clear-status / clear-notifications calls) invoke workspace-global cmux
commands and can wipe other processes' UI state; change these to use
pid/source-scoped clears if cmux supports that (e.g., call cmux
clear-notifications and clear-status with the current PID/source flag) by
updating clearNotifications() and the corresponding cleanup section (search for
clearNotifications and any clear-status or clear-notifications invocations), or
if cmux has no PID-scoping option, remove/skip emitting those global clear
commands and rely on the existing stale-PID cleanup path instead so this process
does not clear another live OpenCode instance's status/notifications.
🧹 Nitpick comments (2)
.gitignore (2)

89-91: Redundant .DS_Store patterns.

Lines 90-91 duplicate the existing .DS_Store pattern on line 9. Gitignore patterns are recursive by default, so .DS_Store already matches these files in all directories. The **/.DS_Store and .DS_Store? patterns add no additional coverage.

♻️ Suggested cleanup

Remove the redundant patterns:

 # Os
-**/.DS_Store
-.DS_Store?
 ._*

The existing line 9 .DS_Store pattern is sufficient.

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

In @.gitignore around lines 89 - 91, Remove the redundant .gitignore patterns:
delete the duplicate entries "**/.DS_Store" and ".DS_Store?" and keep the single
".DS_Store" pattern (the existing ".DS_Store" already matches files
recursively), so update the file by removing those two extra patterns referenced
in the diff.

101-104: Consolidate duplicate Python bytecode patterns.

Lines 102-104 overlap with existing patterns on lines 29-30. The *.py[cod] pattern on line 102 already matches *.pyc and *.pyo (covered on lines 29-30), and line 103's *.pyd is also covered by the bracket expression [cod].

♻️ Suggested consolidation

Option 1: Keep the original section and remove redundant new patterns:

 # Python Bytecode
-*.py[cod]
-*.pyd
 *$py.class

Option 2: Consolidate into the new section and remove from the original:

Remove lines 29-30 from the original Python section and keep lines 102-104, but drop line 103:

 # Python Bytecode
 *.py[cod]
-*.pyd
 *$py.class
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 101 - 104, Consolidate the duplicate Python bytecode
patterns by keeping the bracket pattern '*.py[cod]' as the canonical entry and
removing redundant explicit patterns like '*.pyc' and '*.pyo' and the duplicate
'*.pyd' elsewhere; also ensure '*$py.class' appears only once (remove its
duplicate). Update the gitignore so only the unique patterns ('*.py[cod]',
'*.pyd' if you choose to explicitly keep it, and '*$py.class') remain and remove
any overlapping entries to avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 1739-1743: The CLI currently parses --pid into a Swift Int (using
pidRaw -> parsedPid) but the server expects an Int32, so change the parsing to
parse and validate as Int32 (e.g., attempt Int32(pidRaw)), ensure the parsed
value is > 0 and within Int32.min..Int32.max, and on failure throw the same
CLIError message; assign the Int32 value to the existing pid variable (or
convert explicitly where needed) so the client only sends PIDs the server can
accept (refer to symbols pidRaw, parsedPid/pid and CLIError in cmux.swift).

---

Duplicate comments:
In `@Resources/bin/opencode-cmux-plugin.js`:
- Around line 90-98: The current cleanup commands (in clearNotifications() and
the similar cleanup block around the clear-status / clear-notifications calls)
invoke workspace-global cmux commands and can wipe other processes' UI state;
change these to use pid/source-scoped clears if cmux supports that (e.g., call
cmux clear-notifications and clear-status with the current PID/source flag) by
updating clearNotifications() and the corresponding cleanup section (search for
clearNotifications and any clear-status or clear-notifications invocations), or
if cmux has no PID-scoping option, remove/skip emitting those global clear
commands and rely on the existing stale-PID cleanup path instead so this process
does not clear another live OpenCode instance's status/notifications.

---

Nitpick comments:
In @.gitignore:
- Around line 89-91: Remove the redundant .gitignore patterns: delete the
duplicate entries "**/.DS_Store" and ".DS_Store?" and keep the single
".DS_Store" pattern (the existing ".DS_Store" already matches files
recursively), so update the file by removing those two extra patterns referenced
in the diff.
- Around line 101-104: Consolidate the duplicate Python bytecode patterns by
keeping the bracket pattern '*.py[cod]' as the canonical entry and removing
redundant explicit patterns like '*.pyc' and '*.pyo' and the duplicate '*.pyd'
elsewhere; also ensure '*$py.class' appears only once (remove its duplicate).
Update the gitignore so only the unique patterns ('*.py[cod]', '*.pyd' if you
choose to explicitly keep it, and '*$py.class') remain and remove any
overlapping entries to avoid duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbd21ff2-7c04-4827-9204-3276b37b4be0

📥 Commits

Reviewing files that changed from the base of the PR and between b9ebd9a and ece7e89.

📒 Files selected for processing (5)
  • .gitignore
  • CLI/cmux.swift
  • Resources/bin/opencode
  • Resources/bin/opencode-cmux-plugin.js
  • docs/notifications.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • Resources/bin/opencode

Comment thread CLI/cmux.swift Outdated
Server-side (TerminalController.swift:13072) parses --pid as Int32.
CLI was accepting any positive Int, meaning values exceeding
Int32.max would be silently rejected by the server without error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coygeek
Copy link
Copy Markdown
Author

coygeek commented Mar 16, 2026

Review feedback addressed in c80abe6

Fixed

# Source File Finding Fix
1 CodeRabbit CLI/cmux.swift:1739-1743 --pid parsed as Int but server-side (TerminalController.swift:13072) converts to Int32 — values exceeding Int32.max are silently rejected Changed let pid: Int? to Int32? and Int(pidRaw) to Int32(pidRaw) so CLI rejects out-of-range values with an error instead of silently failing

Test results

All three test suites pass after the change:

  • bash -n Resources/bin/opencode — syntax OK
  • python3 tests/test_opencode_wrapper_hooks.py — PASS
  • node tests/test_opencode_plugin_runtime.js — PASS

@ericmjl
Copy link
Copy Markdown

ericmjl commented Mar 19, 2026

Woohoo! I cannot wait to see this one merged, if it works!

ericmjl added 8 commits March 21, 2026 12:50
Detects busy->idle transition in session.status events and sends a
"Done: Session completed" notification with attention flag. This
creates an unread notification badge similar to Claude Code behavior.
Add a 5-second cooldown to prevent completion notifications from
flashing and disappearing immediately when OpenCode rapidly transitions
between states.
Track completed sessions with a 'completed' flag. When a session transitions
from busy to idle, mark it as completed and show a persistent 'Done' status
in the sidebar that stays until the session is cleared (on new session or deletion).
This mimics the Claude Code behavior where completion notifications remain
until you switch to that workspace/terminal.
Fix indentation bug that was causing notify() and sync() to always execute
regardless of the busy->idle transition condition, potentially causing
phantom notifications.
Prevent phantom 'Done' notifications by resetting the completed flag
when a session transitions from idle to busy (starting new work).
This ensures completion notifications only appear for actual work completion,
not for every idle state transition.
Remove accidentally duplicated code block that was causing the plugin
to stop processing events after session.status handling, which broke
all other event types (permissions, questions, etc.).
Clear waiting text when transitioning from idle to busy state (starting work or resuming from permission/question)
to prevent phantom notifications from stale waiting text.
cmux CLI commands (set-status, notify, clear-notifications, clear-status)
print 'OK' to stdout on success. When executed via Bun Shell's $ tagged
template, this output leaks into the terminal causing phantom 'OK'
notifications and TUI flickering. Use Bun Shell's .quiet() method to
suppress stdout/stderr while still executing commands normally.
austinywang and others added 4 commits March 22, 2026 21:04
…anaflow-ai#1973)

* test: cover queued restore-time terminal portal shift

* fix: resync terminal portal after restore-time bind
fix: persistent completion notifications + suppress phantom OK text
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Resources/bin/opencode-cmux-plugin.js (1)

93-101: ⚠️ Potential issue | 🟠 Major

The cleanup helpers are still workspace-global.

clear-notifications and clear-status opencode can still erase another live OpenCode instance’s prompt or pill in the same workspace. Since setStatus() is already pid-scoped, the safer fallback is to avoid proactive clears until cmux grows scoped clear APIs.

Based on learnings, notifications are automatically cleared by cmux when the user navigates to the relevant tab. There is no need to explicitly call cmux clear-notifications in agent hook scripts.

Also applies to: 117-125

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

In `@Resources/bin/opencode-cmux-plugin.js` around lines 93 - 101, The cleanup
helpers are using workspace-global cmux clears which can erase another OpenCode
instance; remove any proactive calls to `cmux clear-notifications` and `cmux
clear-status opencode` and stop invoking the `clearNotifications()` helper (or
turn it into a no-op), relying on cmux's automatic tab-aware clearing and the
pid-scoped `setStatus()` behavior instead; update references to the
`clearNotifications` function and any explicit `clear-status opencode`
invocations so they no longer perform global clears.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/bin/opencode-cmux-plugin.js`:
- Around line 73-76: The code currently returns a new pill state "Done" in the
completed-sessions check (the items.some((item) => item.completed) branch);
change that to reuse the existing "Idle" state so we don't add a sixth
state—return { value: "Idle", icon: "checkmark.circle.fill", color: BLUE,
attention: true } (keep attention:true and the same icon/color) so completion
still triggers attention without introducing a new pill value.

---

Duplicate comments:
In `@Resources/bin/opencode-cmux-plugin.js`:
- Around line 93-101: The cleanup helpers are using workspace-global cmux clears
which can erase another OpenCode instance; remove any proactive calls to `cmux
clear-notifications` and `cmux clear-status opencode` and stop invoking the
`clearNotifications()` helper (or turn it into a no-op), relying on cmux's
automatic tab-aware clearing and the pid-scoped `setStatus()` behavior instead;
update references to the `clearNotifications` function and any explicit
`clear-status opencode` invocations so they no longer perform global clears.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34de6488-b95e-457f-8dcf-5067cd23e157

📥 Commits

Reviewing files that changed from the base of the PR and between c80abe6 and 29d6277.

📒 Files selected for processing (2)
  • Resources/bin/opencode-cmux-plugin.js
  • tests/test_opencode_plugin_runtime.js
✅ Files skipped from review due to trivial changes (1)
  • tests/test_opencode_plugin_runtime.js

Comment thread Resources/bin/opencode-cmux-plugin.js Outdated
Also addresses CodeRabbit review feedback on Eric's completion changes:
- Replace "Done" sixth state with "Idle" + attention:true (blue ring)
- Merge duplicate idle->busy condition blocks
- Fix inconsistent indentation (normalize to 2-space)
- Fix misaligned else block in setStatus()
@holyhli
Copy link
Copy Markdown

holyhli commented Mar 23, 2026

I need this!

@ericmjl
Copy link
Copy Markdown

ericmjl commented Mar 27, 2026

I'd love to see this go live too!

@ericmjl
Copy link
Copy Markdown

ericmjl commented Mar 27, 2026

I have built the PR branch locally, fyi, and the effect is this:

image

Confirming it works and looks great!

@coygeek
Copy link
Copy Markdown
Author

coygeek commented Mar 27, 2026

@greptile-apps review
@coderabbitai review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Mar 27, 2026

@greptile-apps review
@coderabbitai review
@cubic-dev-ai review

@coygeek I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

@coygeek: Sure! Kicking off a fresh review of the PR now.


🧠 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: apollow
Repo: manaflow-ai/cmux PR: 1089
File: CLI/cmux.swift:462-499
Timestamp: 2026-03-09T02:08:54.956Z
Learning: Repo: manaflow-ai/cmux
PR: `#1089`
File: CLI/cmux.swift
Component: ClaudeHookTagExtractor.extractTags(subtitle:body:)
Learning: For Claude Code session tag extraction, pre-redact sensitive spans (UUIDs, emails, access tokens, filesystem paths, ENV_VAR=..., long numerics) across the combined body+subtitle using unanchored sensitiveSpanPatterns before tokenization. Then tokenize and still filter each token with anchored sensitivePatterns. Rationale: prevents PII/path fragments from slipping into searchable tags after delimiter splitting.

Learnt from: MaTriXy
Repo: manaflow-ai/cmux PR: 1460
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-16T08:02:13.558Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, socket v2 methods that accept surface_id/panel_id must handle cross-window routing. For panel background state mutations, v2PanelMarkBackground(params:) and v2PanelMarkForeground(params:) first try v2ResolveTabManager(params:), but only accept it if that manager actually owns the panelId; otherwise they fall back to AppDelegate.shared?.locateSurface(surfaceId:) to find the correct TabManager across windows. This pattern should be used for any panel_id-only routes to avoid active-window bias.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:3491-3493
Timestamp: 2026-03-04T14:06:16.241Z
Learning: For manaflow-ai/cmux PR `#819` (Japanese i18n), keep scope limited to localization changes; UX enhancements like preferring workspace.customTitle in workspaceDisplayName() or altering move-target labels should be handled in a separate follow-up issue.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 1771
File: Sources/Workspace.swift:8516-8558
Timestamp: 2026-03-18T16:46:47.403Z
Learning: Repo manaflow-ai/cmux — Preference: keep lower-confidence geometry/focus/portal behavior changes out of stabilization checkpoints; evaluate them in a separate, narrowly scoped follow-up PR with DEBUG-gated toggles and diagnostics.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 2262
File: Resources/Localizable.xcstrings:84820-84835
Timestamp: 2026-03-27T22:00:26.038Z
Learning: Repo: manaflow-ai/cmux — In `Resources/Localizable.xcstrings`, it is intentional for lower-confidence locales (e.g., ar, bs, da, pl, th, km, tr) to carry English-language fallback `stringUnit.value` strings with `state: "translated"` for newly added keys. This matches the existing `.xcstrings` catalog convention for the project; do not flag English values with state "translated" in these locales as a localization bug or suggest changing the state to "needs review" / "untranslated".
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".gitignore">

<violation number="1" location=".gitignore:130">
P2: Broad `.env.*` ignore rule also ignores common template files like `.env.example`, risking accidental exclusion of onboarding env templates.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .gitignore
vendor/bundle/

# Security
.env.*
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

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

P2: Broad .env.* ignore rule also ignores common template files like .env.example, risking accidental exclusion of onboarding env templates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .gitignore, line 130:

<comment>Broad `.env.*` ignore rule also ignores common template files like `.env.example`, risking accidental exclusion of onboarding env templates.</comment>

<file context>
@@ -47,3 +47,92 @@ tests/visual_report.html
+vendor/bundle/
+
+# Security
+.env.*
+*.pem
+*.key
</file context>
Suggested change
.env.*
.env.*
!**/.env.example
!**/.env.sample
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
cmuxTests/TerminalAndGhosttyTests.swift (1)

2987-2994: Avoid a time-based queue flush in this regression test.

The bind path under test queues multiple main-queue turns, so a fixed 0.05 second run-loop wait can still race on a busy CI runner. This test case already has drainMainQueue(), and calling it twice makes the sequencing explicit.

♻️ Proposed tweak
         DispatchQueue.main.async {
             shiftedContainer.frame.origin.x += 72
             contentView.layoutSubtreeIfNeeded()
             window.displayIfNeeded()
         }

-        RunLoop.current.run(until: Date().addingTimeInterval(0.05))
+        drainMainQueue()
+        drainMainQueue()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/TerminalAndGhosttyTests.swift` around lines 2987 - 2994, Replace
the ad-hoc time-based RunLoop delay with explicit main-queue drainage: instead
of calling RunLoop.current.run(until: Date().addingTimeInterval(0.05)) after the
DispatchQueue.main.async block that mutates shiftedContainer.frame and calls
contentView.layoutSubtreeIfNeeded() and window.displayIfNeeded(), call
drainMainQueue() twice (or call drainMainQueue() immediately after the async
block and again to ensure any queued follow-up work runs) so the test
deterministically flushes the main queue without relying on a fixed timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 2248-2360: There are duplicate switch cases for
set-status/clear-status/list-status/set-progress/clear-progress/log/clear-log/list-log/sidebar-state
causing a compile error and the new --pid handling in the second block never
reaches the delegated path; remove the duplicate block and consolidate behavior
by either (A) updating forwardSidebarMetadataCommand(...) to parse "--pid" (use
parseOption to capture pidRaw, validate with Int32(pidRaw) > 0, include "
--pid=\(pid)" in the socket command built before calling sendV1Command) or (B)
delete the forwardSidebarMetadataCommand calls for these cases and keep the
single direct implementations (using parseOption, resolveWorkspaceId,
socketQuote and sendV1Command) that include the --pid validation; ensure only
one implementation per case label and that sendV1Command/socket command
construction includes the validated pid when present.

In `@Sources/AppDelegate.swift`:
- Around line 2420-2439: stabilizeUITestLaunchWindowAndForeground currently
considers any NSApp window as satisfying readiness, causing false positives when
non-main windows (settings/import/debug) appear; change the fallback creation
and all readiness checks to only consider main terminal windows by using
isMainTerminalWindow(_:). Specifically, when creating a fallback in the initial
if-block call openNewMainWindow only if no main terminal window exists (filter
NSApp.windows with isMainTerminalWindow), and replace
hasWindow/hasVisibleWindow/hasKeyWindow checks with checks that operate on
NSApp.windows.filter(isMainTerminalWindow) (e.g., any main window exists, any
main window isVisible, NSApp.keyWindow is a main terminal window). Keep calls to
moveUITestWindowToTargetDisplayIfNeeded, activateUITestAppIfNeeded, and
writeUITestDiagnosticsIfNeeded but ensure they operate or log with respect to
main terminal windows only.
- Around line 2447-2450: The macOS 14+ branch drops .activateIgnoringOtherApps
causing weaker activation; to fix, before calling
NSRunningApplication.current.activate in the macOS 14+ branch call
NSApp.yieldActivation(to: NSRunningApplication.current) and then invoke
NSRunningApplication.current.activate(options: [.activateAllWindows,
.activateIgnoringOtherApps]) so behavior matches the other activation sites
(compare with other activate(...) calls) while also performing Apple's
recommended cooperative yield first.

---

Nitpick comments:
In `@cmuxTests/TerminalAndGhosttyTests.swift`:
- Around line 2987-2994: Replace the ad-hoc time-based RunLoop delay with
explicit main-queue drainage: instead of calling RunLoop.current.run(until:
Date().addingTimeInterval(0.05)) after the DispatchQueue.main.async block that
mutates shiftedContainer.frame and calls contentView.layoutSubtreeIfNeeded() and
window.displayIfNeeded(), call drainMainQueue() twice (or call drainMainQueue()
immediately after the async block and again to ensure any queued follow-up work
runs) so the test deterministically flushes the main queue without relying on a
fixed timeout.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 014ac9cf-d899-4efb-9362-7bd5019f9d9a

📥 Commits

Reviewing files that changed from the base of the PR and between 29d6277 and f007166.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/bin/opencode-cmux-plugin.js
  • Sources/AppDelegate.swift
  • Sources/TerminalWindowPortal.swift
  • TODO.md
  • cmuxTests/TerminalAndGhosttyTests.swift
✅ Files skipped from review due to trivial changes (3)
  • TODO.md
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/bin/opencode-cmux-plugin.js

Comment thread CLI/cmux.swift
Comment on lines +2248 to +2360
case "set-status":
let (icon, r1) = parseOption(commandArgs, name: "--icon")
let (color, r2) = parseOption(r1, name: "--color")
let (pidRaw, r3) = parseOption(r2, name: "--pid")
let pid: Int32?
if let pidRaw {
guard let parsedPid = Int32(pidRaw), parsedPid > 0 else {
throw CLIError(message: "--pid must be a positive integer")
}
pid = parsedPid
} else {
pid = nil
}
let (wsFlag, r4) = parseOption(r3, name: "--workspace")
guard r4.count >= 2 else {
throw CLIError(message: "set-status requires <key> and <value>")
}
let key = r4[0]
let value = r4.dropFirst().joined(separator: " ")
guard !value.isEmpty else {
throw CLIError(message: "set-status requires a non-empty value")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "set_status \(key) \(socketQuote(value))"
if let icon { socketCmd += " --icon=\(socketQuote(icon))" }
if let color { socketCmd += " --color=\(socketQuote(color))" }
if let pid { socketCmd += " --pid=\(pid)" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-status":
let (wsFlag, csRemaining) = parseOption(commandArgs, name: "--workspace")
guard let key = csRemaining.first else {
throw CLIError(message: "clear-status requires a <key>")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_status \(key) --tab=\(wsId)", client: client)
print(response)

case "list-status":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("list_status --tab=\(wsId)", client: client)
print(response)

case "set-progress":
let (label, spR1) = parseOption(commandArgs, name: "--label")
let (wsFlag, spR2) = parseOption(spR1, name: "--workspace")
guard let valueStr = spR2.first else {
throw CLIError(message: "set-progress requires a progress value (0.0-1.0)")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "set_progress \(valueStr)"
if let label { socketCmd += " --label=\(socketQuote(label))" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-progress":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_progress --tab=\(wsId)", client: client)
print(response)

case "log":
let (level, r1) = parseOption(commandArgs, name: "--level")
let (source, r2) = parseOption(r1, name: "--source")
let (wsFlag, r3) = parseOption(r2, name: "--workspace")
// Strip leading "--" separator if present
let positional = r3.first == "--" ? Array(r3.dropFirst()) : r3
let message = positional.joined(separator: " ")
guard !message.isEmpty else {
throw CLIError(message: "log requires a message")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "log"
if let level { socketCmd += " --level=\(level)" }
if let source { socketCmd += " --source=\(socketQuote(source))" }
socketCmd += " --tab=\(wsId) -- \(socketQuote(message))"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-log":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_log --tab=\(wsId)", client: client)
print(response)

case "list-log":
let (limitStr, r1) = parseOption(commandArgs, name: "--limit")
let (wsFlag, _) = parseOption(r1, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "list_log"
if let limitStr { socketCmd += " --limit=\(limitStr)" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "sidebar-state":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("sidebar_state --tab=\(wsId)", client: client)
print(response)
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect each sidebar metadata command below to be reported twice.
python3 - <<'PY'
from pathlib import Path
import re

path = Path("CLI/cmux.swift")
targets = {
    "set-status", "clear-status", "list-status",
    "set-progress", "clear-progress",
    "log", "clear-log", "list-log", "sidebar-state",
}

hits = {}
for lineno, line in enumerate(path.read_text().splitlines(), 1):
    m = re.match(r'\s*case\s+"([^"]+)":', line)
    if m and m.group(1) in targets:
        hits.setdefault(m.group(1), []).append(lineno)

for name in sorted(targets):
    lines = hits.get(name, [])
    if len(lines) > 1:
        print(f"{name}: duplicate case labels at lines {lines}")
PY

Repository: manaflow-ai/cmux

Length of output: 619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show context around each duplicate set to understand the switch structure
echo "=== CONTEXT AROUND FIRST SET (2156-2165 for set-status) ==="
sed -n '2145,2175p' CLI/cmux.swift

echo ""
echo "=== CONTEXT AROUND SECOND SET (2248-2260 for set-status) ==="
sed -n '2240,2270p' CLI/cmux.swift

echo ""
echo "=== CONTEXT AROUND THIRD SET (7001-7010 for set-status) ==="
sed -n '6990,7020p' CLI/cmux.swift

Repository: manaflow-ai/cmux

Length of output: 4119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the forwardSidebarMetadataCommand function definition
echo "=== forwardSidebarMetadataCommand function ==="
rg -A 30 'func forwardSidebarMetadataCommand' CLI/cmux.swift | head -50

echo ""
echo "=== Check if forwardSidebarMetadataCommand references --pid ==="
rg 'forwardSidebarMetadataCommand' CLI/cmux.swift -B 2 -A 15

Repository: manaflow-ai/cmux

Length of output: 4994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the complete forwardSidebarMetadataCommand function
echo "=== Complete forwardSidebarMetadataCommand function ==="
rg -A 100 'private func forwardSidebarMetadataCommand' CLI/cmux.swift | head -120

Repository: manaflow-ai/cmux

Length of output: 3859


Remove or consolidate the duplicate sidebar-metadata case block.

Lines 2156-2235 already handle set-status, clear-status, list-status, set-progress, clear-progress, log, clear-log, list-log, and sidebar-state via forwardSidebarMetadataCommand(). The block at Lines 2248-2360 re-declares the same cases in the same switch command, making CLI/cmux.swift invalid Swift—the CLI will not compile and the new --pid support never becomes active.

The second block adds --pid validation (Int32(pidRaw), pidRaw > 0) for set-status, which the delegated helper does not perform. Consolidate by either updating forwardSidebarMetadataCommand to parse and pass --pid to the socket command, or replace the delegated calls with direct implementations that include --pid handling. Keep only one implementation path per case label.

🧰 Tools
🪛 SwiftLint (0.63.2)

[Error] 2248-2248: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2280-2280: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2290-2290: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2297-2297: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2311-2311: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2318-2318: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2337-2337: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2344-2344: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)


[Error] 2355-2355: Duplicate sets of conditions in the same branch instruction should be avoided

(duplicate_conditions)

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

In `@CLI/cmux.swift` around lines 2248 - 2360, There are duplicate switch cases
for
set-status/clear-status/list-status/set-progress/clear-progress/log/clear-log/list-log/sidebar-state
causing a compile error and the new --pid handling in the second block never
reaches the delegated path; remove the duplicate block and consolidate behavior
by either (A) updating forwardSidebarMetadataCommand(...) to parse "--pid" (use
parseOption to capture pidRaw, validate with Int32(pidRaw) > 0, include "
--pid=\(pid)" in the socket command built before calling sendV1Command) or (B)
delete the forwardSidebarMetadataCommand calls for these cases and keep the
single direct implementations (using parseOption, resolveWorkspaceId,
socketQuote and sendV1Command) that include the --pid validation; ensure only
one implementation per case label and that sendV1Command/socket command
construction includes the validated pid when present.

Comment thread Sources/AppDelegate.swift
Comment on lines +2420 to +2439
if NSApp.windows.isEmpty, !didRequestFallbackUITestWindow {
didRequestFallbackUITestWindow = true
openNewMainWindow(nil)
}

moveUITestWindowToTargetDisplayIfNeeded()
activateUITestAppIfNeeded()

let hasWindow = !NSApp.windows.isEmpty
let hasVisibleWindow = NSApp.windows.contains { $0.isVisible }
let hasKeyWindow = NSApp.keyWindow != nil
let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
writeUITestDiagnosticsIfNeeded(stage: stage)

guard attempt < 20 else { return }
if !hasWindow || !hasVisibleWindow || !hasKeyWindow || !NSRunningApplication.current.isActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.stabilizeUITestLaunchWindowAndForeground(attempt: attempt + 1)
}
}
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

Gate UI-test stabilization on a main terminal window.

This loop succeeds as soon as any AppKit window is visible/key. If a settings/import/debug window appears first, retries stop even though no cmux.main window is ready, and the helper can keep moving/activating the wrong window. Filter both the fallback creation and the ready checks to isMainTerminalWindow(_:).

Suggested fix
-        if NSApp.windows.isEmpty, !didRequestFallbackUITestWindow {
+        let mainTerminalWindows = NSApp.windows.filter { self.isMainTerminalWindow($0) }
+        if mainTerminalWindows.isEmpty, !didRequestFallbackUITestWindow {
             didRequestFallbackUITestWindow = true
             openNewMainWindow(nil)
         }

         moveUITestWindowToTargetDisplayIfNeeded()
         activateUITestAppIfNeeded()

-        let hasWindow = !NSApp.windows.isEmpty
-        let hasVisibleWindow = NSApp.windows.contains { $0.isVisible }
-        let hasKeyWindow = NSApp.keyWindow != nil
+        let hasWindow = !mainTerminalWindows.isEmpty
+        let hasVisibleWindow = mainTerminalWindows.contains { $0.isVisible }
+        let hasKeyWindow = NSApp.keyWindow.map(isMainTerminalWindow) ?? false
         let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
         writeUITestDiagnosticsIfNeeded(stage: stage)
📝 Committable suggestion

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

Suggested change
if NSApp.windows.isEmpty, !didRequestFallbackUITestWindow {
didRequestFallbackUITestWindow = true
openNewMainWindow(nil)
}
moveUITestWindowToTargetDisplayIfNeeded()
activateUITestAppIfNeeded()
let hasWindow = !NSApp.windows.isEmpty
let hasVisibleWindow = NSApp.windows.contains { $0.isVisible }
let hasKeyWindow = NSApp.keyWindow != nil
let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
writeUITestDiagnosticsIfNeeded(stage: stage)
guard attempt < 20 else { return }
if !hasWindow || !hasVisibleWindow || !hasKeyWindow || !NSRunningApplication.current.isActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.stabilizeUITestLaunchWindowAndForeground(attempt: attempt + 1)
}
}
let mainTerminalWindows = NSApp.windows.filter { self.isMainTerminalWindow($0) }
if mainTerminalWindows.isEmpty, !didRequestFallbackUITestWindow {
didRequestFallbackUITestWindow = true
openNewMainWindow(nil)
}
moveUITestWindowToTargetDisplayIfNeeded()
activateUITestAppIfNeeded()
let hasWindow = !mainTerminalWindows.isEmpty
let hasVisibleWindow = mainTerminalWindows.contains { $0.isVisible }
let hasKeyWindow = NSApp.keyWindow.map(isMainTerminalWindow) ?? false
let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
writeUITestDiagnosticsIfNeeded(stage: stage)
guard attempt < 20 else { return }
if !hasWindow || !hasVisibleWindow || !hasKeyWindow || !NSRunningApplication.current.isActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.stabilizeUITestLaunchWindowAndForeground(attempt: attempt + 1)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 2420 - 2439,
stabilizeUITestLaunchWindowAndForeground currently considers any NSApp window as
satisfying readiness, causing false positives when non-main windows
(settings/import/debug) appear; change the fallback creation and all readiness
checks to only consider main terminal windows by using isMainTerminalWindow(_:).
Specifically, when creating a fallback in the initial if-block call
openNewMainWindow only if no main terminal window exists (filter NSApp.windows
with isMainTerminalWindow), and replace hasWindow/hasVisibleWindow/hasKeyWindow
checks with checks that operate on NSApp.windows.filter(isMainTerminalWindow)
(e.g., any main window exists, any main window isVisible, NSApp.keyWindow is a
main terminal window). Keep calls to moveUITestWindowToTargetDisplayIfNeeded,
activateUITestAppIfNeeded, and writeUITestDiagnosticsIfNeeded but ensure they
operate or log with respect to main terminal windows only.

Comment thread Sources/AppDelegate.swift
Comment on lines +2447 to +2450
if #available(macOS 14.0, *) {
NSRunningApplication.current.activate(options: [.activateAllWindows])
} else {
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
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

🌐 Web query:

On macOS 14 and later, is NSRunningApplication.ActivationOptions.activateIgnoringOtherApps still supported for NSRunningApplication.current.activate(options:), and how does its behavior differ from using only .activateAllWindows?

💡 Result:

Yes, NSRunningApplication.ActivationOptions.activateIgnoringOtherApps (also known as .activateIgnoringOtherApps) is still supported on macOS 14 (Sonoma) and later for NSRunningApplication.current.activate(options:). Apple's official documentation lists it as an available option in NSApplication.ActivationOptions, used by the activate(options:) method, with copyright dated 2026 indicating current support. A GitHub issue from MacVim confirms it is deprecated in macOS 14 but "still works," with no breakage in macOS 15 planned yet. Rust bindings explicitly note it as deprecated in macOS 14 with no effect, but Apple docs do not mark it deprecated and it remains documented. It differs from .activateAllWindows as follows: - .activateIgnoringOtherApps: Activates the target app regardless of the currently active app, potentially stealing focus (discouraged for poor UX). - .activateAllWindows: Activates the app (cooperatively) and brings all its windows forward (default brings only main/key windows forward). These can be combined. Default activation deactivates the calling app first then activates the target only if no other app is active meanwhile. On macOS 14+, prefer cooperative activation with NSApp.yieldActivation(to:) before calling activate.

Citations:


🏁 Script executed:

# Find and examine the AppDelegate.swift file around lines 2447-2450
fd -type f -name "AppDelegate.swift" | head -20

Repository: manaflow-ai/cmux

Length of output: 230


🏁 Script executed:

# Search for context around the target lines and other uses of activateIgnoringOtherApps
rg -n "activateIgnoringOtherApps" --max-count=20 -B3 -A3

Repository: manaflow-ai/cmux

Length of output: 2760


🏁 Script executed:

# Get the full context of AppDelegate.swift to understand what this code block is doing
wc -l Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 91


🏁 Script executed:

# Get more context around lines 2447-2450 to understand the function and purpose
sed -n '2430,2460p' Sources/AppDelegate.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 1602


Address the weaker activation on macOS 14+, but consider Apple's deprecation guidance.

The macOS 14+ branch inconsistently drops .activateIgnoringOtherApps, making this path weaker than all other activation calls in the file (AppDelegate lines 6065, 10905, 11392, and TerminalController 6484, 6486). This can leave NSRunningApplication.current.isActive false through retries.

However, note that Apple deprecated .activateIgnoringOtherApps on macOS 14 (though it still functions). Instead of unifying around the deprecated option, consider using Apple's recommended cooperative activation: NSApp.yieldActivation(to:) before activate(options:) for macOS 14+, or restore .activateIgnoringOtherApps for consistency if immediate activation is required for test reliability.

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

In `@Sources/AppDelegate.swift` around lines 2447 - 2450, The macOS 14+ branch
drops .activateIgnoringOtherApps causing weaker activation; to fix, before
calling NSRunningApplication.current.activate in the macOS 14+ branch call
NSApp.yieldActivation(to: NSRunningApplication.current) and then invoke
NSRunningApplication.current.activate(options: [.activateAllWindows,
.activateIgnoringOtherApps]) so behavior matches the other activation sites
(compare with other activate(...) calls) while also performing Apple's
recommended cooperative yield first.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 27, 2026

Greptile Summary

This PR ships a bundled opencode wrapper and a companion JS plugin that together auto-inject OpenCode → cmux sidebar integration (status pills + notifications) whenever OpenCode is launched inside a cmux terminal. It also extends the cmux CLI with nine new sidebar metadata commands, adds retry logic for a flaky UI test, and stabilizes the UITest launch window.

Key issues found:

  • Unreachable sidebar command implementations (P1): CLI/cmux.swift already contains case labels for all nine new commands (lines 2156–2235) using the existing forwardSidebarMetadataCommand helper. The new implementations inserted after claude-hook (lines 2248–2360) are never reached, making the --pid validation and improved error handling dead code.
  • set-progress value not validated (P2): The 0.0-1.0 range promised in the help text and error message is not enforced at the Swift CLI level; invalid strings pass directly to the socket.
  • --level not quoted in log (P2): Inconsistent quoting versus --source in the same command.
  • session.idle skips completion path (P2): The blue attention "Idle" badge and "Done" notification only fire on the session.status busy→idle transition; a raw session.idle event does not set state.completed.

The bash wrapper, JS plugin logic, tests, and the CI/UITest stabilization changes are all well-structured and correct.

Confidence Score: 3/5

The core new CLI feature (--pid support in set-status) is dead code due to unreachable duplicate switch cases; needs a fix before the intended behavior is active.

One P1 finding: all nine new sidebar command case blocks in CLI/cmux.swift are unreachable because identical case labels already exist earlier in the same switch statement. The primary advertised feature of this PR (--pid stale-process cleanup for set-status) is never executed. The wrapper, plugin, and tests are clean, but the Swift CLI change is fundamentally broken for its stated purpose.

CLI/cmux.swift — duplicate switch cases make the new set-status/--pid implementation unreachable; the pre-existing forwardSidebarMetadataCommand cases (lines 2156–2235) must be removed or updated.

Important Files Changed

Filename Overview
CLI/cmux.swift Adds sidebar metadata commands with --pid support, but the new case blocks are unreachable dead code — duplicate switch cases at lines 2156–2235 using forwardSidebarMetadataCommand match first.
Resources/bin/opencode New bash wrapper that detects a live cmux socket, builds a temp config overlay dir with the injected JS plugin, runs real opencode, then cleans up status/notifications on exit. Logic is sound with fallback to plain opencode on any setup failure.
Resources/bin/opencode-cmux-plugin.js OpenCode plugin mapping session/permission/question/error events to sidebar status pills and notifications. Event priority logic is well structured; session.idle handler skips the completion-detection path that session.status uses.
.github/workflows/ci.yml Adds a retry wrapper around the flaky display-resolution UI test using PIPESTATUS and pattern-matched error detection; clean and correct.
Sources/AppDelegate.swift Refactors UITest launch stabilization into a dedicated retry loop with a one-shot window fallback guard; matches the CI retry logic added above.
tests/test_opencode_wrapper_hooks.py Regression tests for the bash wrapper covering live-socket injection, missing-socket passthrough, and stale-socket passthrough with fully isolated fake binaries.
tests/test_opencode_plugin_runtime.js Node.js runtime tests for the JS plugin covering all event types using a tagged-template mock for cmux commands.

Sequence Diagram

sequenceDiagram
    participant User
    participant Wrapper as Resources/bin/opencode (bash)
    participant Plugin as opencode-cmux-plugin.js
    participant OpenCode as Real opencode binary
    participant CMux as cmux CLI

    User->>Wrapper: opencode [args]
    Wrapper->>Wrapper: Check CMUX_SURFACE_ID + socket ping
    alt Integration active
        Wrapper->>Wrapper: mktemp overlay config dir
        Wrapper->>Wrapper: Symlink existing user config
        Wrapper->>Wrapper: Copy cmux-integration.js to plugins/
        Wrapper->>CMux: clear-status opencode
        Wrapper->>OpenCode: exec with OPENCODE_CONFIG_DIR=overlay
        OpenCode->>Plugin: session.created / session.status events
        Plugin->>CMux: set-status opencode Running/Idle/Error
        Plugin->>CMux: notify (permission / question / error)
        OpenCode->>Plugin: busy to idle (completion)
        Plugin->>CMux: notify Done
        Plugin->>CMux: set-status opencode Idle (blue attention)
        OpenCode-->>Wrapper: exit
        Wrapper->>CMux: clear-status opencode
        Wrapper->>CMux: clear-notifications
        Wrapper->>Wrapper: cleanup temp config dir (EXIT trap)
    else Socket unavailable / disabled
        Wrapper->>OpenCode: exec directly (passthrough)
    end
    Wrapper-->>User: exit $STATUS
Loading

Reviews (1): Last reviewed commit: "merge: resolve CLI/cmux.swift conflict w..." | Re-trigger Greptile

Comment thread CLI/cmux.swift
Comment on lines +2248 to +2360
case "set-status":
let (icon, r1) = parseOption(commandArgs, name: "--icon")
let (color, r2) = parseOption(r1, name: "--color")
let (pidRaw, r3) = parseOption(r2, name: "--pid")
let pid: Int32?
if let pidRaw {
guard let parsedPid = Int32(pidRaw), parsedPid > 0 else {
throw CLIError(message: "--pid must be a positive integer")
}
pid = parsedPid
} else {
pid = nil
}
let (wsFlag, r4) = parseOption(r3, name: "--workspace")
guard r4.count >= 2 else {
throw CLIError(message: "set-status requires <key> and <value>")
}
let key = r4[0]
let value = r4.dropFirst().joined(separator: " ")
guard !value.isEmpty else {
throw CLIError(message: "set-status requires a non-empty value")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "set_status \(key) \(socketQuote(value))"
if let icon { socketCmd += " --icon=\(socketQuote(icon))" }
if let color { socketCmd += " --color=\(socketQuote(color))" }
if let pid { socketCmd += " --pid=\(pid)" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-status":
let (wsFlag, csRemaining) = parseOption(commandArgs, name: "--workspace")
guard let key = csRemaining.first else {
throw CLIError(message: "clear-status requires a <key>")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_status \(key) --tab=\(wsId)", client: client)
print(response)

case "list-status":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("list_status --tab=\(wsId)", client: client)
print(response)

case "set-progress":
let (label, spR1) = parseOption(commandArgs, name: "--label")
let (wsFlag, spR2) = parseOption(spR1, name: "--workspace")
guard let valueStr = spR2.first else {
throw CLIError(message: "set-progress requires a progress value (0.0-1.0)")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "set_progress \(valueStr)"
if let label { socketCmd += " --label=\(socketQuote(label))" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-progress":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_progress --tab=\(wsId)", client: client)
print(response)

case "log":
let (level, r1) = parseOption(commandArgs, name: "--level")
let (source, r2) = parseOption(r1, name: "--source")
let (wsFlag, r3) = parseOption(r2, name: "--workspace")
// Strip leading "--" separator if present
let positional = r3.first == "--" ? Array(r3.dropFirst()) : r3
let message = positional.joined(separator: " ")
guard !message.isEmpty else {
throw CLIError(message: "log requires a message")
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "log"
if let level { socketCmd += " --level=\(level)" }
if let source { socketCmd += " --source=\(socketQuote(source))" }
socketCmd += " --tab=\(wsId) -- \(socketQuote(message))"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "clear-log":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("clear_log --tab=\(wsId)", client: client)
print(response)

case "list-log":
let (limitStr, r1) = parseOption(commandArgs, name: "--limit")
let (wsFlag, _) = parseOption(r1, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "list_log"
if let limitStr { socketCmd += " --limit=\(limitStr)" }
socketCmd += " --tab=\(wsId)"
let response = try sendV1Command(socketCmd, client: client)
print(response)

case "sidebar-state":
let (wsFlag, _) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
let response = try sendV1Command("sidebar_state --tab=\(wsId)", client: client)
print(response)
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 New sidebar command cases are unreachable dead code

CLI/cmux.swift already contains matching case labels for every command added in this PR — set-status (line 2156), clear-status (line 2165), list-status (line 2174), set-progress (line 2183), clear-progress (line 2192), log (line 2201), clear-log (line 2210), list-log (line 2219), and sidebar-state (line 2228) — all implemented via forwardSidebarMetadataCommand. Because Swift evaluates switch cases top-to-bottom and stops at the first match, the new implementations added here (lines 2248–2360) are never reached.

The critical consequence for this PR is that the --pid validation and the explicit set_status <key> <value> argument shape added at lines 2248–2278 are dead code. A user running cmux set-status opencode Running --pid -99 will silently forward --pid -99 to the socket (via forwardSidebarMetadataCommand) instead of receiving the --pid must be a positive integer error.

To fix, either:

  1. Remove the pre-existing forwardSidebarMetadataCommand cases for these commands (lines 2156–2235) and keep the new implementations, or
  2. Update the existing cases in-place to add --pid parsing/validation rather than inserting new cases after claude-hook.

Comment thread CLI/cmux.swift
}
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "set_progress \(valueStr)"
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 set-progress value is not validated

valueStr is interpolated directly into the socket command without checking that it is a valid float in [0.0, 1.0]. This is inconsistent with the explicit positive-integer guard added for --pid in set-status, and the help text promises 0.0-1.0. An invalid string (e.g. "banana") would reach the server with no useful client-side error.

Suggested change
var socketCmd = "set_progress \(valueStr)"
guard let valueStr = spR2.first else {
throw CLIError(message: "set-progress requires a progress value (0.0-1.0)")
}
guard let progressVal = Double(valueStr), (0.0...1.0).contains(progressVal) else {
throw CLIError(message: "set-progress value must be a number between 0.0 and 1.0")
}

Comment thread CLI/cmux.swift
let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var socketCmd = "log"
if let level { socketCmd += " --level=\(level)" }
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 --level is not quoted, inconsistent with --source

--source is wrapped in socketQuote() on the very next line, but --level is interpolated bare. If a level value somehow contains spaces or special characters the socket parser would misinterpret it. For consistency:

Suggested change
if let level { socketCmd += " --level=\(level)" }
if let level { socketCmd += " --level=\(socketQuote(level))" }

Comment on lines +177 to +185
if (event.type === "session.idle") {
const sessionID = event.properties?.sessionID
if (!sessionID) return
const state = ensure(sessions, sessionID)
state.state = "idle"
state.error = ""
await sync()
return
}
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 session.idle does not trigger the completion path

The completion detection — setting state.completed = true and firing the "Done" notification — lives exclusively in the session.status handler on the busy → idle transition (line 169). The session.idle event handler at line 177 only sets state.state = "idle" and calls sync(), so if OpenCode emits session.idle as its primary completion signal, the blue-attention "Idle" badge and "Done" notification will never fire.

If session.idle is a distinct "work finished" event you expect OpenCode to emit, consider mirroring the completion logic here:

if (event.type === "session.idle") {
  const sessionID = event.properties?.sessionID
  if (!sessionID) return
  const state = ensure(sessions, sessionID)
  const prevState = state.state
  state.state = "idle"
  state.error = ""
  if (prevState === "busy") {
    state.completed = true
    await notify("Done", "Session completed")
  }
  await sync()
  return
}

If session.idle and session.status { type: "idle" } are always fired together, this is safe to leave as-is, but a comment explaining the assumption would help future readers.

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.

Add built-in OpenCode sidebar status integration

4 participants