Skip to content

feat(desktop): warn when the recording's microphone or camera disconnects - #4263

Open
richiemcilroy wants to merge 2 commits into
feat/gpui-recording-settings-menufrom
feat/gpui-device-disconnect-warnings
Open

richiemcilroy wants to merge 2 commits into
feat/gpui-recording-settings-menufrom
feat/gpui-device-disconnect-warnings

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Stacked on #4262.

When the microphone or camera disappears mid-recording, the GPUI app said nothing: the bar's issue card only covered errors, sleep pause and low storage (controls_window.rs), and the camera bubble had no "Camera disconnected" overlay (camera_window.rs listed it as a known gap). The Tauri app warned in both places (in-progress-recording.tsx:199-208, camera.tsx:66,162) and reopened the device when it came back. This brings that back.

sequenceDiagram
    participant W as Input watch (1 s poll)
    participant S as RecordingSession
    participant F as Feed the recording reads
    participant UI as Bar + bubble
    W->>S: selected mic / camera missing from the device list
    S->>UI: "Microphone disconnected. Silence will be used until it reconnects."
    Note over F: recording continues: mic source fills silence, camera track gets no frames
    W->>S: same device listed again
    S->>F: SetInput(same device) on the locked feed
    F-->>S: ready
    S->>UI: warning clears
Loading
  • Detection (session/input_watch.rs): while a recording is live (running or paused), poll the microphone names and camera device ids once a second, as Tauri's spawn_microphone_watcher / spawn_camera_watcher did. Only the inputs the recording uses are watched: an input switched to "None", or one with a switch in flight, is never reported missing.
  • Keeps recording: nothing stops. The microphone source already fabricates keepalive silence for a dead device; the camera track simply has no new frames until it returns.
  • Reconnect: when the same device is listed again it is reopened on the feed the recording reads from. A locked feed accepts the same device back without a new lock (LockedInputReconnected), so Studio and Instant both resume on it. After an Instant mic switch (feat(desktop): recording settings menu with mid-recording microphone and camera switching #4262) that is the switched-to feed. The warning clears only once the reopen succeeds; a failed reopen retries on the next poll.
  • Bar: the issue card shows the Tauri wording for each lost input; the mic indicator turns to the red mic-off icon with no level while disconnected.
  • Bubble: the existing issue overlay shows "Camera disconnected / The selected camera stopped sending video." while the camera is gone.

Edge cases

Case Behaviour
Unplug while paused Detected (the watch runs while paused); reopened on return
Unplug, then stop Stop is unaffected; the watch ends with the recording
Device switched to "None" Not watched, never warns
Device returns but fails to open Warning stays, retried every second
Next recording State resets when a recording starts

Tests

  • session::input_watch::tests: a missing device warns and its return asks for a reopen while the warning stays until the reopen succeeds; an input the recording stopped using never warns.
  • i18n surface pairs for the four new strings in every locale.

Proof

Pending (hardware unplug runs on the Mac mini are blocked on a microphone permission grant); this section will carry the unplug/replug recordings and A/V sync against main.

RetriggerView in GreptileConfidence Score: 4/5

The behavior issues are non-blocking, but the explicit comments policy must be satisfied before merging.

Findings

  1. P2 Camera warning outlives recording ▶
  2. P2 Disabled microphone still warns ▶
  3. P2 Old poll restarts replacement camera ▶
  4. P2 Method documentation describes another action ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/src/session/input_watch.rs:71-72
`disconnected_inputs()` keeps returning the last recording's flags after it stops. If the camera disconnects and the user stops before reconnecting it, the watch exits without clearing the flag. The bubble reads that flag outside a recording, so selecting a working camera still leaves it covered by “Camera disconnected” until another recording starts. Clear the flags when recording ends, or return no disconnected inputs outside `Phase::Recording`.

### Issue 2
apps/desktop-gpui/src/session/input_watch.rs:109-112
An Instant recording still watches the microphone after the user chooses “No Microphone.” That choice only sets `mic_muted`; it leaves `feeds.microphone` and `active.mic_mute` populated. This filter therefore keeps watching the disabled microphone and raises a warning if it is unplugged. Skip watching it while Instant has it muted, while preserving the ability to reopen it when enabled again.

### Issue 3
apps/desktop-gpui/src/session/input_watch.rs:88-93
The poll result is applied without checking which devices it describes. A user can switch from unplugged camera A to working camera B while enumeration runs. A's result then marks B disconnected; the next poll unnecessarily restarts B and interrupts its preview. Carry `WatchedInputs` back with the result and discard results for inputs that changed or now have a switch pending.

### Issue 4
apps/desktop-gpui/src/recording.rs:871
The new comment above `switched_microphone_actor()` repeats the method's name and leaves the existing marker documentation attached to the wrong method. `AGENTS.md` requires comments to explain non-obvious context rather than narrate the code. Remove the redundant line and move the marker documentation back to `add_marker()`. The new comments above `WatchedInputs` and `disconnected_issue()` also repeat their code. This repository requirement must be satisfied before merging.

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!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds recording-time microphone and camera disconnect warnings, retries returned devices, and adds translated text for the controls bar and camera bubble.

  • Recordings watch their selected devices and reopen them when they return.
  • The recording bar warns when a microphone or camera disconnects.
  • The camera bubble explains when its recording feed disconnects.

Review used source inspection. Hardware unplug/replug behavior and native UI rendering were not run.

Reviews (1) · Last reviewed commit: "style: rustfmt" · Reviewed by Greptile

@vercel

vercel Bot commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Skipped Skipped Nov 14, 2026 7:05am UTC

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 14, 2026 07:05 Inactive
@sandchest-com

sandchest-com Bot commented Nov 14, 2026

Copy link
Copy Markdown

Sandchest

Sandchest reviewed 6b34355: nothing exploitable found in this change.

Sandchest reviews every change for vulnerabilities an attacker could use · 3 credits · Review details

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found 2 potential issues (2 medium).

View 1 other finding in Capy.

Open in Capy

Comment on lines +154 to +158
let actor = self
.active
.as_ref()
.and_then(|active| active.switched_microphone_actor())
.or_else(|| Feeds::global(cx).read(cx).mic_actor());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reconnect skips recording-owned fallback feeds

Medium Severity

When start_attempt_with_upload cannot lock an app feed or receives no live app actor, it opens a recording-owned _mic_feed or _camera_feed. These are distinct from the Instant-switched microphone feed. After unplug/replug, this actor selection ignores _mic_feed, so it either returns without reopening or reopens an unrelated app feed. reopen_camera likewise only restarts the app preview. The recording therefore continues without microphone audio or camera frames even if the preview recovers and clears its warning. Retain and reconnect the actual feeds locked by the active recording, including startup fallbacks.

Open in Capy

.is_some_and(|session| session.read(cx).disconnected_inputs().camera);
let session_subscription = session.map(|session| {
cx.observe(&session, |this: &mut Self, session, cx| {
let disconnected = session.read(cx).disconnected_inputs().camera;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Camera disconnect warnings persist after recording stops

Medium Severity

Disconnect the camera during recording, stop before reconnecting it, then reconnect or select a working camera in the idle preview. RecordingSession::finish leaves input_watch.disconnected.camera set, the watcher exits outside Phase::Recording, and only the next recording activation resets it. Both this subscription and the preview initializer consume the stale flag without a phase check, so the disconnect overlay remains over healthy video. Clear recording-scoped disconnect state on completion and prevent late reconnect callbacks from restoring stale state, or gate the preview flag to a live recording.

Open in Capy

Comment on lines +71 to +72
pub fn disconnected_inputs(&self) -> DisconnectedInputs {
self.input_watch.disconnected

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 Camera warning outlives recording

disconnected_inputs() keeps returning the last recording's flags after it stops. If the camera disconnects and the user stops before reconnecting it, the watch exits without clearing the flag. The bubble reads that flag outside a recording, so selecting a working camera still leaves it covered by “Camera disconnected” until another recording starts. Clear the flags when recording ends, or return no disconnected inputs outside Phase::Recording.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/session/input_watch.rs
Line: 71-72

Comment:
**Camera warning outlives recording**

`disconnected_inputs()` keeps returning the last recording's flags after it stops. If the camera disconnects and the user stops before reconnecting it, the watch exits without clearing the flag. The bubble reads that flag outside a recording, so selecting a working camera still leaves it covered by “Camera disconnected” until another recording starts. Clear the flags when recording ends, or return no disconnected inputs outside `Phase::Recording`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +109 to +112
microphone: feeds
.microphone
.clone()
.filter(|_| active.mic_mute.is_some() && switching != Some(InputKind::Microphone)),

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 Disabled microphone still warns

An Instant recording still watches the microphone after the user chooses “No Microphone.” That choice only sets mic_muted; it leaves feeds.microphone and active.mic_mute populated. This filter therefore keeps watching the disabled microphone and raises a warning if it is unplugged. Skip watching it while Instant has it muted, while preserving the ability to reopen it when enabled again.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/session/input_watch.rs
Line: 109-112

Comment:
**Disabled microphone still warns**

An Instant recording still watches the microphone after the user chooses “No Microphone.” That choice only sets `mic_muted`; it leaves `feeds.microphone` and `active.mic_mute` populated. This filter therefore keeps watching the disabled microphone and raises a warning if it is unplugged. Skip watching it while Instant has it muted, while preserving the ability to reopen it when enabled again.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +88 to +93
let presence = cx
.background_executor()
.spawn(async move { list_presence(&watched) })
.await;
if this
.update(cx, |this, cx| this.apply_presence(generation, presence, cx))

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 Old poll restarts replacement camera

The poll result is applied without checking which devices it describes. A user can switch from unplugged camera A to working camera B while enumeration runs. A's result then marks B disconnected; the next poll unnecessarily restarts B and interrupts its preview. Carry WatchedInputs back with the result and discard results for inputs that changed or now have a switch pending.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/session/input_watch.rs
Line: 88-93

Comment:
**Old poll restarts replacement camera**

The poll result is applied without checking which devices it describes. A user can switch from unplugged camera A to working camera B while enumeration runs. A's result then marks B disconnected; the next poll unnecessarily restarts B and interrupts its preview. Carry `WatchedInputs` back with the result and discard results for inputs that changed or now have a switch pending.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

/// Marks `at` in a Studio recording and returns the new marker count.
/// Instant recordings have no editor timeline to put a marker on, so
/// they take none.
/// The feed a microphone switch moved the recording onto, if any.

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 Method documentation describes another action

The new comment above switched_microphone_actor() repeats the method's name and leaves the existing marker documentation attached to the wrong method. AGENTS.md requires comments to explain non-obvious context rather than narrate the code. Remove the redundant line and move the marker documentation back to add_marker(). The new comments above WatchedInputs and disconnected_issue() also repeat their code. This repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/recording.rs
Line: 871

Comment:
**Method documentation describes another action**

The new comment above `switched_microphone_actor()` repeats the method's name and leaves the existing marker documentation attached to the wrong method. `AGENTS.md` requires comments to explain non-obvious context rather than narrate the code. Remove the redundant line and move the marker documentation back to `add_marker()`. The new comments above `WatchedInputs` and `disconnected_issue()` also repeat their code. This repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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!

This branch was successfully deployed

1 active and 1 inactive deployments
Preview – cap-web — 6b343551 Deployed Nov 14, 2026 by vercel[bot]
Preview – cap-status — 6b343551 Deployed Nov 14, 2026 by vercel[bot]
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.

1 participant