Skip to content

fix(surface): accept surface_ref and surface aliases in send/read RPCs (related #2045) - #3032

Open
aryateja2106 wants to merge 1 commit into
manaflow-ai:mainfrom
aryateja2106:fix/surface-rpc-ref-alias-acceptance
Open

aryateja2106 wants to merge 1 commit into
manaflow-ai:mainfrom
aryateja2106:fix/surface-rpc-ref-alias-acceptance

Conversation

@aryateja2106

@aryateja2106 aryateja2106 commented Apr 20, 2026 •

Copy link
Copy Markdown

Related #2045 (same class of asymmetric targeting bug).

Problem

surface.send_text, surface.send_key, surface.read_text, and surface.clear_history only inspected params[\"surface_id\"] when resolving the target surface. If absent, they fell back to ws.focusedPanelId (the caller's own pane).

This makes the input/output schemas asymmetric: responses already include both surface_id (UUID) and surface_ref (short ref like surface:12), but inputs only accept surface_id. Callers passing surface_ref or surface had their target silently ignored — the RPC operated on the wrong pane and the bug was easy to miss because no error fired.

Reproducer (the silent retargeting that this fixes)

```
$ cmux new-pane --type terminal --direction right --id-format both
OK surface:12 pane:11 workspace:5

$ cmux rpc surface.send_text '{"surface":"surface:12","text":"x\\n"}'
{
"workspace_ref" : "workspace:5",
"surface_ref" : "surface:7", ← caller's own surface, not surface:12
"surface_id" : "9E1F1A31-...", ← caller's own UUID
...
}
```

In practice this hits OMC / Claude Code orchestration sessions hard: an orchestrator trying to drive a worker pane via RPC ends up typing into its own prompt box, which then gets submitted as user input on the next newline. The bug is invisible in the response shape because the response always echoes a valid surface_id+surface_ref pair (just the wrong one).

Fix

Replace the params[\"surface_id\"] check in all four handlers (v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText) with a fallback chain:

```swift
let rawSurface = params["surface_id"] ?? params["surface_ref"] ?? params["surface"]
if rawSurface != nil {
surfaceId = v2UUIDAny(rawSurface)
guard surfaceId != nil else {
result = .err(code: "not_found", message: "Surface not found for the given surface_id/surface_ref/surface", data: nil)
return
}
} else {
surfaceId = ws.focusedPanelId
}
```

Uses the existing v2UUIDAny helper (line 3136), which already accepts UUIDs and short-form handle refs via v2ResolveHandleRef. No new helpers, no new dependencies.

Compatibility

  • Existing surface_id callers: behavior unchanged (still hit the chain first).
  • New surface_ref / surface callers: now resolve correctly instead of silently retargeting.
  • Response shape unchanged.
  • tab_id and other parameters: untouched.

Files changed

File Change
Sources/TerminalController.swift 4 identical handler patches via fallback-chain pattern

Verification

I do not have an Xcode build chain locally, so this PR has been code-reviewed but not Xcode-built. The change is a mechanical pattern substitution using a helper (v2UUIDAny) that already exists and is already used elsewhere in the file (e.g. lines 3136–3144). I'd appreciate a maintainer applying the existing build/test pipeline. The companion test additions for tmux_split_ref_test.go-style coverage are in scope for a follow-up — happy to draft them once the approach here is validated.

Related

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced surface-targeting handlers to accept multiple parameter formats for surface identifiers, improving compatibility and error messaging.

Summary by cubic

Accepts surface_ref and surface in surface.send_text, surface.send_key, surface.read_text, and surface.clear_history so RPCs target the correct surface instead of falling back to the caller’s pane. Aligns inputs with outputs and addresses the asymmetric targeting bug (related to #2045).

  • Bug Fixes
    • Added fallback chain: surface_id → surface_ref → surface, resolved via v2UUIDAny.
    • Return not_found when the ref is invalid; stop defaulting to ws.focusedPanelId.
    • No response changes; existing surface_id callers are unaffected.

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

Related manaflow-ai#2045.

`surface.send_text`, `surface.send_key`, `surface.read_text`, and
`surface.clear_history` only inspected `params["surface_id"]` when
resolving the target surface; if absent, they silently fell back to
`ws.focusedPanelId` (the caller's own pane). Callers using `surface_ref`
(short refs like `surface:12`) or `surface` keys had their target
silently ignored — the RPC then operated on the wrong pane.

This is the same class as manaflow-ai#2045: the targeting parameter is asymmetric
between request inputs and response outputs. Responses already include
both `surface_id` and `surface_ref`; inputs should accept both forms.

Replace the `params["surface_id"]` check in all four handlers with a
fallback chain through `surface_id`, `surface_ref`, and `surface`, using
the existing `v2UUIDAny` helper (which already accepts UUIDs and
short-form handle refs via `v2ResolveHandleRef`). Update the error
message to reflect all three accepted keys.

Reproducer of the silent retargeting bug that this fixes:

  $ cmux new-pane --type terminal --direction right --id-format both
  OK surface:12 pane:11 workspace:5

  $ cmux rpc surface.send_text '{"surface":"surface:12","text":"x\n"}'
  # response surface_id is the CALLER's surface (e.g. 9E1F1A31...),
  # not the requested surface:12. Text was injected into the caller's
  # own input box, which in a Claude Code / OMC session ends up
  # submitted as user input.

After this change the request honors `surface`/`surface_ref`/`surface_id`
identically.
@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Updated multiple V2 surface-targeting handlers in TerminalController to accept flexible surface identifier sources (surface_id, surface_ref, or surface parameters) and improved error messaging to reflect all supported keys. Surface-ID parsing was shifted to use v2UUIDAny for the resolved value.

Changes

Cohort / File(s) Summary
V2 Surface Resolution Logic
Sources/TerminalController.swift
Updated multiple V2 surface-targeting handlers to derive surfaceId from the first non-nil of params["surface_id"], params["surface_ref"], or params["surface"]. Changed surface-ID parsing from v2UUID(params, "surface_id") to v2UUIDAny(rawSurface) and broadened error messages to reference all three supported parameter keys.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 With whiskers twitched and joy so keen,
More surface paths than e'er been seen!
The handlers now dance, flexible and free,
Accepting three keys where once was but one, see?
Surface, surface\_ref, surface\_id all align—
A parametric bounty, oh how they shine! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers problem statement, fix details, compatibility, and verification notes, but lacks explicit testing methodology and checklist completion. Complete the Testing and Checklist sections; clarify how the change was validated beyond code review and specify if maintainer build/test is required before merge.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: accepting surface_ref and surface aliases in send/read RPCs, matching the core fix described in the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
Sources/TerminalController.swift (1)

5666-5674: Extract the repeated surface-key fallback into a helper.

This exact 7-line block (rawSurface chain + v2UUIDAny resolve + not_found guard + focusedPanelId fallback) is duplicated verbatim across v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, and v2SurfaceReadText. Any future change (e.g., switching to invalid_params, enriching error data, adding a fourth alias, logging) has to be mirrored in four places and is easy to drift.

Consider extracting once, e.g.:

♻️ Suggested helper
/// Resolves an optional surface identifier from any of "surface_id", "surface_ref", "surface".
/// Returns:
///  - .some(.some(uuid)) if a key was present and resolved
///  - .some(.none)       if a key was present but could not be resolved (caller should error)
///  - .none              if no key was provided (caller should fall back to focused panel)
private func v2ResolveOptionalSurfaceId(_ params: [String: Any]) -> UUID?? {
    guard let raw = params["surface_id"] ?? params["surface_ref"] ?? params["surface"] else {
        return .none
    }
    return .some(v2UUIDAny(raw))
}

Call sites collapse to:

let surfaceId: UUID?
switch v2ResolveOptionalSurfaceId(params) {
case .some(.some(let id)): surfaceId = id
case .some(.none):
    result = .err(code: "not_found",
                  message: "Surface not found for the given surface_id/surface_ref/surface",
                  data: nil)
    return
case .none:
    // existing focusedPanelId fallback
    ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 5666 - 5674, Extract the
repeated seven-line surface-id resolution into a single helper (suggested name
v2ResolveOptionalSurfaceId) that checks params["surface_id"] ??
params["surface_ref"] ?? params["surface"], returns .none if no key was
provided, .some(.none) if a key was present but v2UUIDAny(raw) failed, or
.some(.some(uuid)) when resolved; then update v2SurfaceSendText,
v2SurfaceSendKey, v2SurfaceClearHistory and v2SurfaceReadText to call this
helper, switch on its three outcomes to either set surfaceId, emit the existing
.err(...) with the same message when .some(.none) is returned, or fall back to
focusedPanelId when .none is returned. Ensure you reference and reuse v2UUIDAny
and preserve the exact error code/message and existing focusedPanelId fallback
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TerminalController.swift`:
- Around line 5667-5673: The handler currently returns .err(code: "not_found")
when a provided surface key (rawSurface) is present but v2UUIDAny(rawSurface)
fails to resolve; change this to return .err(code: "invalid_params") when
rawSurface != nil but surfaceId == nil, and include the offending value in the
data payload (e.g. data: ["surface_id": rawSurface]) so clients can recover
programmatically; apply the same change for the three sibling handlers that call
v2UUIDAny for pane/pane_ref/related keys and ensure the error message and code
use "invalid_params" rather than "not_found".

---

Nitpick comments:
In `@Sources/TerminalController.swift`:
- Around line 5666-5674: Extract the repeated seven-line surface-id resolution
into a single helper (suggested name v2ResolveOptionalSurfaceId) that checks
params["surface_id"] ?? params["surface_ref"] ?? params["surface"], returns
.none if no key was provided, .some(.none) if a key was present but
v2UUIDAny(raw) failed, or .some(.some(uuid)) when resolved; then update
v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory and v2SurfaceReadText
to call this helper, switch on its three outcomes to either set surfaceId, emit
the existing .err(...) with the same message when .some(.none) is returned, or
fall back to focusedPanelId when .none is returned. Ensure you reference and
reuse v2UUIDAny and preserve the exact error code/message and existing
focusedPanelId fallback logic.
🪄 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: 7bfe0087-795c-4952-80a2-0f1b6e2b1e6c

📥 Commits

Reviewing files that changed from the base of the PR and between 3af1173 and cf02ce7.

📒 Files selected for processing (1)
  • Sources/TerminalController.swift

Comment on lines +5667 to 5673
let rawSurface = params["surface_id"] ?? params["surface_ref"] ?? params["surface"]
if rawSurface != nil {
surfaceId = v2UUIDAny(rawSurface)
guard surfaceId != nil else {
result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)
result = .err(code: "not_found", message: "Surface not found for the given surface_id/surface_ref/surface", data: nil)
return
}

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 | 🟡 Minor

Consider invalid_params instead of not_found when a surface key is present but unresolvable.

When rawSurface is provided but v2UUIDAny cannot resolve it (malformed UUID / unknown handle ref), the handler returns not_found. Per established convention for v2 endpoints that accept surface_id/pane_id/etc., a present-but-unresolvable key should yield invalid_params rather than not_found — not_found is typically reserved for well-formed IDs that don't match any live surface. This same concern applies to the three sibling handlers at lines 5728-5734, 5771-5777, and 5832-5838.

Also worth noting: the data payload is nil, so clients cannot programmatically recover which of the three keys was supplied or what value failed to resolve. Consider including data: ["surface_id": <rawSurface string>] (or similar) to keep parity with the existing error-data conventions exercised by cmuxTests/TerminalControllerSocketSecurityTests.swift.

Based on learnings: "TerminalController.v2UUID(params:key) resolves either a raw UUID string or a ref-style handle ... when the key is present but unresolvable, prefer returning invalid_params instead of falling back."

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

In `@Sources/TerminalController.swift` around lines 5667 - 5673, The handler
currently returns .err(code: "not_found") when a provided surface key
(rawSurface) is present but v2UUIDAny(rawSurface) fails to resolve; change this
to return .err(code: "invalid_params") when rawSurface != nil but surfaceId ==
nil, and include the offending value in the data payload (e.g. data:
["surface_id": rawSurface]) so clients can recover programmatically; apply the
same change for the three sibling handlers that call v2UUIDAny for
pane/pane_ref/related keys and ensure the error message and code use
"invalid_params" rather than "not_found".

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 1 file

@greptile-apps

greptile-apps Bot commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an asymmetric input/output schema bug in four surface RPC handlers (v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText) by adding a fallback chain that accepts surface_ref and surface params in addition to surface_id.

  • The fallback chain uses params[\"surface_id\"] ?? params[\"surface_ref\"] ?? params[\"surface\"] on raw Any? values. Swift's ?? only triggers on Optional.none, not on Optional.some(NSNull()). If a caller passes {\"surface_id\": null, \"surface_ref\": \"surface:12\"}, rawSurface binds to NSNull(), v2UUIDAny returns nil, and all four handlers return not_found — never attempting surface_ref. The codebase already uses the correct alternative pattern: v2UUIDAny(x) ?? v2UUIDAny(y) (line 2707, 2765).

Confidence Score: 4/5

Safe to merge for the common case, but has a regression when any surface key is present with an explicit JSON null value.

One P1 finding: the raw-Any ?? chain does not fall through NSNull(), which is a regression compared to both pre-PR behavior (where null surface_id fell back to focused pane) and the established codebase pattern. The fix is trivially the existing idiom from line 2707.

Sources/TerminalController.swift — specifically the four identical surface-resolution blocks.

Important Files Changed

Filename Overview
Sources/TerminalController.swift Adds surface_ref/surface fallback chain to four RPC handlers (v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText); correct intent but the ?? chaining on raw Any? values breaks when any key is present with a JSON null value (NSNull), causing an unexpected not_found error instead of falling through or using the focused surface.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[RPC params received] --> B{params contains surface_id?}
    B -- "present (any value)" --> C["rawSurface = params[surface_id]"]
    B -- "absent" --> D{params contains surface_ref?}
    D -- "present" --> E["rawSurface = params[surface_ref]"]
    D -- "absent" --> F{params contains surface?}
    F -- "present" --> G["rawSurface = params[surface]"]
    F -- "absent" --> H[rawSurface = nil]
    C --> I{rawSurface != nil?}
    E --> I
    G --> I
    H --> J[surfaceId = ws.focusedPanelId]
    I -- "yes" --> K["v2UUIDAny(rawSurface)"]
    I -- "no" --> J
    K -- "resolved UUID" --> L[surfaceId = UUID]
    K -- "nil (NSNull trap!)" --> M["err: not_found — surface_ref never tried"]
    L --> N[ws.terminalPanel lookup]
    J --> N
Loading

Comments Outside Diff (1)

  1. Sources/TerminalController.swift, line 5667-5676 (link)

    P1 NSNull short-circuits the fallback chain

    When an RPC caller sends {"surface_id": null, "surface_ref": "surface:12", ...}, JSONSerialization represents the null as NSNull(). Because Swift's ?? only fires on Optional.none (not on .some(NSNull())), rawSurface is set to NSNull(), v2UUIDAny returns nil, and the handler errors out with "not_found" — never trying surface_ref at all.

    The pre-existing codebase already uses the correct pattern for chaining identifier lookups (e.g. line 2707: v2UUIDAny(callerObj["surface_id"]) ?? v2UUIDAny(callerObj["tab_id"])). Matching that style here avoids the NSNull trap and is more idiomatic:

    The same applies to the identical block in v2SurfaceSendKey (line 5728), v2SurfaceClearHistory (line 5771), and v2SurfaceReadText (line 5832).

Reviews (1): Last reviewed commit: "fix(surface): accept surface_ref and sur..." | Re-trigger Greptile

@EtanHey

EtanHey commented Apr 23, 2026

Copy link
Copy Markdown

Reviewed in the context of a session-mining sweep on 2026-04-23 where we surfaced the exact symptom this PR fixes: cmux send --surface surface:N being silently retargeted to the caller's own pane when the ref wasn't a UUID.

The patch looks right to me. A few observations from the code-review side:

  • Pattern is clean. Four identical handler patches against Sources/TerminalController.swift (v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText). Each replaces the params["surface_id"] != nil check with a surface_id ?? surface_ref ?? surface fallback chain and routes through the existing v2UUIDAny helper (already used elsewhere in the file, e.g. ~line 3136). No new helpers, no new dependencies — the blast radius is contained.
  • Error message improvement is a win on its own. "Surface not found for the given surface_id/surface_ref/surface" is self-describing; callers hitting the new error path will know all three accepted keys.
  • Backwards-compat is solid. Existing surface_id callers still hit the chain first and see the same behavior. Response shape unchanged. No change to tab_id handling.
  • Symmetry with the response shape. Closing the input/output asymmetry (response always returns both surface_id and surface_ref, inputs now accept both plus the short surface key) is the architectural payoff — it eliminates a whole class of silent-retargeting bugs, not just this one.

What I'd like to see before merge (nothing blocking, just flagging):

  1. Xcode build verification. Author notes they don't have a local Xcode toolchain; a maintainer run would close the loop. The change is mechanical so confidence is high, but Swift's type system could catch something subtle (e.g. v2UUIDAny signature specifics on optional handling that differ from v2UUID's two-arg form).
  2. Test coverage. The PR body mentions tmux_split_ref_test.go-style coverage as a follow-up. For a fix to an asymmetric-targeting bug where the old behavior was silent (not an error), a regression test is high-value — without one, a future refactor could re-introduce the fallback-to-focusedPanelId default and CI would stay green. Happy to stub the test shape on a follow-up PR if that helps.
  3. Consider also covering read paths. v2SurfaceReadText is in scope here (good). If there are related read/inspect RPCs (surface.info, enumerate, etc.) with the same params["surface_id"]-only check, worth grep'ing before merge so we don't leave half the surface area silently wrong.

From the cmuxlayer side (the TypeScript MCP wrapper that sits above this), this unblocks send_to / send_to_agent routing to work correctly across workspaces without the wrapper having to pre-resolve refs to UUIDs. Strong +1 on shipping.

— Context: session-mining 2026-04-23 surfaced three bugs in this class; this PR covers Bug 1 directly.

@teamleaderleo teamleaderleo added S2: major A crash, hang, lost state, broken connection, or a regression on a path people use area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants