Skip to content

Share one terminal workspace with browser guests - #8468

Closed
lawrencecchen wants to merge 72 commits into
mainfrom
feat-mpshare-do
Closed

lawrencecchen wants to merge 72 commits into
mainfrom
feat-mpshare-do

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Share Workspace… copies an invitation URL and opens a floating host panel.
  • Signed-in guests request access. The host can allow editing, grant view-only access, deny, change roles, or kick.
  • The browser preserves the focused workspace's exact split tree. Terminal leaves stream live grids; browser, agent, and other leaves remain inert placeholders.
  • Editors can type into current terminal leaves. Viewers cannot send input.
  • Participants get presence, cursors, session chat, and guest-created cursor bubbles.
  • V1 shares one focused workspace. Browser control/video, composer co-editing, Follow, multi-workspace navigation, and mobile clients are deferred.

Boundaries

  • One Cloudflare Durable Object owns session admission, bounded fan-out, reconnect grace, and a token-safe ended-session tombstone.
  • Short-lived Ed25519 tokens bind every connection to the share code and verified Stack identity.
  • The Mac revalidates the current role, workspace, layout, and terminal target before applying guest input.
  • ACK-backed delivery credit, strict frame ceilings, per-socket and per-room rate budgets, and bounded persisted state isolate slow or abusive guests.
  • Native WebSockets use an Authorization header. Browser query tokens stay out of automatic Worker logs. Share pages disable analytics, referrers, and indexing.
  • The release feature flag remains off until production keys, rate limits, and endpoints are configured.

Verification

  • Share Worker: 127 tests, TypeScript checks, and production/development Wrangler dry-runs.
  • Web: 966 tests, 122 environment-dependent skips, TypeScript checks, scoped ESLint, and optimized Next.js build.
  • Swift package: 42 Swift Testing tests under Swift 6.
  • macOS integration: 5 focused host-presentation and WebSocket-request tests.
  • CI now runs CmuxWorkspaceShare package tests and validates the Share Worker on pull requests.

Design and operation details: multiplayer share guide and wire protocol.

Dictionary: A Durable Object is the single stateful relay instance for one invitation code. Delivery credit is the bounded unacknowledged data reserved for one socket. A dry-run bundles and validates a Worker deployment without publishing it.

Summary by CodeRabbit

  • New Features
    • Added live multiplayer workspace sharing with share links, presence, participant cursors, workspace selection, and real-time chat.
    • Hosts can approve, deny, remove, and assign editor or viewer access to participants.
    • Editors can interact with shared terminal, browser, and agent panes where permitted.
    • Added share controls to the command palette, including start and stop sharing.
    • Added localized English and Japanese text for sharing workflows and status messages.
  • Documentation
    • Added guides covering sharing, access roles, security, and session behavior.

…kspace sharing

One ShareSession DO per share code fans host streams (render-grid frames,
layout, cursors, chat) out to guest browsers and relays editor-role guest
input back to the host. Pure session core + offline Ed25519 token verify,
mirroring workers/presence conventions (bun test, hibernation WebSockets,
worker-side auth boundary).
Guest path for multiplayer sharing: Stack-gated share page with a
cmux-shaped viewer (workspace sidebar with presence dots, split-pane
canvas terminals painted from render-grid frames, remote kite cursors,
cursor chat bubbles, floating session chat, guest typing for editors),
plus /api/share/sessions create + guest-token routes minting Ed25519
JWTs the share worker verifies offline (mirrors the relay token system).
… input forwarding

DO relays compose/pointer/webkey (editor-gated, shared-workspace-scoped)
to the host; host focus feeds presence for follow-the-host. Web viewer
decodes H.264/WebP pixel frames for browser/agent panes, adds the shared
composer strip with remote carets, and forwards pointer/keyboard into
interactive browser panes.
New Sources/Share/ implements the host side of the cmux share protocol
(workers/share/src/protocol.ts): session create + host-token refresh via
the web API, a reconnecting WebSocket to the session Durable Object,
bonsplit layout serialization, subscriber-driven render-grid streaming,
guest input (editor-role gated), chat window with inline access-request
moderation, and guest cursor overlays with host cursor broadcast.
Palette commands palette.shareWorkspaces / palette.stopSharing wire into
ShareSessionController.shared; strings localized en+ja.
…input

Browser and agent panes stream to guests as binary pixel frames (kind 0x02):
SharePixelStreamer paces up to 10 fps per subscribed pane, skips unchanged
frames via a 32x32 downsample digest, and encodes VideoToolbox H.264 Annex B
(SPS/PPS inline on keyframes, forced keyframe per new subscriber/resync)
with a JPEG-still fallback at ~3 fps; capped at 4 concurrent panes, newest
subscription wins. ShareComposerSync makes the host authoritative for agent
composer text: guest compose ops are positionally rebased (codepoint
indices, rule documented in the file), applied, pushed into the pane via a
new composer.setText bridge event, and re-broadcast as compose-state; host
typing reaches the sync through a new composer.changed bridge method (both
ends added to webviews/src/agent-session and the committed webviews-app
bundle regenerated). ShareBrowserInputApplier delivers editor-role guest
pointer and key events to BrowserPanel webviews as synthesized NSEvents
(wheel via JS on the scrollable under the pointer). guest-sub now routes by
pane kind.
Found in live preflight: a rejoining guest's first frame was a delta
(blank pane in the viewer) because the host never learned the previous
subscriber left, so its per-pane count stayed stale and the
new-subscriber full frame was skipped.
The subscription key separator was an accidental literal NUL byte; the
disconnect fix split on a space and never matched, so dropped-subscriber
counts still failed to reach the host. Name the separator, split on it,
and keep the regression tests.
@socket-security

socket-security Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​xterm/​addon-unicode11@​0.9.01001007595100
Addednpm/​@​xterm/​addon-unicode-graphemes@​0.4.0761007696100
Addednpm/​wrangler@​4.110.0981009296100
Addednpm/​@​cloudflare/​workers-types@​4.20260702.110010010099100

View full report

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

Changes

Multiplayer workspace sharing is added across the native app, web viewer, authenticated session APIs, Cloudflare Durable Object worker, streaming protocol, chat, cursors, terminal input, composer synchronization, localization, documentation, and CI workflows.

Multiplayer workspace sharing

Layer / File(s) Summary
Protocol, authorization, and Durable Object session
workers/share/src/*, workers/share/test/*
Defines the share protocol, Ed25519 token validation, WebSocket routing, Durable Object persistence, roles, approvals, subscriptions, binary forwarding, chat, cursors, and lifecycle handling.
Web session APIs and token issuance
web/app/api/share/*, web/services/share/*, web/tests/share-token-route.test.ts
Creates share codes and host or guest tokens, validates authentication and configuration, and tests route responses and token claims.
Native host session and streaming
Sources/Share/*, Sources/ContentView.swift, cmux.xcodeproj/project.pbxproj
Adds host session orchestration, reconnecting sockets, layout serialization, grid streaming, cursor overlays, chat controls, workspace sharing commands, and Xcode integration.
Browser share viewer
web/app/[locale]/share/[code]/*, web/tests/share-viewer-logic.test.ts
Adds the authenticated share route, browser session client, streamed pane rendering, terminal grid painting, keyboard input, cursors, chat, participant controls, and viewer tests.
Composer synchronization
Sources/Panels/*, webviews/src/agent-session/*, Resources/agent-session-solid/*
Adds native-to-web composer updates and web-to-native change notifications with deduplication.
Integration and support
Resources/Localizable.xcstrings, web/messages/*, docs/*, .github/workflows/*, workers/share/*
Adds localized strings, feature documentation, worker configuration, deployment checks, generated asset updates, and CI coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NativeHost
  participant WebAPI
  participant ShareWorker
  participant ShareSession
  participant BrowserViewer

  NativeHost->>WebAPI: create share session
  WebAPI-->>NativeHost: share code and host token
  NativeHost->>ShareWorker: open authenticated WebSocket
  ShareWorker->>ShareSession: route verified host connection
  BrowserViewer->>WebAPI: request guest token
  WebAPI-->>BrowserViewer: guest token and WebSocket URL
  BrowserViewer->>ShareWorker: open guest WebSocket
  ShareWorker->>ShareSession: route verified guest connection
  NativeHost->>ShareSession: send layout and binary frames
  ShareSession-->>BrowserViewer: session state and streamed frames
  BrowserViewer->>ShareSession: send chat, cursor, or editor input
  ShareSession-->>NativeHost: relay authorized guest actions
Loading

Suggested reviewers: austinywang, azooz2003-bit


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (7 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production Swift adds ContinuousClock().sleep(for:) in ShareCursorOverlayController and throttle(..., scheduler: RunLoop.main) in ShareSessionController, both banned timing-based sync. Replace the sleep/throttle with explicit state-change callbacks or a cancellation-aware timer/async signal; bubble expiry and layout sync should not rely on Task.sleep or RunLoop throttling.
Cmux Algorithmic Complexity ❌ Error web/app/[locale]/share/[code]/terminal-grid.ts:79 scans cleared_rows inside the row_spans loop, creating a hot-path O(spans×cleared_rows) delta apply. Precompute a Set of cleared rows (or clear rows first and skip the per-span includes) so delta apply stays linear in spans.
Cmux Swift Concurrency ❌ Error ShareSessionController adds Combine publishers/AnyCancellable for share-state observation despite using @Observable, which the modernization rule flags as legacy async flow. Replace the new Combine subscription graph with Observation/async-driven tracking or explicit async change streams; avoid AnyCancellable/Publishers.MergeMany in the share controller.
Cmux Swift Package Boundaries ❌ Error Fail: new core share runtime (ShareSessionAPI, ShareSocket, ShareGridStreamer, ShareLayoutSerializer) lives in app-target Sources/Share/, while CmuxWorkspaceShare already exists for reu... Move the reusable share runtime into Packages/macOS/CmuxWorkspaceShare (or a new CmuxWorkspaceShareRuntime) and keep Sources/Share for UI/AppKit glue only.
Cmux Swiftpm Lockfiles ❌ Error cmux.xcodeproj/project.pbxproj adds a SwiftPM package ref for CmuxWorkspaceShare, but there’s no matching root cmux.xcodeproj/.../Package.resolved diff. Add the root Xcode cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved change for the new package reference, or avoid the package-ref edit.
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/Share/ShareSessionController.swift adds DEBUG-only bindOwnerForWindowLifecycleTesting, and cmuxTests/ClosedMainWindowRoutingTests.swift calls it, creating a test seam in production source. Move the helper into the test target (or a debug-only file/folder) and expose only the needed state via @testable internal APIs.
Cmux No Ambient Global State ❌ Error Sources/Share/ShareLayoutSerializer.swift:10 adds a caseless enum namespace with only static helpers, which this rule forbids. Move layout serialization onto an injectable serializer object or onto the owning session/workspace type, instead of a static-helper namespace.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers summary and testing, but it omits required template sections like Demo Video, Review Trigger, and Checklist. Add the missing template sections: Demo Video, Review Trigger block, and Checklist, and format Summary and Testing to match the repository template.
✅ Passed checks (16 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New Swift types are either MainActor UI coordinators, actors, or documented @unchecked Sendable helpers; no new actor-isolation mistake is evident.
Cmux Browser Automation Off-Main ✅ Passed The diff doesn’t touch the rule’s target browser-automation routing files; changed Bonsplit/UI files contain no browser.* wait/callback routing.
Cmux Expensive Synchronous Load ✅ Passed New share UI/controller code only starts async tasks and decodes small websocket frames; no RestorableAgentSessionIndex.load() or transcript/JSONL scans were added to main-actor paths.
Cmux Cache Substitution Correctness ✅ Passed The only caches in snapshot/persist paths are event-driven or explicitly documented (theme invalidation, pending access requests across session snapshots); no stale cold-cache swap found.
Cmux No Hacky Sleeps ✅ Passed Only timers are reconnect/backoff, cursor throttling, and bubble expiry in ShareClient plus probe-script waits; the former are product behavior and the latter are explicit verification scaffolding.
Cmux Swift @Concurrent ✅ Passed No changed Swift file adds a missing/invalid @concurrent; network-heavy work is isolated in actors, and UI entry points hop via Task @MainActor or actor calls.
Cmux Swift Logging ✅ Passed The new Swift logs use nonisolated private Logger constants, stay generic, and the only extra diagnostic output is #if DEBUG cmuxDebugLog; no print/NSLog or sensitive leakage found.
Cmux User-Facing Error Privacy ✅ Passed UI/API errors are generic and sanitized; route bodies return codes like invalid_code/share_not_configured, with no secrets, tokens, or vendor names exposed.
Cmux Full Internationalization ✅ Passed Share UI strings use String(localized:), and both web/messages and Localizable.xcstrings provide translated entries for all supported locales.
Cmux Swiftui State Layout ✅ Passed New share SwiftUI state uses @Observable, rows take snapshots+closures, and no new GeometryReader/@Published/@StateObject or render-time state writes appear in touched views.
Cmux Architecture Rethink ✅ Passed Share state has one clear owner (ShareSessionController/AppDelegate); the added observers/timers are isolated bridge code, not a split lifecycle or symptom patch.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The new NSPanel uses cmux.shareSession, and Sources/cmuxApp.swift adds it to cmuxAuxiliaryWindowIdentifiers; scripts/lint_auxiliary_window_close_shortcuts.py enforces it.
Cmux Source Artifacts ✅ Passed Only changed top-level paths are intentional submodule gitlinks (ghostty, vendor/bonsplit) listed in .gitmodules; no logs, caches, temp, or build-output artifacts were added.
Title check ✅ Passed The title is specific and aligned with the main change: multiplayer workspace sharing for browser guests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-mpshare-do

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 017dbb406a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread workers/share/src/do.ts
Comment on lines +49 to +52
let core = await this.ensureCore();
if (!core) {
if (!isHost) return new Response("no such session", { status: 404 });
const persisted = ShareSessionCore.create(code, { user, email }, Date.now());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind host-claim tokens before creating sessions

When no persisted session exists, this creates it for any connection carrying a host claim. The /api/share/sessions/[code]/token endpoint currently mints host:true for any authenticated caller that posts {"host":true}, and the Mac copies the share URL before socket.start(), so a guest who receives the code and races the host can claim the DO as themselves; the real creator is then rejected by the core.persisted.host.user !== user check. Bind host tokens to the creator or make only the create-time host token able to materialize the DO.

Useful? React with 👍 / 👎.

Comment thread workers/share/src/session.ts Outdated
Comment on lines +444 to +446
for (const c of this.conns.values()) {
if (!c.isHost && c.active && c.subs.has(key)) {
effects.push({ kind: "sendBinary", to: c.id, data });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop routing frames after a workspace is unshared

When the host removes a workspace from shared, existing conn.subs entries for panes in that workspace are not cleared, and this routing path only checks the subscription key. Because the browser's shared handler doesn't send unsubs either, any approved guest already viewing that workspace continues receiving live grid/pixel frames after it was unshared until they disconnect or switch, so unshare does not revoke access.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds multiplayer workspace sharing to cmux: a Cloudflare Durable Object (workers/share) fans host streams to guests and relays input back, a new macOS host orchestrates streaming and moderation, and a Next.js viewer at /share/[code] renders terminals as render-grid canvases and browser panes as H.264/JPEG pixel frames with cursors, chat, and co-editing.

  • Worker & auth: ShareSession DO implements protocol v1 with role-based access, per-session Ed25519 JWT verification (offline, no external calls on the hot path), host-grace alarm, and subscription-driven fan-out. The create claim gates session materialisation so reconnect tokens cannot squat codes.
  • macOS host: ShareSessionController orchestrates ShareSocket, ShareGridStreamer, SharePixelStreamer, ShareComposerSync, and ShareBrowserInputApplier; all guest input is re-validated locally before being applied.
  • Web viewer: ShareClient (framework-free) manages WebSocket lifecycle, reconnect backoff, binary frame dispatch, and three narrow stores; React subscribes only to what it re-renders. Localization covers all 36 share.* keys in both en.json and ja.json and all 27 Swift string-catalog entries include Japanese translations.

Confidence Score: 4/5

Safe to merge with one defect in the web viewer reconnect logic worth fixing before wide rollout.

The auth chain, DO session core, and macOS host input validation are solid. The one current defect is in share-connection.ts: any non-OK HTTP response from the Vercel token endpoint (including transient 5xx) permanently sets the viewer to unavailable with no self-healing, while only thrown network exceptions trigger the reconnect backoff path. Additionally, several macOS-side issues from previous reviews (ObservableObject broad invalidation, singleton ambient-state in closures, Task.sleep backoff) remain open.

web/app/[locale]/share/[code]/share-connection.ts (token fetch error handling), Sources/Share/ShareSessionController.swift and Sources/Share/ShareSocket.swift (see prior review threads)

Important Files Changed

Filename Overview
web/app/[locale]/share/[code]/share-connection.ts Full ShareClient implementation: WebSocket lifecycle, reconnect, frame dispatch, and cursor throttle. Non-2xx token fetch responses permanently halt reconnection instead of retrying on transient 5xx.
workers/share/src/session.ts Pure ShareSessionCore state machine: connection lifecycle, roles/approvals, chat, cursors, subscriptions, and host-grace alarm. Well-structured with clear effect model.
workers/share/src/do.ts Thin Durable Object wiring over ShareSessionCore: accepts WebSockets with hibernation, persists session state, runs alarms, and applies effects. Correctly gates session creation to create-endpoint tokens only.
workers/share/src/jwt.ts Offline Ed25519 JWT verification without any external calls. Module-level key cache is intentional and safe for the Workers isolate model.
web/services/share/token.ts Token minting: generates unguessable share codes, signs Ed25519 JWTs. create flag correctly appears only on session-create tokens.
Sources/Share/ShareSessionController.swift Host-side session orchestrator. ObservableObject broad-invalidation, static let shared singleton, and optimistic access-request resolution issues noted in previous reviews.
Sources/Share/ShareSocket.swift URLSessionWebSocketTask wrapper with exponential backoff. Task.sleep backoff in reconnect loop flagged in previous reviews.
Sources/Share/ShareBrowserInputApplier.swift Synthesizes NSEvent mouse/keyboard events from validated guest input. key/code strings are JSON-escaped before JS interpolation, preventing injection.
Sources/Share/ShareChatView.swift Chat panel SwiftUI view. Sub-rows reach into ShareSessionController.shared from closures instead of the injected controller, noted in previous reviews.
web/app/[locale]/share/[code]/share-panes.tsx Recursive layout tree renderer for terminal canvases, pixel panes, and interactive browser overlays. Keyboard focus yields correctly on Escape.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Mac as macOS Host
    participant WebAPI as Web API (Vercel)
    participant Worker as Share Worker (CF)
    participant DO as ShareSession DO
    participant Guest as Guest Browser

    Mac->>WebAPI: POST /api/share/sessions (native auth)
    WebAPI-->>Mac: "{ code, token(host+create), wsUrl, shareUrl }"
    Mac->>Worker: "WS connect ?token=JWT"
    Worker->>Worker: verifyShareToken offline
    Worker->>DO: "fetch (x-share-host=1, x-share-create=1)"
    DO->>DO: ShareSessionCore.create() + persist
    DO-->>Mac: session-state snapshot
    Guest->>WebAPI: "POST /api/share/sessions/{code}/token (cookie auth)"
    WebAPI-->>Guest: "{ token(guest), wsUrl }"
    Guest->>Worker: "WS connect ?token=guest JWT"
    Worker->>DO: "fetch (x-share-host=0)"
    DO-->>Guest: access-pending
    DO-->>Mac: access-request
    Mac->>DO: "approve { user, role }"
    DO-->>Guest: session-state snapshot
    Mac->>DO: binary grid/pixel frame
    DO->>DO: routeBinary to subscribed guests
    DO-->>Guest: binary frame fan-out
    Guest->>DO: "input { ws, pane, data }"
    DO->>DO: validate editor role + shared workspace
    DO-->>Mac: guest-input
    Mac->>Mac: applyGuestInput (re-validate locally)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Mac as macOS Host
    participant WebAPI as Web API (Vercel)
    participant Worker as Share Worker (CF)
    participant DO as ShareSession DO
    participant Guest as Guest Browser

    Mac->>WebAPI: POST /api/share/sessions (native auth)
    WebAPI-->>Mac: "{ code, token(host+create), wsUrl, shareUrl }"
    Mac->>Worker: "WS connect ?token=JWT"
    Worker->>Worker: verifyShareToken offline
    Worker->>DO: "fetch (x-share-host=1, x-share-create=1)"
    DO->>DO: ShareSessionCore.create() + persist
    DO-->>Mac: session-state snapshot
    Guest->>WebAPI: "POST /api/share/sessions/{code}/token (cookie auth)"
    WebAPI-->>Guest: "{ token(guest), wsUrl }"
    Guest->>Worker: "WS connect ?token=guest JWT"
    Worker->>DO: "fetch (x-share-host=0)"
    DO-->>Guest: access-pending
    DO-->>Mac: access-request
    Mac->>DO: "approve { user, role }"
    DO-->>Guest: session-state snapshot
    Mac->>DO: binary grid/pixel frame
    DO->>DO: routeBinary to subscribed guests
    DO-->>Guest: binary frame fan-out
    Guest->>DO: "input { ws, pane, data }"
    DO->>DO: validate editor role + shared workspace
    DO-->>Mac: guest-input
    Mac->>Mac: applyGuestInput (re-validate locally)
Loading

Reviews (4): Last reviewed commit: "Extract spliceDiff into a pure module" | Re-trigger Greptile

Comment on lines +503 to +508
cursorOverlay.isWorkspaceVisible = { [weak self] ws in
guard let self, let tabManager = self.tabManager,
let wsUUID = UUID(uuidString: ws) else { return false }
return tabManager.selectedTabId == wsUUID
}
cursorOverlay.sendHostCursor = { [weak self] pos in

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 Raw error detail exposed to users

String(describing: error) on a thrown network/HTTP error will produce strings like "NSURLErrorDomain Code=-1009 …" or stack-style descriptions that name internal types and domains. Since lastErrorText is @Published and shown in the UI, this violates the user-facing error message policy. The same pattern at line 812 (lastErrorText = "\(code): \(message)") directly exposes internal worker error key strings (e.g. share_not_configured: …) to users.

Both sites should map the error to a user-friendly cmux-terms message, keeping the raw code/detail in a debug log only.

Comment on lines +430 to +432

private func rebuildFeed(chat: [ShareChatMessage]) {
var items = chat.map(feedItem(for:))

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 ObservableObject causes broad SwiftUI invalidation

ShareSessionController is marked @MainActor final class and conforms to ObservableObject with several @Published properties (participants, feed, workspaceRows, status, …). With ObservableObject, any single @Published change — including high-frequency cursor-driven participants updates — invalidates the entire ShareChatView body and all its child views. Swift's @Observable macro would give property-granular invalidation so only views that actually read the changed property rerender.

Comment on lines +431 to +432
private func rebuildFeed(chat: [ShareChatMessage]) {
var items = chat.map(feedItem(for:))

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 New static let shared singleton for runtime state

ShareSessionController.shared is a new static let shared singleton for orchestrating the entire sharing session lifecycle. ShareChatView correctly receives the controller as @ObservedObject var controller, but the value-type sub-views (ShareWorkspaceToggleRow, ShareFeedRow, ShareParticipantRow) ignore the injected reference and reach directly into ShareSessionController.shared from their action closures, creating a second ambient access path alongside the injected one and making the views non-testable in isolation.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/multiplayer-share.md`:
- Around line 33-36: Add the text language identifier to the fenced ASCII
diagram code block in the multiplayer sharing documentation, changing the
opening fence to specify text while preserving the diagram content.

In `@Sources/Share/ContentView`+ShareCommandPalette.swift:
- Around line 16-35: Add catalog translations for command.shareWorkspaces.title,
command.stopSharing.title, and command.share.subtitle in
Resources/Localizable.xcstrings for every supported locale beyond en and ja.
Preserve the existing localization keys and provide appropriate localized values
for each shipped language.

In `@Sources/Share/ShareSessionController.swift`:
- Around line 100-110: Update the error handling in
Sources/Share/ShareSessionController.swift lines 100-110 to log the raw error
separately and assign a localized product-level message via
String(localized:defaultValue:). Also update lines 413-414 to map known error
codes to localized user-facing copy, while logging the raw code and message
instead of exposing them through lastErrorText.
- Around line 33-49: Update ShareSessionController to use the Observation
framework’s `@Observable` model instead of ObservableObject and `@Published` for its
state properties. Preserve the existing status, shareUrl, code, participants,
feed, workspaceRows, and lastErrorText access behavior, and keep Combine
throttle usage only at publisher boundaries.
- Around line 82-94: Add entries for share.error.noWorkspaceContext and
share.error.notSignedIn to every supported locale in
Resources/Localizable.xcstrings, preserving the existing English and Japanese
translations and matching the catalog’s established localization structure.

In `@web/app/`[locale]/share/[code]/pixel-pane.ts:
- Around line 19-20: Replace the unbounded stillChain queue with
single-in-flight decode behavior so a new frame supersedes any pending stale
frame instead of extending the promise chain. Update the still-frame decode and
setImage flow around push/close to track closed state, prevent post-close
setImage calls, and ensure pending work is discarded when close() runs.

In `@web/app/`[locale]/share/[code]/share-panes.tsx:
- Around line 239-247: Update the interactive browser pane’s onKeyDown handler
to detect Escape and blur the focused tabIndex={0} overlay, returning before
preventDefault and web-key forwarding. Preserve the existing meta-key shortcut
behavior and forwarding for all other keys.

In `@web/app/api/share/sessions/route.ts`:
- Around line 46-70: Update handleShareSessionCreate to enforce a rate limit
before generating the session code or minting the token, using the authenticated
user or request IP as the limiter key and the existing dependency/configuration
pattern for shared state. Return the established rate-limit response when the
limit is exceeded while preserving the current unauthorized and
share_not_configured responses.

In `@workers/share/src/index.ts`:
- Around line 41-63: Remove the incoming Authorization header from the forwarded
headers in the request-building flow after token verification, while preserving
the verified x-share-* identity headers and existing URL behavior. Update the
headers handling around the forward request before calling stub.fetch so the
bearer token never reaches the Durable Object.

In `@workers/share/src/session.ts`:
- Around line 418-422: Bound subscription growth in the "sub" branch of the
session message handler: validate msg.pane against the workspace’s known layout
panes or enforce a fixed maximum on conn.subs before adding subKey(msg.ws,
msg.pane). Ignore or reject new subscriptions beyond the bound while preserving
existing subscriptions and subCountChanged behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21317dc2-2a72-4040-8192-8f8c8e30757c

📥 Commits

Reviewing files that changed from the base of the PR and between af987b6 and 017dbb4.

⛔ Files ignored due to path filters (1)
  • workers/share/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (70)
  • .github/workflows/share.yml
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/chunks/agentSessionSurface.mjs
  • Sources/ContentView.swift
  • Sources/Panels/AgentSessionWebRendererCoordinator.swift
  • Sources/Panels/AgentSessionWebRendererSession.swift
  • Sources/Share/ContentView+ShareCommandPalette.swift
  • Sources/Share/ShareBrowserInputApplier.swift
  • Sources/Share/ShareChatView.swift
  • Sources/Share/ShareChatWindowController.swift
  • Sources/Share/ShareComposerSync.swift
  • Sources/Share/ShareCursorOverlayController.swift
  • Sources/Share/ShareCursorPointerView.swift
  • Sources/Share/ShareGridStreamer.swift
  • Sources/Share/ShareLayoutSerializer.swift
  • Sources/Share/SharePixelEncoder.swift
  • Sources/Share/SharePixelStreamer.swift
  • Sources/Share/ShareProtocolModels.swift
  • Sources/Share/ShareSessionAPI.swift
  • Sources/Share/ShareSessionController+PaneStreams.swift
  • Sources/Share/ShareSessionController.swift
  • Sources/Share/ShareSocket.swift
  • cmux.xcodeproj/project.pbxproj
  • docs/multiplayer-share.md
  • web/.env.example
  • web/app/[locale]/share/[code]/layout.tsx
  • web/app/[locale]/share/[code]/loading.tsx
  • web/app/[locale]/share/[code]/page.tsx
  • web/app/[locale]/share/[code]/pixel-pane.ts
  • web/app/[locale]/share/[code]/share-chat.tsx
  • web/app/[locale]/share/[code]/share-colors.ts
  • web/app/[locale]/share/[code]/share-connection.ts
  • web/app/[locale]/share/[code]/share-cursors.tsx
  • web/app/[locale]/share/[code]/share-panes.tsx
  • web/app/[locale]/share/[code]/share-protocol.ts
  • web/app/[locale]/share/[code]/share-viewer.tsx
  • web/app/[locale]/share/[code]/shared-composer.tsx
  • web/app/[locale]/share/[code]/terminal-grid.ts
  • web/app/[locale]/share/[code]/terminal-keys.ts
  • web/app/[locale]/share/[code]/use-store.ts
  • web/app/api/share/sessions/[code]/token/route.ts
  • web/app/api/share/sessions/route.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/services/share/token.ts
  • web/tests/share-token-route.test.ts
  • web/tests/share-viewer-logic.test.ts
  • webviews/src/agent-session/react/main.ts
  • webviews/src/agent-session/shared/sessionModel.ts
  • webviews/src/agent-session/shared/types.ts
  • workers/share/.gitignore
  • workers/share/PROTOCOL.md
  • workers/share/README.md
  • workers/share/package.json
  • workers/share/scripts/dev-proof.ts
  • workers/share/scripts/guest-probe.ts
  • workers/share/src/do.ts
  • workers/share/src/index.ts
  • workers/share/src/jwt.ts
  • workers/share/src/protocol.ts
  • workers/share/src/session.ts
  • workers/share/test/browser-input.test.ts
  • workers/share/test/compose.test.ts
  • workers/share/test/jwt.test.ts
  • workers/share/test/protocol.test.ts
  • workers/share/test/session.test.ts
  • workers/share/tsconfig.json
  • workers/share/tsconfig.test.json
  • workers/share/wrangler.dev.toml
  • workers/share/wrangler.toml

Comment thread docs/multiplayer-share.md Outdated
Comment on lines +16 to +35
CommandPaletteCommandContribution(
commandId: "palette.shareWorkspaces",
title: constant(String(
localized: "command.shareWorkspaces.title",
defaultValue: "Share Workspaces…"
)),
subtitle: constant(String(localized: "command.share.subtitle", defaultValue: "Share")),
keywords: ["share", "multiplayer", "collab", "collaborate", "invite", "session", "live"]
),
CommandPaletteCommandContribution(
commandId: "palette.stopSharing",
title: constant(String(
localized: "command.stopSharing.title",
defaultValue: "Stop Sharing"
)),
subtitle: constant(String(localized: "command.share.subtitle", defaultValue: "Share")),
keywords: ["share", "multiplayer", "stop", "end", "session"],
when: { $0.bool(CommandPaletteContextKeys.shareSessionActive) },
enablement: { $0.bool(CommandPaletteContextKeys.shareSessionActive) }
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify each new share command key exists in the string catalog.
fd -t f 'Localizable.xcstrings' Resources | while read -r f; do
  echo "== $f =="
  for k in command.shareWorkspaces.title command.stopSharing.title command.share.subtitle; do
    if jq -e --arg k "$k" '.strings[$k]' "$f" >/dev/null 2>&1; then
      echo "present: $k"
      jq --arg k "$k" '.strings[$k].localizations | keys' "$f"
    else
      echo "MISSING: $k"
    fi
  done
done

Repository: manaflow-ai/cmux

Length of output: 355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat <<'PY' > /tmp/inspect_xcstrings.py
import json, pathlib
p = pathlib.Path("Resources/Localizable.xcstrings")
data = json.loads(p.read_text())
print("catalog localizations:", sorted(data.get("sourceLanguage", "") and [data.get("sourceLanguage")] or []))
# xcstrings schema varies; print top-level keys and any locale-like keys under variations
print("top-level keys:", sorted(data.keys()))
strings = data.get("strings", {})
locales = set()
for k, v in strings.items():
    locs = v.get("localizations", {})
    locales.update(locs.keys())
print("all locales found in strings:", sorted(locales))
for k in ["command.shareWorkspaces.title", "command.stopSharing.title", "command.share.subtitle"]:
    print(f"{k}:")
    v = strings.get(k)
    if not v:
        print("  MISSING")
        continue
    for loc, locv in sorted(v.get("localizations", {}).items()):
        print(f"  {loc}: state={locv.get('state')} variations={list(locv.keys())}")
PY
python3 /tmp/inspect_xcstrings.py

Repository: manaflow-ai/cmux

Length of output: 745


Add translations for the new share command-palette strings

command.shareWorkspaces.title, command.stopSharing.title, and command.share.subtitle only have en and ja entries in Resources/Localizable.xcstrings. Add the remaining locale translations in the catalog so these user-facing strings are covered everywhere the app ships.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Share/ContentView`+ShareCommandPalette.swift around lines 16 - 35,
Add catalog translations for command.shareWorkspaces.title,
command.stopSharing.title, and command.share.subtitle in
Resources/Localizable.xcstrings for every supported locale beyond en and ja.
Preserve the existing localization keys and provide appropriate localized values
for each shipped language.

Source: Path instructions

Comment on lines +33 to +49
final class ShareSessionController: ObservableObject {
static let shared = ShareSessionController()

enum Status: Equatable {
case idle
case starting
case active
case reconnecting
}

@Published private(set) var status: Status = .idle
@Published private(set) var shareUrl: String?
@Published private(set) var code: String?
@Published private(set) var participants: [ShareParticipant] = []
@Published private(set) var feed: [ShareFeedItem] = []
@Published private(set) var workspaceRows: [ShareWorkspaceRow] = []
@Published private(set) var lastErrorText: String?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Prefer @Observable over ObservableObject/@Published for this new state.

New cmux-owned UI state should use @Observable rather than introducing an ObservableObject with @Published properties; the existing Combine throttle pipelines can remain at the publisher boundaries.

As per coding guidelines ("Avoid introducing Combine constructs such as ObservableObject, @Published ... for app state ... prefer @Observable").

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 33-33: Classes should have an explicit deinit method

(required_deinit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Share/ShareSessionController.swift` around lines 33 - 49, Update
ShareSessionController to use the Observation framework’s `@Observable` model
instead of ObservableObject and `@Published` for its state properties. Preserve
the existing status, shareUrl, code, participants, feed, workspaceRows, and
lastErrorText access behavior, and keep Combine throttle usage only at publisher
boundaries.

Comment on lines +82 to +94
lastErrorText = String(
localized: "share.error.noWorkspaceContext",
defaultValue: "Sharing is unavailable: no workspace window is open."
)
return
}
guard let coordinator = AppDelegate.shared?.auth?.coordinator else {
lastErrorText = String(
localized: "share.error.notSignedIn",
defaultValue: "Sign in to cmux to share workspaces."
)
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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify new share.* localization keys exist in the catalog and enumerate locales.
fd -a 'Localizable.xcstrings' Resources | while read -r f; do
  echo "== $f =="
  echo "-- share.error keys present --"
  jq -r '.strings | keys[] | select(startswith("share."))' "$f" 2>/dev/null | rg 'share\.error' || echo "MISSING share.error.* keys"
  echo "-- locales referenced anywhere in catalog --"
  jq -r '[.strings[].localizations // {} | keys[]] | unique[]' "$f" 2>/dev/null
  echo "-- locales present for share.error.noWorkspaceContext --"
  jq -r '.strings["share.error.noWorkspaceContext"].localizations // {} | keys[]' "$f" 2>/dev/null || echo "key absent"
done

Repository: manaflow-ai/cmux

Length of output: 478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

catalog="Resources/Localizable.xcstrings"

python3 - <<'PY'
import json
from pathlib import Path

catalog = Path("Resources/Localizable.xcstrings")
data = json.loads(catalog.read_text())

keys = [k for k in data.get("strings", {}).keys() if k.startswith("share.error.")]
print(f"catalog: {catalog}")
print("supported locales:")
all_locales = sorted({loc for entry in data["strings"].values() for loc in (entry.get("localizations") or {}).keys()})
print(", ".join(all_locales))

for key in keys:
    locales = sorted((data["strings"][key].get("localizations") or {}).keys())
    print(f"{key}: {', '.join(locales) if locales else '(none)'}")
PY

Repository: manaflow-ai/cmux

Length of output: 377


Add the new share error strings to every locale
share.error.noWorkspaceContext and share.error.notSignedIn only have en and ja entries in Resources/Localizable.xcstrings; add the remaining supported locales so these errors don't ship English-only in other languages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Share/ShareSessionController.swift` around lines 82 - 94, Add entries
for share.error.noWorkspaceContext and share.error.notSignedIn to every
supported locale in Resources/Localizable.xcstrings, preserving the existing
English and Japanese translations and matching the catalog’s established
localization structure.

Source: Path instructions

Comment on lines +100 to +110
startTask = Task { @MainActor [weak self] in
do {
let created = try await api.createSession()
guard let self, self.status == .starting else { return }
self.activate(created: created, api: api)
} catch {
guard let self, self.status == .starting else { return }
self.status = .idle
self.lastErrorText = String(describing: error)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

User-facing lastErrorText is populated with unlocalized, potentially internal error text. lastErrorText is rendered in the share UI (set with localized share.error.* strings elsewhere), but two paths bypass that: they emit raw/verbatim error strings, which are unlocalized and can leak implementation details. Log the raw values and assign a localized, product-level message.

  • Sources/Share/ShareSessionController.swift#L100-L110: replace lastErrorText = String(describing: error) with a localized String(localized:defaultValue:) message and log the underlying error separately.
  • Sources/Share/ShareSessionController.swift#L413-L414: replace lastErrorText = "\(code): \(message)" with localized copy mapped from known codes, keeping the raw code/message in logs only.

As per coding guidelines (localize user-facing text; do not expose raw upstream error messages).

📍 Affects 1 file
  • Sources/Share/ShareSessionController.swift#L100-L110 (this comment)
  • Sources/Share/ShareSessionController.swift#L413-L414
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Share/ShareSessionController.swift` around lines 100 - 110, Update
the error handling in Sources/Share/ShareSessionController.swift lines 100-110
to log the raw error separately and assign a localized product-level message via
String(localized:defaultValue:). Also update lines 413-414 to map known error
codes to localized user-facing copy, while logging the raw code and message
instead of exposing them through lastErrorText.

Comment thread web/app/[locale]/share/[code]/pixel-pane.ts Outdated
Comment thread web/app/[locale]/share/[code]/share-panes.tsx Outdated
Comment on lines +46 to +70
export async function handleShareSessionCreate(
request: Request,
deps: ShareSessionCreateDeps,
): Promise<Response> {
const user = await deps.verifyRequest(request);
if (!user) return unauthorized();
const key = deps.signingKey();
if (!key) return json({ error: "share_not_configured" }, 503);
const code = deps.generateCode();
const { token, expiresAt } = mintShareToken({
sub: user.id,
email: user.primaryEmail ?? "",
code,
host: true,
key,
nowSeconds: deps.nowSeconds(),
});
return json({
code,
token,
expiresAt,
wsUrl: shareSessionWsUrl(code),
shareUrl: sharePageUrl(code),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider rate-limiting session creation.

Each authenticated POST mints a fresh code and, once the host connects, a new ShareSession Durable Object. Nothing here bounds how many sessions one user can spin up. Worth a rate limit (per-user or per-IP) at this route or at the edge, since DO instances/tokens are otherwise "free" to mint repeatedly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/share/sessions/route.ts` around lines 46 - 70, Update
handleShareSessionCreate to enforce a rate limit before generating the session
code or minting the token, using the authenticated user or request IP as the
limiter key and the existing dependency/configuration pattern for shared state.
Return the established rate-limit response when the limit is exceeded while
preserving the current unauthorized and share_not_configured responses.

Comment thread workers/share/src/index.ts
Comment thread workers/share/src/session.ts
…caps, error hygiene

- Only create-endpoint tokens may materialize a session in the DO; a
  host-claim refresh token reconnects but can never squat an unminted
  code (verified live: 404).
- Unsharing a workspace prunes guest subscriptions, notifies the host,
  and routeBinary drops frames for unshared workspaces.
- Per-connection subscription cap (64) bounds guest-supplied state.
- The worker strips Authorization/Cookie before forwarding to the DO.
- Web still-frame decoding is latest-wins and stops after close();
  Escape releases keyboard focus from interactive browser panes.
- Mac error surfaces are localized (en+ja); raw error detail goes to
  the debug log only.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Addressed the substantive review findings in 6aa0435: session creation is now gated on a create-endpoint token claim (a host refresh token can reconnect but never squat an unminted code — verified live against the dev worker, 404), unsharing a workspace prunes guest subscriptions and stops frame routing, per-connection subscriptions are capped at 64, the worker strips Authorization/Cookie before forwarding upgrades to the DO, web still-frame decoding is latest-wins and halts on close, Escape releases keyboard focus from interactive browser panes, and Mac-side error surfaces are localized with detail going to the debug log. Declined for now: @observable migration and the singleton note (matches the codebase's current patterns; happy to revisit), and session-create rate limiting (follow-up alongside the relay-token precedent).

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Share/ShareSessionController.swift (1)

453-456: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Avoid full-collection scans for chat deduplication.

The chat feed is a scalable collection that can grow continuously during an active session. Calling feed.contains(where:) on every incoming message results in O(N²) quadratic scaling and violates algorithmic complexity guidelines for hot paths.

Please use a Set of known message IDs for O(1) deduplication. As per path instructions, prefer Set/dictionary/indexed lookups over repeated array scans for scalable collections like chat feeds.

🛠️ Proposed fix

Add a private Set to track known IDs, updating it here and ensuring you clear/repopulate it in rebuildFeed and teardownSession:

-    private func appendChat(_ message: ShareChatMessage) {
-        guard !feed.contains(where: { $0.id == message.id }) else { return }
-        feed.append(feedItem(for: message))
-    }
+    private var knownChatIDs = Set<String>()
+
+    private func appendChat(_ message: ShareChatMessage) {
+        guard !knownChatIDs.contains(message.id) else { return }
+        knownChatIDs.insert(message.id)
+        feed.append(feedItem(for: message))
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Share/ShareSessionController.swift` around lines 453 - 456, Replace
the full-feed scan in appendChat(_:) with a private Set of known chat message
IDs, inserting each newly accepted ID before appending its feed item. Keep the
set synchronized by clearing and repopulating it when rebuildFeed executes, and
clearing it during teardownSession.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Share/ShareSessionController.swift`:
- Around line 453-456: Replace the full-feed scan in appendChat(_:) with a
private Set of known chat message IDs, inserting each newly accepted ID before
appending its feed item. Keep the set synchronized by clearing and repopulating
it when rebuildFeed executes, and clearing it during teardownSession.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a1378c8-e02d-4d68-bd1e-04c831bdd689

📥 Commits

Reviewing files that changed from the base of the PR and between 017dbb4 and 6aa0435.

📒 Files selected for processing (14)
  • Resources/Localizable.xcstrings
  • Sources/Share/ShareSessionController.swift
  • docs/multiplayer-share.md
  • web/app/[locale]/share/[code]/pixel-pane.ts
  • web/app/[locale]/share/[code]/share-panes.tsx
  • web/app/api/share/sessions/route.ts
  • web/services/share/token.ts
  • workers/share/scripts/dev-proof.ts
  • workers/share/src/do.ts
  • workers/share/src/index.ts
  • workers/share/src/jwt.ts
  • workers/share/src/session.ts
  • workers/share/test/jwt.test.ts
  • workers/share/test/session.test.ts

CI's agent-session-web-resources job rebuilds Resources/agent-session-*
from webviews/src/agent-session/shared, which slice 2 changed; the
committed bundles were stale (the earlier local build skipped the root
frozen-lockfile install the script needs).
Comment thread workers/share/src/session.ts Outdated
Comment on lines +575 to +590
private chat(conn: Conn, text: string, bubble: CursorPos | undefined): Effect[] {
const trimmed = text.slice(0, CHAT_TEXT_LIMIT).trim();
if (!trimmed) return [];
const msg: ChatMessage = {
id: crypto.randomUUID(),
user: conn.user,
text: trimmed,
ts: Date.now(),
...(bubble ? { bubble } : {}),
};
this.s.chat.push(msg);
if (this.s.chat.length > CHAT_HISTORY_LIMIT) {
this.s.chat = this.s.chat.slice(-CHAT_HISTORY_LIMIT);
}
return [{ kind: "persist" }, ...this.broadcastActive({ t: "chat", msg }, null)];
}

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 Chat bubble workspace not scrubbed against shared set

cursorBroadcast scrubs cursor positions to null when pos.ws is not a shared workspace, but the chat handler broadcasts bubble verbatim. If a workspace is unshared mid-session and a guest sends a chat with a stale bubble.ws, the DO fans it out to all participants with an invalid workspace reference. On the web client, showBubble stores msg.bubble as the cursor position fallback, so the bubble text silently fails to render for all recipients (the cursor layer filters by activeWs). A malicious editor can also deliberately suppress another participant's bubble display by sending any non-matching workspace ID.

The same scrubbing used in cursorBroadcast should be applied to the bubble field before it is stored in the chat history.

Suggested change
private chat(conn: Conn, text: string, bubble: CursorPos | undefined): Effect[] {
const trimmed = text.slice(0, CHAT_TEXT_LIMIT).trim();
if (!trimmed) return [];
const msg: ChatMessage = {
id: crypto.randomUUID(),
user: conn.user,
text: trimmed,
ts: Date.now(),
...(bubble ? { bubble } : {}),
};
this.s.chat.push(msg);
if (this.s.chat.length > CHAT_HISTORY_LIMIT) {
this.s.chat = this.s.chat.slice(-CHAT_HISTORY_LIMIT);
}
return [{ kind: "persist" }, ...this.broadcastActive({ t: "chat", msg }, null)];
}
const scrubbedBubble = bubble && this.isSharedWorkspace(bubble.ws) ? bubble : undefined;
const msg: ChatMessage = {
id: crypto.randomUUID(),
user: conn.user,
text: trimmed,
ts: Date.now(),
...(scrubbedBubble ? { bubble: scrubbedBubble } : {}),
};

Keeps the viewer-logic test framework-free: importing the composer
component pulled next-intl (and next/navigation) into bun's test module
graph, the only plausible source of CI's RedirectType interop error.
Comment on lines +153 to +162
});
if (!res.ok) {
this.session.update({ status: "unavailable" });
return;
}
grant = (await res.json()) as { token: string; wsUrl: string };
} catch {
this.scheduleReconnect();
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.

P1 Non-2xx token response permanently halts reconnection

Any non-OK HTTP status from POST /api/share/sessions/${code}/token — including transient 5xx errors from Vercel — unconditionally sets status: "unavailable" and returns without scheduling a reconnect. Only thrown network exceptions reach scheduleReconnect. If the token endpoint returns a transient 500 or 502, the viewer is permanently stuck at "Session unavailable" with no self-healing path; the user must manually refresh.

The current distinction collapses a continuum of error types — definite terminal states (e.g. 400 invalid code), auth errors (401), and configuration errors (503 for missing key) — alongside genuinely transient infrastructure failures (500/502). Only the first three categories warrant a permanent stop; the last should schedule a retry just as a network exception does.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8733adea1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +343 to +347
guard let taskToStop = webSocketTask, !isStopped else {
if connection == nil {
await stop()
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revoke the relay when stopping during reconnect

If the user chooses Stop Sharing while the host socket is reconnecting, webSocketTask is nil, so this branch reports success after only stopping the local socket and never sends the .end message. The Durable Object therefore treats this as an ordinary disconnect and keeps the guest sockets, grants, chat, and invitation valid for the two-minute host grace period, even though the Mac has returned to the idle state and indicates sharing stopped. This path needs an authenticated server-side revocation or must reconnect long enough to deliver the end command.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfd663240c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +611 to +612
guard let workspace = sharedWorkspace(in: tabManager) else {
teardownSession()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge End the relay when the shared workspace disappears

When the host closes the shared workspace while its window remains open, this path calls teardownSession() without .end. The socket is therefore closed as an ordinary disconnect, so the relay retains the previous workspace state, guest connections, chat, and invitation for the two-minute host grace period even though the Mac reports sharing as idle. Pass the final end message through this automatic teardown path so removing the shared workspace revokes the session immediately.

Useful? React with 👍 / 👎.

Comment on lines +327 to +328
self.status = .active
_ = self.sendHello()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Request fresh terminal baselines after reconnecting

When terminal output occurs while the host socket is disconnected, ShareGridStreamer rejects the send and marks that pane as needing a baseline. A normal socket reconnect reaches this .opened branch, but it only replays hello; the worker's connectHost path sends a snapshot without a resync, and the controller only calls streamer.resendFullFrames() when handling an explicit relay resync. If the terminal becomes idle before reconnection, guests remain on the pre-disconnect screen until some later Ghostty tick or output happens to trigger capture, so opening a replacement connection should explicitly schedule fresh baselines for existing subscriber streams.

Useful? React with 👍 / 👎.

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 419349620c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/AppDelegate.swift
CloudVMActionLauncher.shared.terminateAll()
CmuxSSHURLProcessLauncher.shared.terminateAll()
MobileHostService.shared.stop()
shareSessionController.stopSharing()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await share revocation before terminating

When quitting while a share is active, this synchronous delegate call only causes teardownSession to schedule a new main-actor socketStopTask; it does not await ShareSocket.sendAndStop(.end). The applicationShouldTerminate owned-cleanup path does not include sharing, so the process can exit immediately after this callback before the task runs, leaving guests and the invitation active until the two-minute host-disconnect grace period expires. Include share revocation in the bounded terminateLater cleanup and await it before replying.

Useful? React with 👍 / 👎.

Comment on lines +69 to +70
guard CmuxFeatureFlags.shared.isMultiplayerShareUIEnabled else { return }
shareSessionController.stopSharing()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep active-share controls available after flag disablement

When the remote PostHog flag changes to false during an active session and the host has closed the chat window, the contribution builder removes the Show/Stop commands and this guard also makes any cached Stop command a no-op. The remaining end-user stop control exists only inside the now-closed chat window, so the host cannot revoke the active share without closing its owner window or quitting the app. Gate session creation, but keep Show and Stop available for an already-active session.

AGENTS.md reference: AGENTS.md:L78-L78

Useful? React with 👍 / 👎.

guard socket?.send(.approve(user: user, role: role)) == .admitted else {
return
}
resolveAccessRequest(user: user, resolution: role == .editor ? .approvedEditor : .approvedViewer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Roll back access decisions when delivery fails

If the connection drops after socket.send admits an approval into the local mailbox but before URLSession sends it, this immediately marks the request as approved even though the Durable Object still considers the guest pending. There is no delivery completion or previous snapshot attached to this mutation, so the host sees a false permission decision until a later reconnect replays authoritative pending state; the deny path has the same issue. Keep the request pending until authoritative reconciliation or explicitly restore it when delivery fails.

AGENTS.md reference: AGENTS.md:L85-L85

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39cd18da3c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

this.markUnavailable();
return;
}
this.scheduleReconnect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop retrying permanently missing share sessions

When a syntactically valid link refers to a session whose tombstone has already been deleted, the token route still successfully mints a token, while the Durable Object rejects the WebSocket upgrade with HTTP 404. Browsers surface that failed upgrade as an abnormal close (1006), so this unconditional reconnect path fetches another token and retries forever, leaving expired or mistyped links stuck on the connecting screen and repeatedly consuming the token rate limit. Add a bounded terminal condition or an authoritative existence signal that can transition the page to unavailable while preserving the short host-creation race retry.

Useful? React with 👍 / 👎.

Comment on lines +640 to +642
onTextChange: (input: string) => {
dispatch({ type: "setInput", input });
notifyComposerTextChanged(input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid unused native IPC on every composer edit

Every agent-composer text change now posts composer.changed across the WKWebView bridge, generates a native reply, and allocates a promise, but no production code assigns onComposerTextChanged, and the sharing protocol explicitly does not use composer edits. Consequently normal typing pays one unnecessary JS-to-native round trip per edit and can accumulate pending requests during rapid input. Install this callback only when a consumer exists, or debounce/batch the notifications before enabling them.

Useful? React with 👍 / 👎.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants