Skip to content

feat(webview): in-page screen-share picker for getDisplayMedia (#713)#809

Merged
graycyrus merged 12 commits intotinyhumansai:mainfrom
oxoxDev:feat/713-screen-share-picker
Apr 24, 2026
Merged

feat(webview): in-page screen-share picker for getDisplayMedia (#713)#809
graycyrus merged 12 commits intotinyhumansai:mainfrom
oxoxDev:feat/713-screen-share-picker

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented Apr 22, 2026

Summary

  • Embedded webviews (Meet, Slack Huddles, Discord, Zoom) that call navigator.mediaDevices.getDisplayMedia now see a native-feeling OpenHuman picker with real screen / window thumbnails instead of silently capturing the entire primary display.
  • Picker is injected into each child webview's own DOM so a single shim covers every provider without per-provider host-side glue.
  • Screen-share Tauri commands are session-gated ([Security] screen-share list/thumbnail commands reachable from third-party page origin (follow-up to #809) #812 Stage A): the shim must open a short-lived session with begin_session (gated on caller label + user-activation claim + rate limit) before it can enumerate sources or fetch thumbnails, and a valid token only authorises thumbnails for IDs that were in the picker the user was actually shown.
  • macOS implementation landed; Windows/Linux return "not implemented yet" until the design is proven end-to-end.

Problem

tinyhumansai/openhuman#713 AC2 — "Click 'Present now' → OS screen/window picker appears → user selects a screen or window → share streams into the Meet call".

Our child webviews run under the CEF Alloy runtime, which does not link Chromium's built-in DesktopMediaPicker (that code lives in //chrome/browser and Alloy strips it). With only the permission-handler grant in place, Chromium auto-selects the primary display and the user's entire screen silently streams into the call with no visible picker or consent flow.

Solution

Three parts.

1. Native source enumerator + thumbnail capture (screen_capture/)

CoreGraphics + CoreFoundation + ImageIO FFI (no new transitive crate tree). Screens via CGGetActiveDisplayList, windows via CGWindowListCopyWindowInfo, thumbnails via CGDisplayCreateImage / CGWindowListCreateImageCGImageDestinationCreate{WithData,AddImage,Finalize}, base64-encoded for IPC. Thumbnails are captured on demand rather than during enumeration so the picker opens instantly.

Source IDs are formatted as screen:<CGDirectDisplayID>:0 / window:<CGWindowID>:0 — Chromium's DesktopMediaID::Parse reads these directly, so the shim can hand them straight to getUserMedia without going through the extension-only chrome.desktopCapture API.

2. In-page getDisplayMedia shim (webview_accounts/runtime.js)

Installed via the existing initialization-script path for every account webview. Overrides MediaDevices.prototype.getDisplayMedia (instance assignment is non-writable in modern Chromium, so we Object.defineProperty on the prototype), builds the picker overlay imperatively (Trusted Types CSP rejects innerHTML on Meet), and on Share calls getUserMedia with a hand-crafted chromeMediaSource: 'desktop' + chromeMediaSourceId constraint. Our CEF permission callback grants DESKTOP_VIDEO_CAPTURE bits, so Chromium honours the ID and opens a real capture device.

Patches the returned MediaStreamTrack.getSettings() to expose displaySurface + contentHint = 'detail' — without that, Meet rejects the stream with a generic "Can't share your screen" error because the track looks indistinguishable from a getUserMedia video source. Cancel throws AbortError so Meet silently dismisses (instead of "Meet needs permission to screen share"). navigator.permissions.query is shadowed to report display-capture / camera / microphone as granted so sites that front-check the Permissions API before calling getUserMedia don't short-circuit. If the page requests audio, shim tries audio+video and falls back to video-only on rejection (stock macOS has no loopback audio driver).

3. Session gating (#812 Stage A)

The first-pass landing exposed screen_share_list_sources / screen_share_thumbnail directly on the recipe-webview allowlist, which let any script running inside an embedded site (page JS, compromised third-party CDN) silently enumerate every open window title + live thumbnail with no user gesture. CodeRabbit + graycyrus flagged this as a blocker on the first push.

The commands are now session-gated. screen_share_begin_session returns { token, sources } after enforcing:

  • Caller webview label starts with acct_ (rejects main/overlay windows and any non-recipe webview).
  • has_user_activation claim from the shim is true (frontend reads navigator.userActivation.isActive).
  • ≤10 begin attempts per account in any 60s window.
  • A new begin replaces any in-flight session for the same account (host-side half of the "concurrent getDisplayMedia → stacked overlays" fix; the shim-side guard rejects concurrent calls with InvalidStateError).

screen_share_thumbnail(token, id) requires a live token and an id that was in that session's enumerated source list — a leaked token can only replay thumbnails for IDs the shim already showed the user. screen_share_finalize_session is called on Share/Cancel to release the session immediately instead of waiting for the 30s TTL.

Residual risk (honest scope of Stage A)

The has_user_activation flag begin_session reads is page-trusted. A Meet-origin script that lies with hasUserActivation: true still bypasses the gesture gate — its only remaining bounds are acct_* caller + 10/min rate limit + thumbnail IDs restricted to what the picker actually showed. Host-verified gating needs the OnRequestMediaAccessPermission hook in vendored tauri-cef (Stage B); that's tracked for a follow-up PR in #812. Stage A kills drive-by enumeration and cross-webview abuse cleanly; in-gesture malicious first-party JS is not closed by it.

Submission Checklist

  • Unit tests — 13 tests in screen_capture/mod.rs::tests: parse_source_id happy paths + 5 error shapes, session purge_expired, rate-limit 11th-call block, rate-limit per-account scoping, generate_token URL-safe shape + uniqueness, token_prefix truncation.
  • E2E / integration — N/A here. The E2E harness does not currently drive embedded account webviews into a live Meet call; adding that scaffolding is its own follow-up. Live-verified manually on Meet (picker opens, sources enumerate, picked source streams, Cancel dismisses silently, concurrent getDisplayMedia call rejected cleanly).
  • Doc comments//! module preamble on screen_capture/mod.rs explains the flow end-to-end including the Stage A gating rationale; shim carries a long header block. New commands are documented in docs/src-tauri/02-commands.md.
  • Inline comments — Gate rationale, token generation design, u32::try_from overflow reasoning, Sequoia TCC quirk, cfstr UB avoidance, each carry a short grep-friendly comment.

Impact

  • Platform: macOS only for enumeration + thumbnail capture; Linux/Windows fall through to "not implemented yet". The shim itself is cross-platform JavaScript, so as soon as a Windows/Linux enumerator lands, the picker works there too.
  • macOS TCC: first screen-capture call prompts the user for Screen Recording permission via System Settings, same as any other Chromium-based app. Signed builds persist the grant. A 1×1 transparent CGImage (Sequoia's return when TCC is not granted) is treated as "unavailable" now via the min-dimension guard — the picker falls back to the glyph placeholder instead of showing a 1px thumbnail.
  • Performance: enumeration is a handful of Quartz calls + zero thumbnails; each thumbnail IPC is deduped on the shim side and captured on demand, so the picker opens without blocking.
  • Security: the chromeMediaSource: 'desktop' constraint path is normally gated to Chromium extensions. We accept the exposure deliberately because the permission grant still flows through our CEF OnRequestMediaAccessPermission handler, and the session-gated command API bounds what page JS can observe outside of a picker interaction. Residual risk documented above.
  • Compatibility: no schema / config / migration surface.

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added screen and window sharing functionality with an in-page picker overlay for source selection.
    • Implemented session-gated screen enumeration and thumbnail capture (macOS support).
    • Introduced custom getDisplayMedia integration with session-based access control and user gesture validation.
  • Documentation

    • Added documentation for screen share picker commands and workflows.

@oxoxDev oxoxDev requested a review from a team April 22, 2026 21:24
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bff416c2-021e-495f-bf96-b0ca635be778

📥 Commits

Reviewing files that changed from the base of the PR and between f7a575e and 40b9bfb.

📒 Files selected for processing (2)
  • app/src-tauri/Cargo.toml
  • app/src-tauri/src/lib.rs

📝 Walkthrough

Walkthrough

Adds CEF/macOS screen-share support: new Tauri screen_capture module with session-gated commands (begin/thumbnail/finalize), permission allowlist updates, a webview getDisplayMedia shim with in-page picker and permission shims, a base64 crate dependency, and documentation for the new commands.

Changes

Cohort / File(s) Summary
Dependency
app/src-tauri/Cargo.toml
Added dependency: base64 = "0.22".
Permissions
app/src-tauri/permissions/allow-core-process.toml, app/src-tauri/permissions/allow-webview-recipe.toml
Expanded allowlists to include screen_share_begin_session, screen_share_thumbnail, screen_share_finalize_session; updated allow-webview-recipe description to mention session-gated screen-share enumeration.
Tauri integration
app/src-tauri/src/lib.rs
Under cef feature, registers new screen_capture module, adds ScreenShareState, and wires invoke handlers for begin/thumbnail/finalize session.
Screen capture backend
app/src-tauri/src/screen_capture/mod.rs
New module: ScreenSource, session-gated commands and state, token/session lifecycle, per-account rate limiting, ID parsing/validation, macOS enumeration and PNG→base64 thumbnails, unit tests; non-macOS stubs return not-implemented/empty.
Webview shim / runtime
app/src-tauri/src/webview_accounts/runtime.js
Injects getDisplayMedia shim with session gating, overlay picker, IPC calls to new commands, permission-query shims, and post-capture track adjustments.
Docs
docs/src-tauri/02-commands.md
Documents CEF/macOS screen share picker commands, session semantics, and thumbnail behavior.

Sequence Diagram

sequenceDiagram
    actor User
    participant WebView as WebView/JS
    participant Overlay as Overlay Picker
    participant Tauri as Tauri Cmds (Rust)
    participant CG as macOS CoreGraphics
    participant Browser as Browser getUserMedia

    User->>WebView: navigator.mediaDevices.getDisplayMedia()
    WebView->>Tauri: invoke screen_share_begin_session (has_user_activation)
    Tauri->>Tauri: enforce rate limits, create token, enumerate allowed IDs
    Tauri->>CG: enumerate displays & windows (macOS)
    CG-->>Tauri: source metadata
    Tauri->>Tauri: capture/encode thumbnails (PNG → base64)
    Tauri-->>WebView: BeginSessionResult { token, sources }
    WebView->>Overlay: render picker with sources
    User->>Overlay: select source
    Overlay-->>WebView: chosen source id
    WebView->>Tauri: (optional) screen_share_thumbnail with token,id
    Tauri-->>WebView: thumbnail (base64) or error
    WebView->>Browser: getUserMedia({ chromeMediaSource: 'desktop', chromeMediaSourceId })
    Browser-->>WebView: MediaStream
    WebView->>WebView: set contentHint & override track.getSettings()
    WebView->>Tauri: screen_share_finalize_session(token)
    Tauri->>Tauri: remove session (no-op if expired)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • senamakel
  • M3gA-Mind

Poem

🐰 I hop through macOS screens at night,
I list each window, capture light,
A token given, thumbnails spun,
Picker chosen, capture won —
Base64 bunnies share the sight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: an in-page screen-share picker for the getDisplayMedia API.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/src-tauri/src/screen_capture/mod.rs (1)

61-94: Add command-level debug logs for the new screen-share IPCs.

Right now failures are mostly silent unless CoreGraphics returns a low-level error. When the picker comes up empty or a thumbnail never arrives, there is no log with the command path, source id, or returned count to correlate from the webview side. Please add debug logs on entry/exit with a stable [screen-share] prefix.

As per coding guidelines, "**/*.rs: Use log / tracing at debug / trace levels. Default to verbose diagnostics on new/changed flows with stable grep-friendly prefixes ([domain], [rpc], [ui-flow]) and correlation fields (request IDs, method names, entity IDs)."

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

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 61 - 94, Add
debug-entry/exit tracing to the new screen-share IPC commands: in
screen_share_list_sources() log a debug line on entry like "[screen-share]
screen_share_list_sources request" and on successful return include the returned
Vec length (and on Err include the error) so callers can correlate picker
results; in screen_share_thumbnail(args: ThumbnailArgs) log on entry including
the args.id ("[screen-share] screen_share_thumbnail request id={id}") and on
success return include a debug log with id and that a thumbnail was returned (or
on Err log id plus the error). Use the existing macos::enumerate and
macos::thumbnail_for_id call sites to attach the logs, follow project convention
using the log/tracing debug level and the stable "[screen-share]" prefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src-tauri/permissions/allow-webview-recipe.toml`:
- Around line 3-6: Remove the raw screen-source commands from the allow list so
third-party pages cannot directly invoke them: in the allow-webview-recipe
permission block, delete "screen_share_list_sources" and
"screen_share_thumbnail" from permission.commands.allow, leaving only safe
commands like "webview_recipe_event"; instead implement a host-mediated flow
where the webview calls a new safe command (e.g., request_screen_share_session)
that returns a one-shot session nonce, and the host (Rust) uses that nonce to
run the picker and deliver results back to the webview via a controlled channel;
update handlers for screen_share_list_sources/screen_share_thumbnail to only
accept requests tied to a valid server-generated nonce/session and reject direct
invocations from the page.

In `@app/src-tauri/src/webview_accounts/runtime.js`:
- Around line 656-678: The helper window.__ohScreenShareTest is a global
screen-capture bypass and must not be exposed in production; either remove its
definition entirely or wrap it so it only attaches in non-production/dev builds
(e.g., check an explicit dev flag like process.env.NODE_ENV !== 'production' or
a runtime isDev boolean) and also remove the hardcoded default sourceId
('screen:1:0') so it never attempts a default capture; update the runtime.js
code that defines window.__ohScreenShareTest to only create the function when
running in a dev-only context and ensure no global reference remains in
production.
- Around line 453-485: render currently fires
rawInvoke('screen_share_thumbnail', ...) on every re-render while
src.thumbnailPngBase64 is unset; to deduplicate, add and check an in-flight
promise/flag on the source (e.g. src._thumbnailRequest) before calling
rawInvoke: if src.thumbnailPngBase64 exists return; if src._thumbnailRequest
exists attach a .then handler to it to update the live node; otherwise set
src._thumbnailRequest = rawInvoke(...).then(b64 => { if (typeof b64 ===
'string') { src.thumbnailPngBase64 = b64; /* update liveThumb same as current
code */ } return b64 }).catch(()=>{}).finally(()=>{ delete src._thumbnailRequest
}); update all places that currently call rawInvoke('screen_share_thumbnail',
...) to use this pattern so each source only issues one in-flight request.
- Around line 279-283: The DOM-not-ready branch currently calls
resolveOuter(null) causing an artificial AbortError; instead, wait for
DOMContentLoaded and then re-check host() and proceed with the normal picker
flow (i.e., call the same code path that would run when host() is available) or,
if host() is still falsy after the event, fall back to the original
getDisplayMedia implementation and resolve/reject accordingly. Update the block
that adds the DOMContentLoaded listener (referencing host() and resolveOuter) so
the handler re-evaluates host() and either invokes the host-based picker flow or
delegates to the original getDisplayMedia behavior rather than resolving null
immediately.

---

Nitpick comments:
In `@app/src-tauri/src/screen_capture/mod.rs`:
- Around line 61-94: Add debug-entry/exit tracing to the new screen-share IPC
commands: in screen_share_list_sources() log a debug line on entry like
"[screen-share] screen_share_list_sources request" and on successful return
include the returned Vec length (and on Err include the error) so callers can
correlate picker results; in screen_share_thumbnail(args: ThumbnailArgs) log on
entry including the args.id ("[screen-share] screen_share_thumbnail request
id={id}") and on success return include a debug log with id and that a thumbnail
was returned (or on Err log id plus the error). Use the existing
macos::enumerate and macos::thumbnail_for_id call sites to attach the logs,
follow project convention using the log/tracing debug level and the stable
"[screen-share]" prefix.
🪄 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: 46d70bc1-99fb-488f-876e-02349da0da3a

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb714b and 9c23e36.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • app/src-tauri/Cargo.toml
  • app/src-tauri/permissions/allow-core-process.toml
  • app/src-tauri/permissions/allow-webview-recipe.toml
  • app/src-tauri/src/lib.rs
  • app/src-tauri/src/screen_capture/mod.rs
  • app/src-tauri/src/webview_accounts/runtime.js

Comment on lines +3 to +6
description = "Allow injected per-provider recipe code (running inside the third-party site's origin) to invoke the recipe ingest command back to Rust. Also includes screen_share_list_sources so the in-page getDisplayMedia shim (#713) can enumerate real screens and windows to build its picker UI inside the child webview's DOM."

[permission.commands]
allow = ["webview_recipe_event"]
allow = ["webview_recipe_event", "screen_share_list_sources", "screen_share_thumbnail"]
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not expose raw screen-source commands to the third-party page context.

This capability runs inside the embedded site's origin, so the site itself can call window.__TAURI_INTERNALS__.invoke('screen_share_list_sources'|'screen_share_thumbnail'). That exposes cross-app window titles and live thumbnails before the user has chosen anything in the picker, which is a privacy/security blocker. Keep these commands out of allow-webview-recipe and route picker data through a host-owned, one-shot session/nonce instead.

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

In `@app/src-tauri/permissions/allow-webview-recipe.toml` around lines 3 - 6,
Remove the raw screen-source commands from the allow list so third-party pages
cannot directly invoke them: in the allow-webview-recipe permission block,
delete "screen_share_list_sources" and "screen_share_thumbnail" from
permission.commands.allow, leaving only safe commands like
"webview_recipe_event"; instead implement a host-mediated flow where the webview
calls a new safe command (e.g., request_screen_share_session) that returns a
one-shot session nonce, and the host (Rust) uses that nonce to run the picker
and deliver results back to the webview via a controlled channel; update
handlers for screen_share_list_sources/screen_share_thumbnail to only accept
requests tied to a valid server-generated nonce/session and reject direct
invocations from the page.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tracked as follow-up in #812 — host-driven session/nonce flow requires adjusting the vendored tauri-cef media-access callback so it can be held while the shim renders the picker, which is a bigger change than landing the immediate two-critical/two-major fixes. Leaving this PR's allowlist as-is but ack'd for the next round.

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.

@oxoxDev, understood — thanks for the context. The tauri-cef media-access callback constraint is a reasonable blocker for a proper nonce/session flow. I'll track this against #812 and re-raise it there. Happy to help design the host-mediated IPC shape when that work starts.

Comment thread app/src-tauri/src/webview_accounts/runtime.js Outdated
Comment thread app/src-tauri/src/webview_accounts/runtime.js Outdated
Comment thread app/src-tauri/src/webview_accounts/runtime.js Outdated
oxoxDev added a commit to oxoxDev/openhuman that referenced this pull request Apr 22, 2026
)

Picks up the 2 critical + 2 major findings on tinyhumansai#809 that do not require
a host/tauri-cef architecture change:

- Delete `window.__ohScreenShareTest` entirely. The PoC helper was a
  page-global screen-capture primitive (default `screen:1:0`, no
  picker) that any third-party script in the embedded site could have
  invoked to capture the main display silently. We have the live
  in-page picker now; the helper has no production value.
- Retry `showInPagePicker` after `DOMContentLoaded` when `document.body`
  isn't ready yet, instead of resolving `null` and letting the shim
  surface an `AbortError` for a picker that was never actually shown.
- Deduplicate in-flight `screen_share_thumbnail` IPCs per source. The
  picker's `render()` runs again on every selection change and tab
  switch; without a cache, every un-cached source would re-issue its
  thumbnail capture on each pass. Cache the in-flight promise on the
  source object and reuse it, which also lets all pending render
  passes settle when the first one completes.

The third critical finding (commands still reachable from the recipe
origin via direct `invoke()`) needs a host-driven nonce/session flow
and lands in a follow-up PR — see issue tracker.

Refs coderabbit review on tinyhumansai#809.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src-tauri/src/webview_accounts/runtime.js (1)

531-536: Consider validating selectedId before the render loop.

The current flow renders buttons, then checks if selectedId is valid, then manually adds the selected class to firstChild. This works but requires the post-render fixup. Moving the validation before the forEach loop would make the code flow clearer.

♻️ Suggested simplification
       function render() {
         while (gridEl.firstChild) gridEl.removeChild(gridEl.firstChild);
         const filtered = sources.filter(function (s) { return s.kind === activeTab; });
         if (filtered.length === 0) {
           // ... empty state handling
           return;
         }
+        // Validate selectedId before rendering so buttons get correct 'selected' class
+        if (!selectedId || !filtered.some(function (s) { return s.id === selectedId; })) {
+          selectedId = filtered[0].id;
+        }
         filtered.forEach(function (src) {
           const btn = document.createElement('button');
           btn.className = 'srcbtn' + (selectedId === src.id ? ' selected' : '');
           // ...
         });
-        if (!selectedId || !filtered.some(function (s) { return s.id === selectedId; })) {
-          selectedId = filtered[0].id;
-          gridEl.firstChild && gridEl.firstChild.classList.add('selected');
-        }
         shareBtn.disabled = !selectedId;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/webview_accounts/runtime.js` around lines 531 - 536,
Validate and normalize selectedId before the render loop: check if selectedId is
falsy or not present in filtered and if so set selectedId = filtered[0].id, then
proceed into the forEach that creates children so you can add the 'selected'
class to the correct element during element creation (using gridEl child
creation logic) instead of adding gridEl.firstChild.classList.add('selected')
after rendering; also ensure shareBtn.disabled is computed from the validated
selectedId after the loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src-tauri/src/webview_accounts/runtime.js`:
- Around line 531-536: Validate and normalize selectedId before the render loop:
check if selectedId is falsy or not present in filtered and if so set selectedId
= filtered[0].id, then proceed into the forEach that creates children so you can
add the 'selected' class to the correct element during element creation (using
gridEl child creation logic) instead of adding
gridEl.firstChild.classList.add('selected') after rendering; also ensure
shareBtn.disabled is computed from the validated selectedId after the loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3404d6d-693c-4721-8c56-96a8988cd8e6

📥 Commits

Reviewing files that changed from the base of the PR and between 9c23e36 and ee527ab.

📒 Files selected for processing (2)
  • app/src-tauri/src/screen_capture/mod.rs
  • app/src-tauri/src/webview_accounts/runtime.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src-tauri/src/screen_capture/mod.rs

@graycyrus
Copy link
Copy Markdown
Contributor

Hey @oxoxDev — this PR has merge conflicts with main. Could you rebase and resolve them when you get a chance? We'll pick up the review once it's clean.

oxoxDev and others added 8 commits April 23, 2026 11:10
…inyhumansai#713)

New `screen_capture` module exposes two Tauri commands that back the
in-page getDisplayMedia picker shim:

- `screen_share_list_sources` enumerates real screens (CGGetActiveDisplayList)
  and on-screen windows (CGWindowListCopyWindowInfo), tagging each with a
  Chromium-compatible DesktopMediaID string (`screen:<CGDirectDisplayID>:0` /
  `window:<CGWindowID>:0`). No thumbnails are captured during enumeration so
  the picker opens instantly.

- `screen_share_thumbnail` captures a single source's live thumbnail on
  demand via CGDisplayCreateImage / CGWindowListCreateImage, encodes it to
  PNG through ImageIO, and returns base64. The picker shim fires these in
  parallel so thumbnails fade in as they arrive.

Uses raw CoreGraphics + CoreFoundation + ImageIO FFI so we don't pull in
the full `core-graphics` crate (~50 extra transitive deps). Adds the
existing-workspace `base64` crate for PNG serialization.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire `screen_share_list_sources` and `screen_share_thumbnail` into the
Tauri invoke_handler so the getDisplayMedia shim injected into child
webviews (Meet, Slack, Discord, Zoom) can reach them over IPC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ebviews (tinyhumansai#713)

Add `screen_share_list_sources` and `screen_share_thumbnail` to both
permission scopes:

- `allow-core-process` so the host window can call them if a future
  surface ever needs enumeration host-side.
- `allow-webview-recipe` so the per-provider recipe runtime (which now
  includes the in-page getDisplayMedia picker) can enumerate and fetch
  thumbnails directly from the third-party origin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…umansai#713)

Child webviews (Meet, Slack Huddles, Discord, Zoom) run under CEF Alloy,
which does not link Chromium's DesktopMediaPicker. Without an
interceptor the permission handler auto-grants `DESKTOP_VIDEO_CAPTURE`
and Chromium silently picks the primary display — the user never sees a
picker and their whole screen streams.

This shim, injected via `WebviewBuilder.initialization_script`:

- Overrides `navigator.mediaDevices.getDisplayMedia` on the MediaDevices
  prototype (plain assignment is non-writable in modern Chromium, so we
  `Object.defineProperty` on the prototype).
- Enumerates real sources via the new `screen_share_list_sources` Tauri
  command, then renders a picker overlay directly into the host page's
  DOM (no main-window React modal) so it works uniformly for every
  provider without per-provider host-side glue and without native-view
  stacking problems. Picker DOM is built imperatively to satisfy Meet's
  Trusted Types CSP (innerHTML would throw).
- Lazy-fetches thumbnails in parallel through `screen_share_thumbnail`
  so the picker opens instantly and images fade in.
- On Share, calls `getUserMedia` with a hand-crafted
  `chromeMediaSource: 'desktop' + chromeMediaSourceId` constraint. Our
  CEF permission callback grants `DESKTOP_VIDEO_CAPTURE` bits, so
  Chromium honours the ID and opens a real capture device.
- Patches the returned track's `getSettings()` to expose
  `displaySurface: 'monitor' | 'window'` and `contentHint: 'detail'`,
  because Meet refuses streams that lack the display-media metadata real
  getDisplayMedia would populate.
- Falls back to video-only if the page asked for audio but audio+video
  getUserMedia rejects (stock macOS has no loopback audio driver).
- Throws `AbortError` on picker cancel so Meet silently dismisses instead
  of showing "Meet needs permission to screen share".
- Shims `navigator.permissions.query` to report `display-capture` as
  `granted`; CEF Alloy's Permissions API defaults to `prompt`/`denied`
  and pages that consult it otherwise refuse to call getDisplayMedia at
  all.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extract the `screen:<id>:0` / `window:<id>:0` ID parser out of the
macOS-only thumbnail helper into a platform-agnostic `parse_source_id`
and add unit tests for:

- valid screen and window IDs
- trailing segments beyond the canonical `:0`
- unknown kind prefixes (tab, browser, empty)
- missing or non-numeric numeric segment
- u32 overflow and negative numbers
- enumerator → parser round-trip shape

The parser is where bad shim input would silently degrade to "thumbnail
unavailable" in production, so pinning its contract keeps the
shim/host boundary honest even though the surrounding macOS capture
layer itself can only be exercised in live-app testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#713)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
)

Picks up the 2 critical + 2 major findings on tinyhumansai#809 that do not require
a host/tauri-cef architecture change:

- Delete `window.__ohScreenShareTest` entirely. The PoC helper was a
  page-global screen-capture primitive (default `screen:1:0`, no
  picker) that any third-party script in the embedded site could have
  invoked to capture the main display silently. We have the live
  in-page picker now; the helper has no production value.
- Retry `showInPagePicker` after `DOMContentLoaded` when `document.body`
  isn't ready yet, instead of resolving `null` and letting the shim
  surface an `AbortError` for a picker that was never actually shown.
- Deduplicate in-flight `screen_share_thumbnail` IPCs per source. The
  picker's `render()` runs again on every selection change and tab
  switch; without a cache, every un-cached source would re-issue its
  thumbnail capture on each pass. Cache the in-flight promise on the
  source object and reuse it, which also lets all pending render
  passes settle when the first one completes.

The third critical finding (commands still reachable from the recipe
origin via direct `invoke()`) needs a host-driven nonce/session flow
and lands in a follow-up PR — see issue tracker.

Refs coderabbit review on tinyhumansai#809.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented Apr 23, 2026

Hey @oxoxDev — this PR has merge conflicts with main. Could you rebase and resolve them when you get a chance? We'll pick up the review once it's clean.

yes bro doing it right away

@oxoxDev oxoxDev force-pushed the feat/713-screen-share-picker branch from ee527ab to 4c9274f Compare April 23, 2026 06:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/src-tauri/src/screen_capture/mod.rs (3)

444-451: Incomplete bounds filtering — documented as intentional MVP scope.

The bounds check code structure exists but doesn't implement the actual size filter. The comment explains this is intentional for MVP. Consider adding a TODO or follow-up issue reference for completing this filter to exclude tooltips/tiny panels.

📝 Optional: Add TODO for future bounds filtering
             // kCGWindowBounds is actually a CFDictionary with Width/Height
             // keys. Cheap filter: if the dict has a "Width" key and it's
             // < 50, skip. Implementing full parse isn't worth it for the
             // MVP; Chromium renders a scrollable picker anyway.
+            // TODO(`#713`): Implement bounds filtering to exclude tooltips/tiny panels
             let _ = bounds_dict;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 444 - 451, The bounds
check block around CFDictionaryGetValue(dict, key_bounds) currently creates
bounds_dict but does not perform the documented Width/Height filtering; either
implement the simple size filter (read the "Width" key from bounds_dict and skip
when < 50) or at minimum add a clear TODO with a tracking issue reference so
future work isn't lost; update the block around CFDictionaryGetValue, dict,
key_bounds and bounds_dict to either perform the Width check or include a TODO
comment referencing the issue number and explaining that tiny windows (e.g.,
tooltips with Width < 50) should be excluded.

434-437: Window ID type mismatch is benign but could be cleaner.

cfnumber_to_u64 returns u64, but CGWindowID is u32. The string formatting works correctly, but for type safety you could cast to u32 when constructing the ID string to match the Chromium DesktopMediaID expectation.

♻️ Optional: Explicit u32 cast for clarity
             let window_id = match cfnumber_to_u64(number_cf) {
-                Some(v) => v,
+                Some(v) => v as u32,
                 None => continue,
             };

Also applies to: 467-468

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

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 434 - 437, The window
ID value returned by cfnumber_to_u64 is a u64 but the Chromium DesktopMediaID
expects a CGWindowID (u32); in the code paths that use window_id (the variable
set from cfnumber_to_u64 and later when building the ID string), cast the u64 to
u32 (e.g., let window_id_u32 = window_id as u32) before formatting the
DesktopMediaID so types match and intent is explicit; apply the same explicit
cast in the other occurrence noted around the cfnumber_to_u64 usage at the
second location (lines near the other cfnumber_to_u64 call).

61-71: Consider adding debug-level logging for command entry.

Per coding guidelines, new/changed flows should have verbose diagnostics. Adding a log::debug!("[screen-share] list_sources called") at entry would aid debugging without impacting performance.

🔧 Optional: Add entry logging
 #[tauri::command]
 pub fn screen_share_list_sources() -> Result<Vec<ScreenSource>, String> {
+    log::debug!("[screen-share] list_sources invoked");
     #[cfg(target_os = "macos")]
     {
         macos::enumerate().map_err(|e| format!("enumerate failed: {e}"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 61 - 71, Add a
debug-level entry log to the screen_share_list_sources command so callers are
recorded; specifically, at the top of the pub fn screen_share_list_sources() add
a log::debug!("[screen-share] list_sources called") (or use debug! if the log
macro is already in scope). Keep the log call unconditional and before the cfg
blocks so it runs on all platforms; ensure the log crate/macros are available in
this module if not already.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src-tauri/src/screen_capture/mod.rs`:
- Around line 61-71: Add documentation entries for the two Tauri IPC commands
screen_share_list_sources and screen_share_thumbnail to the project's Tauri
commands documentation (02-commands.md): create a new "Screen Capture" section
(or add to the appropriate section), describe each command's purpose,
input/output types and return values, and clearly note they are currently
macOS-only; ensure the command names exactly match the exported functions
(screen_share_list_sources, screen_share_thumbnail) so the docs align with the
registered IPC handlers.

---

Nitpick comments:
In `@app/src-tauri/src/screen_capture/mod.rs`:
- Around line 444-451: The bounds check block around CFDictionaryGetValue(dict,
key_bounds) currently creates bounds_dict but does not perform the documented
Width/Height filtering; either implement the simple size filter (read the
"Width" key from bounds_dict and skip when < 50) or at minimum add a clear TODO
with a tracking issue reference so future work isn't lost; update the block
around CFDictionaryGetValue, dict, key_bounds and bounds_dict to either perform
the Width check or include a TODO comment referencing the issue number and
explaining that tiny windows (e.g., tooltips with Width < 50) should be
excluded.
- Around line 434-437: The window ID value returned by cfnumber_to_u64 is a u64
but the Chromium DesktopMediaID expects a CGWindowID (u32); in the code paths
that use window_id (the variable set from cfnumber_to_u64 and later when
building the ID string), cast the u64 to u32 (e.g., let window_id_u32 =
window_id as u32) before formatting the DesktopMediaID so types match and intent
is explicit; apply the same explicit cast in the other occurrence noted around
the cfnumber_to_u64 usage at the second location (lines near the other
cfnumber_to_u64 call).
- Around line 61-71: Add a debug-level entry log to the
screen_share_list_sources command so callers are recorded; specifically, at the
top of the pub fn screen_share_list_sources() add a log::debug!("[screen-share]
list_sources called") (or use debug! if the log macro is already in scope). Keep
the log call unconditional and before the cfg blocks so it runs on all
platforms; ensure the log crate/macros are available in this module if not
already.
🪄 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: 43892bbf-4d2d-4eef-80f2-1f682218722d

📥 Commits

Reviewing files that changed from the base of the PR and between ee527ab and 4c9274f.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • app/src-tauri/Cargo.toml
  • app/src-tauri/permissions/allow-core-process.toml
  • app/src-tauri/permissions/allow-webview-recipe.toml
  • app/src-tauri/src/lib.rs
  • app/src-tauri/src/screen_capture/mod.rs
  • app/src-tauri/src/webview_accounts/runtime.js
✅ Files skipped from review due to trivial changes (3)
  • app/src-tauri/Cargo.toml
  • app/src-tauri/permissions/allow-core-process.toml
  • app/src-tauri/permissions/allow-webview-recipe.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src-tauri/src/webview_accounts/runtime.js

Comment thread app/src-tauri/src/screen_capture/mod.rs Outdated
Comment on lines +61 to +71
#[tauri::command]
pub fn screen_share_list_sources() -> Result<Vec<ScreenSource>, String> {
#[cfg(target_os = "macos")]
{
macos::enumerate().map_err(|e| format!("enumerate failed: {e}"))
}
#[cfg(not(target_os = "macos"))]
{
Err("screen-share picker not implemented for this platform yet".to_string())
}
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the new commands are documented
rg -i 'screen_share' docs/src-tauri/02-commands.md 2>/dev/null || echo "File not found or commands not documented"

Repository: tinyhumansai/openhuman

Length of output: 107


🏁 Script executed:

# Check if docs/src-tauri/02-commands.md exists and what's in it
fd -type f 'commands.md' docs/ 2>/dev/null || echo "Not in docs/"

# Also search more broadly
fd -type f 'commands.md' . 2>/dev/null | head -20

Repository: tinyhumansai/openhuman

Length of output: 78


🏁 Script executed:

# Check the exact commands at the specified lines in the file
sed -n '61,71p' app/src-tauri/src/screen_capture/mod.rs
echo "---"
sed -n '107,118p' app/src-tauri/src/screen_capture/mod.rs

Repository: tinyhumansai/openhuman

Length of output: 797


🏁 Script executed:

# Check if there's any documentation structure for Tauri commands elsewhere
fd -type f '*.md' docs/ | head -20

Repository: tinyhumansai/openhuman

Length of output: 236


🏁 Script executed:

# Let's properly search for markdown files in docs
find docs -name "*.md" 2>/dev/null | head -20

Repository: tinyhumansai/openhuman

Length of output: 535


🏁 Script executed:

# Check if docs directory exists at all
ls -la docs/ 2>/dev/null || echo "docs/ directory not found"

# Also check from root
ls -la . | grep -i doc

Repository: tinyhumansai/openhuman

Length of output: 2472


🏁 Script executed:

# Search for any reference to "screen_share" in the entire repo to understand documentation patterns
rg 'screen_share' --type md 2>/dev/null | head -20

Repository: tinyhumansai/openhuman

Length of output: 48


🏁 Script executed:

# Check what's in docs/src-tauri directory
ls -la docs/src-tauri/

Repository: tinyhumansai/openhuman

Length of output: 432


🏁 Script executed:

# Now let's check the content of 02-commands.md if it exists
cat docs/src-tauri/02-commands.md 2>/dev/null || echo "File does not exist"

Repository: tinyhumansai/openhuman

Length of output: 3722


🏁 Script executed:

# Let's also check the lib.rs file to see how other commands are documented (if at all)
rg '#\[tauri::command\]' app/src-tauri/src/lib.rs -A 2 | head -40

Repository: tinyhumansai/openhuman

Length of output: 1365


🏁 Script executed:

# Check if screen_share commands are registered in the generate_handler macro in lib.rs
rg 'generate_handler' app/src-tauri/src/lib.rs -A 50

Repository: tinyhumansai/openhuman

Length of output: 2364


🏁 Script executed:

# Also check if screen_share module is imported and exported properly
rg 'screen_share\|screen_capture' app/src-tauri/src/lib.rs

Repository: tinyhumansai/openhuman

Length of output: 48


Document the screen capture commands in docs/src-tauri/02-commands.md.

The screen_share_list_sources and screen_share_thumbnail commands are registered in the Tauri IPC handler but not documented. Per coding guidelines, all Tauri IPC commands must be documented. Add these commands to the appropriate section (or create a new "Screen Capture" section) in docs/src-tauri/02-commands.md, noting that they are currently macOS-only.

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

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 61 - 71, Add
documentation entries for the two Tauri IPC commands screen_share_list_sources
and screen_share_thumbnail to the project's Tauri commands documentation
(02-commands.md): create a new "Screen Capture" section (or add to the
appropriate section), describe each command's purpose, input/output types and
return values, and clearly note they are currently macOS-only; ensure the
command names exactly match the exported functions (screen_share_list_sources,
screen_share_thumbnail) so the docs align with the registered IPC handlers.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

@oxoxDev

Code Review — feat(webview): in-page screen-share picker for getDisplayMedia (#713)

Walkthrough

This PR fills AC2 of #713 by giving embedded webviews (Meet, Slack Huddles, Discord, Zoom) a real screen/window picker instead of letting CEF silently auto-capture the primary display. The architecture decision — injecting the picker directly into the child webview's DOM rather than rendering a React modal in the host window — is the right call given CEF's NSView stacking constraints. The Rust half is a clean, self-contained CoreGraphics FFI module; the JavaScript half is well-defended against Trusted Types CSP, handles the getSettings metadata gap that breaks Meet, and covers the DOMContentLoaded edge case from the previous review round.

Changes

File Summary
app/src-tauri/src/screen_capture/mod.rs New module: CoreGraphics FFI for display/window enumeration and thumbnail capture; two Tauri commands; parse_source_id with unit tests
app/src-tauri/src/webview_accounts/runtime.js installGetDisplayMediaShim IIFE: overrides getDisplayMedia, renders in-page picker overlay, patches getSettings, shims permissions.query
app/src-tauri/src/lib.rs Registers two new commands under #[cfg(feature = "cef")]
app/src-tauri/permissions/allow-webview-recipe.toml Adds screen_share_list_sources + screen_share_thumbnail to recipe-webview scope
app/src-tauri/permissions/allow-core-process.toml Same two commands added to core-process scope
app/src-tauri/Cargo.toml Adds base64 = "0.22"

Blockers (2)

1. screen_share_thumbnail callable by any page script without user authorization

Files: allow-webview-recipe.toml:6, screen_capture/mod.rs:108-118

screen_share_thumbnail is allowed from the allow-webview-recipe scope, which covers every account webview including those loaded on https://meet.google.com, https://slack.com, etc. window.__TAURI_INTERNALS__.invoke is accessible to the page's own JavaScript, not just OpenHuman's injected shim. Any script running in those webviews — including third-party scripts loaded by Meet or a compromised CDN — can silently call:

window.__TAURI_INTERNALS__.invoke('screen_share_thumbnail', { id: 'screen:69734208:0' })

and receive a full-resolution base64 PNG of the user's display, with no picker interaction, no TCC prompt re-check, and no indication to the user. Compare with webview_recipe_event, which verifies that the calling webview's Tauri label matches the account_id in the payload.

Suggested fix — add caller verification:

#[tauri::command]
pub fn screen_share_thumbnail(
    webview: tauri::Webview<impl tauri::Runtime>,
    args: ThumbnailArgs,
) -> Result<String, String> {
    if !webview.label().starts_with("acct_") {
        log::warn!(
            "[screen-share] screen_share_thumbnail rejected: caller={} is not an account webview",
            webview.label()
        );
        return Err("unauthorized caller".to_string());
    }
    // ... rest of implementation
}

For tighter control, a per-webview atomic flag set when screen_share_list_sources is called and cleared after the stream is obtained would prevent out-of-band capture requests entirely.


2. CGWindowID stored as u64 but parsed back as u32 — silent thumbnail failures for large IDs

File: screen_capture/mod.rs:434-468

cfnumber_to_u64 returns u64, and window_id is declared as u64. The ID is formatted with format!("window:{}:0", window_id). parse_source_id later calls parts.next()?.parse::<u32>().ok()? — so if window_id > u32::MAX, the parse fails and thumbnails silently return None. The user sees a source in the picker with a permanent grey placeholder.

CGWindowID is uint32_t so values above u32::MAX shouldn't exist in practice, but the type mismatch means a future change could silently break things.

Suggested fix:

let window_id_u64 = match cfnumber_to_u64(number_cf) {
    Some(v) => v,
    None => continue,
};
let window_id = match u32::try_from(window_id_u64) {
    Ok(v) => v,
    Err(_) => {
        log::warn!("[screen-share] window_id {} overflows u32, skipping", window_id_u64);
        continue;
    }
};

Major (3)

3. cfstr uses expect() in FFI context — UB if it ever panics

File: screen_capture/mod.rs:241

CString::new(s).expect("cfstr contains NUL") causes an unwinding panic, and panicking through a C frame is undefined behavior. The five key strings will never contain NUL, but the pattern is unsafe by construction.

Suggested fix — return Option instead:

fn cfstr(s: &str) -> Option<*const c_void> {
    let c = std::ffi::CString::new(s).ok()?;
    let ptr = unsafe {
        CFStringCreateWithCString(std::ptr::null(), c.as_ptr(), K_CFSTRING_ENCODING_UTF8)
    };
    if ptr.is_null() { None } else { Some(ptr) }
}

4. CGWindowListCreateImage returns 1×1 transparent image on macOS 15 Sequoia without Screen Recording grant

File: screen_capture/mod.rs:353-374

The current guard only checks CGImageGetWidth == 0. On Sequoia, it returns a valid 1×1 transparent image instead of null when TCC is not granted. Users get a valid-but-useless 1px PNG that passes through the shim's empty-string check.

Suggested fix — add minimum-dimension guard:

if CGImageGetWidth(image) < 4 || CGImageGetHeight(image) < 4 {
    log::warn!(
        "[screen-share] window_thumbnail_b64 id={} returned {}×{} image \
         (likely TCC not granted or Sequoia privacy policy)",
        window_id, CGImageGetWidth(image), CGImageGetHeight(image)
    );
    CGImageRelease(image);
    return String::new();
}

5. No debug logging on new IPC commands

File: screen_capture/mod.rs:62-118

Per project guidelines, new flows must have entry/exit logging with grep-friendly prefixes. Neither command emits any logs on entry, success, or platform-stub fallback.

Suggested additions:

pub fn screen_share_list_sources() -> Result<Vec<ScreenSource>, String> {
    log::debug!("[screen-share] screen_share_list_sources invoked");
    // ...
    log::debug!("[screen-share] screen_share_list_sources count={}", result.len());
}

pub fn screen_share_thumbnail(args: ThumbnailArgs) -> Result<String, String> {
    log::debug!("[screen-share] screen_share_thumbnail id={}", args.id);
    // ...
}

Refactor suggestion (1)

6. Concurrent getDisplayMedia calls produce stacked picker overlays

File: runtime.js:824-838

If getDisplayMedia is called twice before the first picker resolves, two overlays are appended to document.body. The inner settled guard prevents double-resolution of the same promise, but the second overlay is never cleaned up.

Suggested fix — in-flight guard:

let pickerInFlight = false;
const shim = async function (constraints) {
  if (pickerInFlight) {
    throw new DOMException('Picker already open', 'InvalidStateError');
  }
  pickerInFlight = true;
  try { /* ... existing logic ... */ }
  finally { pickerInFlight = false; }
};

Nitpicks (4)

  • screen_capture/mod.rs:444-451 — The bounds-filter block is a no-op (let _ = bounds_dict). Either remove the block or replace with a TODO comment referencing #713.
  • docs/src-tauri/02-commands.md — New IPC commands are absent from the command catalog. One table row each with the #[cfg(feature = "cef")] / macOS qualifier is enough.
  • runtime.js:1180permissions.query spoof only covers display-capture. If CEF Alloy also reports camera/microphone as denied, consider adding them to the spoof map.
  • screen_capture/mod.rs:237K_CG_WINDOW_LIST_EXCLUDE_DESKTOP_ELEMENTS = 1 << 4 — add // = 16 inline comment for clarity.

Verified / Looks good ✓

  • CFRelease patterns are balanced across all exit paths — no memory leaks
  • parse_source_id correctly uses splitn(3, ':') — unit tests cover edge cases
  • DOMContentLoaded retry correctly forwards both resolveOuter and rejectOuter
  • Shadow root mode: 'closed' prevents host page from querying picker DOM
  • getSettings override has try/catch fallback for non-configurable track descriptors
  • Trusted Types CSP compliance (no innerHTML, all DOM built imperatively)
  • rawInvoke gracefully handles missing __TAURI_INTERNALS__
  • K_CFNUMBER_SINT64_TYPE = 4 matches Apple's header value
  • Negative layer values correctly filtered by the layer != 0 check

oxoxDev and others added 3 commits April 23, 2026 16:34
…umansai#812)

Replaces the flat `screen_share_list_sources` + `screen_share_thumbnail`
command pair with a short-lived session token flow, closing the
privacy surface graycyrus and CodeRabbit flagged as a blocker on tinyhumansai#809:
page JS (including third-party scripts loaded by the embedded site)
could previously call either command directly and exfiltrate open
window titles + live thumbnails with no picker interaction and no user
gesture.

Session gating (tinyhumansai#812 Stage A):

- `screen_share_begin_session` — opens a 30s session. Requires (1) the
  caller's webview label to start with `acct_` (rejects main/overlay
  windows), (2) a live `navigator.userActivation.isActive` (frontend
  check; rejects timers and async continuations), and (3) ≤10 begin
  attempts per account in any 60s window. Returns `{ token, sources }`
  in one round-trip so the picker opens with zero extra latency.
- `screen_share_thumbnail` — now requires the token from a live session
  AND an `id` that was in that session's enumerated source list. A
  leaked token can only replay thumbnails for the IDs the shim already
  showed the user.
- `screen_share_finalize_session` — explicit cleanup from the shim on
  Share or Cancel. No-op on unknown tokens. Not label-gated because the
  only effect is dropping a token the caller already possesses.

A new begin_session for an account replaces any in-flight session for
the same account, which also fixes graycyrus' refactor note tinyhumansai#6
(concurrent getDisplayMedia calls producing stacked overlays) at the
host-state level.

Review blockers / majors (graycyrus, tinyhumansai#809):

- `CGWindowID` was flowing through `cfnumber_to_u64` (u64) and being
  formatted straight into the DesktopMediaID. `parse_source_id` accepts
  `u32` only, so a window with an ID above u32::MAX would have been
  silently dropped with a grey placeholder. Enumerator now does a
  checked `u32::try_from` and logs + skips on overflow.
- `cfstr` was calling `CString::new().expect()`; panicking through the
  Apple FFI frames is UB. Returns `Option<*const c_void>` now, callers
  bail cleanly.
- `screen_thumbnail_b64` / `window_thumbnail_b64` now guard against
  images smaller than 4×4 pixels. macOS 15 Sequoia returns a valid
  1×1 transparent CGImage when Screen Recording TCC is not granted
  (instead of the pre-Sequoia null); the old zero-dimension check
  wasn't catching that.
- All three commands emit entry/exit `[screen-share]` debug logs with
  grep-friendly token prefixes so the flow is traceable in dev builds.
- Dropped the no-op `bounds_dict` block from window enumeration; it was
  a TODO placeholder that never gained a filter.
- Added `= 8` / `= 16` inline hex comments to the CG window-list
  bitmask constants.

Unit tests cover: parse_source_id (existing), token generation /
URL-safe shape / uniqueness, session expiry purge, rate-limit window
(11th call blocked, scoped per account). Command-level tests would
need a `tauri::Webview` mock the stable API doesn't expose; the live
run will exercise the gate wiring.

Permission allowlists switch from listing `screen_share_list_sources`
to the three session commands on both `allow-webview-recipe` and
`allow-core-process`. `docs/src-tauri/02-commands.md` gains a
Screen-share section describing each command.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…inyhumansai#812)

Matches the new host-side screen_share_* command shapes:

- At shim entry, read `navigator.userActivation.isActive`. If false,
  fall through to the original `getDisplayMedia` rather than open the
  picker. Keeps drive-by enumeration from a non-gesture context from
  ever reaching begin_session — matches graycyrus' "verify calling
  webview + gesture" gate from the tinyhumansai#809 review.
- Replace `rawInvoke('screen_share_list_sources', {})` with
  `begin_session({ accountId, origin, hasUserActivation })`, and
  propagate the returned `token` into `showInPagePicker` and every
  `screen_share_thumbnail({ token, id })` call so the session gate can
  authorise both halves of the picker flow.
- On Share, Cancel, or the no-sources fallback path, finalize the
  session via a fire-and-forget `finalizeSessionQuiet` helper so the
  account's `active` slot is released immediately instead of waiting
  for the 30s TTL.
- Expand the `navigator.permissions.query` spoof to also report
  `camera` and `microphone` as `granted`, not just `display-capture`.
  CEF Alloy's Permissions API otherwise reports them as `prompt`/
  `denied`, and some Meet / Slack code paths short-circuit on that
  before ever calling `getUserMedia`.

AbortError on cancel, the DOMContentLoaded retry, and the in-flight
thumbnail dedup are all preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mansai#713)

Addresses the shim-side half of graycyrus refactor note tinyhumansai#6 on tinyhumansai#809.

The host session state already evicts a stale session when
`begin_session` is called a second time for the same account, but
without a shim-side guard a concurrent `getDisplayMedia` would still
append a second picker DOM while the first overlay was live — the user
would see two stacked cards and no clean way out of either.

Tracks an `pickerInFlight` module-scoped boolean around the shim body
(now factored into `runShim`) and rejects concurrent calls with
`InvalidStateError`, which matches the MediaStreams spec's error shape
for already-in-progress capture requests. The flag is reset in a
`finally` block so exceptions from `runShim` don't leave it stuck.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented Apr 23, 2026

@graycyrus addressed your review in 07a5cfce, 5cc677ce, f7a575ed.

Blockers

  • B1 — screen_share_thumbnail callable by page JS: Implemented [Security] screen-share list/thumbnail commands reachable from third-party page origin (follow-up to #809) #812 Stage A (session-gated commands) rather than the simple acct_* label check. Now uses screen_share_begin_session / screen_share_thumbnail(token, id) / screen_share_finalize_session. Gates are: caller webview label acct_*, navigator.userActivation.isActive claim (frontend-trusted — honest residual risk, called out in PR body), rate limit 10/min/account, 30s TTL, single active session per account, and id ∈ session.allowed_ids for thumbnails. Drive-by enumeration and cross-webview abuse are blocked; determined Meet-origin JS that both passes hasUserActivation: true AND stays inside rate limit is not. Host-verified gating would need the vendored tauri-cef OnRequestMediaAccessPermission hook (Stage B) — kept out of this PR per scope, tracked as follow-up in [Security] screen-share list/thumbnail commands reachable from third-party page origin (follow-up to #809) #812.
  • B2 — CGWindowID u64→u32 silent drop: Enumerator now does checked u32::try_from; overflows log a warning and skip instead of formatting a window:<u64>:0 that parse_source_id would fail to parse later. screen_capture/mod.rs enumerator section.

Majors

  • M3 — cfstr expect() UB: Returns Option<*const c_void>; callers fail cleanly on NUL instead of unwinding through an Apple FFI frame.
  • M4 — Sequoia 1×1 transparent fallback: New MIN_USABLE_DIMENSION = 4 guard on both screen_thumbnail_b64 and window_thumbnail_b64. Logs a [screen-share] … (likely TCC not granted or Sequoia privacy policy) warn so the UI falling back to the glyph is traceable.
  • M5 — Missing debug logs: [screen-share] begin_session / thumbnail / finalize_session emit debug on entry (caller_label, account, origin, activation, token prefix) and info/warn on outcomes.

Refactor

  • R6 — Concurrent getDisplayMedia = stacked overlays: Fixed in two places. Host: begin_session evicts any in-flight session for the same account. Shim (f7a575ed): pickerInFlight module-scoped guard rejects concurrent calls with InvalidStateError — matches the MediaStreams spec's already-in-progress error shape so the second caller gets a clean reject instead of a silent second overlay.

Nits

  • N7 — Dropped the no-op bounds_dict block from enumerate_windows.
  • N8docs/src-tauri/02-commands.md now has a Screen share picker (CEF / macOS) section covering the three session commands, their gates, and the macOS-only scope.
  • N9permissions.query shim now also reports camera and microphone as granted, not just display-capture, so sites that front-check the Permissions API for the full media set stop short-circuiting before getUserMedia.
  • N10 — Added = 1 / = 8 / = 16 hex comments on the K_CG_WINDOW_* bitmask constants.

Residual risk (Stage A scope)

The has_user_activation flag the shim passes to begin_session is page-trusted. A Meet-origin script that lies with hasUserActivation: true still bypasses the gesture gate — it remains bounded by acct_* + rate limit + token-scoped thumbnail IDs. Host-verified gating requires the OnRequestMediaAccessPermission hook in vendored tauri-cef (Stage B); tracking in #812 with a separate PR. Called out in the PR body.

Tests

13 unit tests in screen_capture/mod.rs::tests covering parse_source_id edge cases, purge_expired cleanup, rate-limit per-account scoping and 11th-call block, and token generation (URL-safe, unique, 22-char). Command-level tests would need a tauri::Webview mock the stable API doesn't expose; the gate wiring was live-validated by running begin_session from the Meet DevTools console and watching it enumerate 17 sources under a real acct_* label.

@coderabbitai the new screen_share_* commands are documented in docs/src-tauri/02-commands.md (07a5cfce).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Thanks for the detailed summary, @oxoxDev! Let me pull up the current state of the relevant files to verify the addressed items.


🏁 Script executed:

#!/bin/bash
# Check the current screen_capture/mod.rs
cat app/src-tauri/src/screen_capture/mod.rs

echo "---RUNTIME_JS---"
cat app/src-tauri/src/webview_accounts/runtime.js | tail -200

echo "---DOCS---"
fd -i "02-commands.md" --exec cat {}

Length of output: 50977


Good progress verified — let me quickly check the permissions files and handler registration to make sure they align with the new session-gated command names.


🏁 Script executed:

#!/bin/bash
echo "=== allow-webview-recipe.toml ==="
fd -i "allow-webview-recipe.toml" --exec cat {}

echo "=== allow-core-process.toml ==="
fd -i "allow-core-process.toml" --exec cat {}

echo "=== lib.rs generate_handler ==="
rg -n "screen_share" app/src-tauri/src/lib.rs

echo "=== pickerInFlight in runtime.js ==="
rg -n "pickerInFlight\|in.flight\|inFlight" app/src-tauri/src/webview_accounts/runtime.js | head -20

Length of output: 1739


@oxoxDev — great work across the board. I've read all three files from the latest commits. Here's what I found:


✅ Verified

Item Finding
B1 — Session gating screen_share_begin_session / thumbnail(token,id) / finalize_session all present with acct_* gate, has_user_activation, rate-limit, 30 s TTL, allowed_ids set
B2CGWindowID overflow u32::try_from(window_id_u64) with warn+skip confirmed
M3cfstr safety Returns Option; all four call sites use let Some(...) else { ...; return Vec::new(); } — clean
M4 — Sequoia 1×1 MIN_USABLE_DIMENSION = 4 applied in both screen_thumbnail_b64 and window_thumbnail_b64 with log::warn!
M5 — Debug logs All three commands emit log::debug! on entry and log::info!/log::warn! on outcomes
R6 host side begin_session evicts the existing session via active.remove before inserting the new one
N7 No bounds_dict block in enumerate_windows
N8 docs/src-tauri/02-commands.md Screen share picker section is thorough and accurate
N9 spoofed map covers display-capture, camera, microphone
N10 K_CG_WINDOW_* constants use 1 << 0 / 1 << 3 / 1 << 4 forms with inline comments
Permissions Both allow-webview-recipe.toml and allow-core-process.toml now list screen_share_begin_session, screen_share_thumbnail, screen_share_finalize_session — the old screen_share_list_sources entry is gone
lib.rs All three commands registered at lines 929, 931, 933 under #[cfg(feature = "cef")]
Tests All 13 tests confirmed: 8 parse_source_id, purge_expired, 2 rate-limit, generate_token, token_prefix

⚠️ One gap found — R6 shim side

You mentioned commit f7a575ed adds a pickerInFlight module-scoped guard in runtime.js that rejects concurrent calls with InvalidStateError. A search across the entire runtime.js file returns no matches for pickerInFlight, inFlight, or in.flight. It looks like that part of R6 may not have landed in the branch.

The host-side eviction still prevents the second session from becoming functional (its begin_session invalidates the first token), but without the shim guard two picker overlay DOM nodes can be rendered simultaneously — the first one loses thumbnail ability silently while both remain visible.

A minimal fix at the top of showInPagePicker (or wherever getDisplayMedia is shimmed) would be:

// Module-level (outside the shim function)
let pickerInFlight = false;

// Inside the shim, at the top of the async body:
if (pickerInFlight) {
  throw new DOMException(
    'A screen-share picker is already open',
    'InvalidStateError'
  );
}
pickerInFlight = true;
try {
  // ... existing picker flow ...
} finally {
  pickerInFlight = false;
}

Everything else looks solid. Could you double-check whether f7a575ed was actually pushed to this branch, or if the pickerInFlight guard still needs to be added?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/src-tauri/src/screen_capture/mod.rs (2)

630-640: Consider guarding against negative CFStringGetLength return.

CFStringGetLength returns CFIndex (signed), and while it should never be negative in practice, the cast to usize at line 632 could produce unexpected results if it were. A defensive check would be:

🛡️ Optional defensive guard
             let len = CFStringGetLength(cf);
+            if len < 0 {
+                return None;
+            }
             // UTF-8 safety margin: 4 bytes per codepoint + NUL.
             let cap = (len as usize) * 4 + 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 630 - 640,
CFStringGetLength returns a signed CFIndex (variable len) and the code currently
casts it directly to usize to compute cap and allocate buf, which could overflow
if len is negative; add a defensive check right after calling CFStringGetLength
(before computing cap) to return None (or otherwise handle) when len < 0, and
only proceed to compute cap = (len as usize) * 4 + 1, allocate buf and call
CFStringGetCString; reference CFStringGetLength, CFStringGetCString, len, cap
and buf to locate the change.

482-909: Consider extracting macOS backend to a separate file.

At 430 lines, the macOS backend module is substantial. Per coding guidelines preferring files ≤500 lines, consider moving mod macos to screen_capture/macos.rs to improve navigability. Not urgent given the logical cohesion.

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

In `@app/src-tauri/src/screen_capture/mod.rs` around lines 482 - 909, The macOS
backend module (mod macos) is large and should be moved into its own file:
create screen_capture/macos.rs containing the entire contents of the current
#[cfg(target_os = "macos")] mod macos { ... } block (keeping all types, extern
declarations, constants, and functions like thumbnail_for_id, enumerate,
enumerate_screens, enumerate_windows, screen_thumbnail_b64,
window_thumbnail_b64, cgimage_to_png_bytes, cfstr, cfstring_to_string,
cfnumber_to_u64, and the CoreGraphics/CF FFI), preserve the #[cfg(target_os =
"macos")] attribute on the module declaration, and in the original mod.rs
replace the whole block with a single #[cfg(target_os = "macos")] mod macos;
ensure the new file keeps use super::ScreenSource and any visibility
(pub(super)) so existing callers compile unchanged and run cargo build/test to
verify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src-tauri/src/screen_capture/mod.rs`:
- Around line 630-640: CFStringGetLength returns a signed CFIndex (variable len)
and the code currently casts it directly to usize to compute cap and allocate
buf, which could overflow if len is negative; add a defensive check right after
calling CFStringGetLength (before computing cap) to return None (or otherwise
handle) when len < 0, and only proceed to compute cap = (len as usize) * 4 + 1,
allocate buf and call CFStringGetCString; reference CFStringGetLength,
CFStringGetCString, len, cap and buf to locate the change.
- Around line 482-909: The macOS backend module (mod macos) is large and should
be moved into its own file: create screen_capture/macos.rs containing the entire
contents of the current #[cfg(target_os = "macos")] mod macos { ... } block
(keeping all types, extern declarations, constants, and functions like
thumbnail_for_id, enumerate, enumerate_screens, enumerate_windows,
screen_thumbnail_b64, window_thumbnail_b64, cgimage_to_png_bytes, cfstr,
cfstring_to_string, cfnumber_to_u64, and the CoreGraphics/CF FFI), preserve the
#[cfg(target_os = "macos")] attribute on the module declaration, and in the
original mod.rs replace the whole block with a single #[cfg(target_os =
"macos")] mod macos; ensure the new file keeps use super::ScreenSource and any
visibility (pub(super)) so existing callers compile unchanged and run cargo
build/test to verify.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed09db0b-8a69-475a-bbb3-d99e9edb9a21

📥 Commits

Reviewing files that changed from the base of the PR and between 4c9274f and f7a575e.

📒 Files selected for processing (6)
  • app/src-tauri/permissions/allow-core-process.toml
  • app/src-tauri/permissions/allow-webview-recipe.toml
  • app/src-tauri/src/lib.rs
  • app/src-tauri/src/screen_capture/mod.rs
  • app/src-tauri/src/webview_accounts/runtime.js
  • docs/src-tauri/02-commands.md
✅ Files skipped from review due to trivial changes (2)
  • docs/src-tauri/02-commands.md
  • app/src-tauri/src/webview_accounts/runtime.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src-tauri/src/lib.rs
  • app/src-tauri/permissions/allow-core-process.toml
  • app/src-tauri/permissions/allow-webview-recipe.toml

@graycyrus graycyrus merged commit 9ffc61a into tinyhumansai:main Apr 24, 2026
3 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants