Implement Deep-Link Remote Control via Actor Dispatch (#1540)#1699
Implement Deep-Link Remote Control via Actor Dispatch (#1540)#1699denaev-dev wants to merge 2 commits intoCapSoftware:mainfrom
Conversation
| Self::TogglePauseRecording => { | ||
| app.emit("recording-action", "toggle-pause") | ||
| .map_err(|e| anyhow::anyhow!(e.to_string()))?; | ||
| Ok(()) | ||
| } | ||
| Self::StopRecording => { | ||
| app.emit("recording-action", "stop") | ||
| .map_err(|e| anyhow::anyhow!(e.to_string()))?; | ||
| Ok(()) | ||
| } | ||
| Self::TakeScreenshot => { | ||
| app.emit("recording-action", "screenshot") | ||
| .map_err(|e| anyhow::anyhow!(e.to_string()))?; | ||
| Ok(()) |
There was a problem hiding this comment.
recording-action event has no listener — core feature is non-functional
TogglePauseRecording, StopRecording, and TakeScreenshot all emit a recording-action event, but a codebase-wide search finds zero listeners for this event anywhere in the frontend or backend. The actual recording functions (toggle_pause_recording, stop_recording, take_screenshot) are Tauri commands and are called directly in hotkeys.rs and tray.rs — they are never wired to consume this event.
As a result, a deep-link like cap-desktop://toggle-pause will silently do nothing: the event is emitted and immediately dropped.
The implementation should directly call the recording functions (as hotkeys do) rather than relying on an event no-one subscribes to, e.g.:
Self::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state())
.await
.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}The same pattern applies to TogglePauseRecording → toggle_pause_recording and TakeScreenshot → take_screenshot.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 80-93
Comment:
**`recording-action` event has no listener — core feature is non-functional**
`TogglePauseRecording`, `StopRecording`, and `TakeScreenshot` all emit a `recording-action` event, but a codebase-wide search finds **zero listeners** for this event anywhere in the frontend or backend. The actual recording functions (`toggle_pause_recording`, `stop_recording`, `take_screenshot`) are Tauri commands and are called directly in `hotkeys.rs` and `tray.rs` — they are never wired to consume this event.
As a result, a deep-link like `cap-desktop://toggle-pause` will silently do nothing: the event is emitted and immediately dropped.
The implementation should directly call the recording functions (as hotkeys do) rather than relying on an event no-one subscribes to, e.g.:
```rust
Self::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state())
.await
.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
```
The same pattern applies to `TogglePauseRecording` → `toggle_pause_recording` and `TakeScreenshot` → `take_screenshot`.
How can I resolve this? If you propose a fix, please make it concise.
apps/desktop/src/utils/auth.ts
Outdated
| stopListening = (await listen<string[]>("tauri://deep-link", (event) => { | ||
| for (const urlString of event.payload) { | ||
| if (signal.aborted) return; | ||
| settle(parseAuthParams(new URL(urlString))); | ||
| } | ||
| }); | ||
| })) as unknown as () => void; |
There was a problem hiding this comment.
Unhandled
ZodError for non-auth deep-link URLs
parseAuthParams calls paramsValidator.parse(...), which throws a ZodError (not returns null) when the URL doesn't match the expected schema. Now that all cap-desktop:// URLs — including recording-control ones like cap-desktop://toggle-pause — are emitted on tauri://deep-link, any such URL received while the sign-in listener is active will throw an uncaught exception inside the event callback. This will surface as an unhandled error in the console and could leave the sign-in complete promise hanging indefinitely.
Use safeParse to avoid the throw:
| stopListening = (await listen<string[]>("tauri://deep-link", (event) => { | |
| for (const urlString of event.payload) { | |
| if (signal.aborted) return; | |
| settle(parseAuthParams(new URL(urlString))); | |
| } | |
| }); | |
| })) as unknown as () => void; | |
| stopListening = (await listen<string[]>("tauri://deep-link", (event) => { | |
| for (const urlString of event.payload) { | |
| if (signal.aborted) return; | |
| const result = paramsValidator.safeParse( | |
| [...new URL(urlString).searchParams].reduce( | |
| (acc, [key, value]) => { acc[key] = value; return acc; }, | |
| {} as Record<string, string> | |
| ) | |
| ); | |
| if (result.success) settle(result.data); | |
| } | |
| })) as unknown as () => void; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/auth.ts
Line: 178-183
Comment:
**Unhandled `ZodError` for non-auth deep-link URLs**
`parseAuthParams` calls `paramsValidator.parse(...)`, which **throws a `ZodError`** (not returns null) when the URL doesn't match the expected schema. Now that all `cap-desktop://` URLs — including recording-control ones like `cap-desktop://toggle-pause` — are emitted on `tauri://deep-link`, any such URL received while the sign-in listener is active will throw an uncaught exception inside the event callback. This will surface as an unhandled error in the console and could leave the sign-in `complete` promise hanging indefinitely.
Use `safeParse` to avoid the throw:
```suggestion
stopListening = (await listen<string[]>("tauri://deep-link", (event) => {
for (const urlString of event.payload) {
if (signal.aborted) return;
const result = paramsValidator.safeParse(
[...new URL(urlString).searchParams].reduce(
(acc, [key, value]) => { acc[key] = value; return acc; },
{} as Record<string, string>
)
);
if (result.success) settle(result.data);
}
})) as unknown as () => void;
```
How can I resolve this? If you propose a fix, please make it concise.
apps/desktop/src-tauri/src/lib.rs
Outdated
| // 2. Handle .cap files | ||
| let cap_file = args.iter() | ||
| .find(|arg| arg.ends_with(".cap")) | ||
| .map(PathBuf::from); | ||
|
|
||
| if let Some(file) = cap_file { | ||
| let _ = open_project_from_path(&file, app.clone()); | ||
| } | ||
|
|
||
| let _ = app.get_webview_window("main").map(|w| w.set_focus()); |
There was a problem hiding this comment.
Main window not shown when second instance is activated with no arguments
The previous implementation always brought up the main window when a second instance was launched with no .cap file:
// old code
ShowCapWindow::Main { init_target_mode: None }.show(&app).await;The new code replaces this with:
let _ = app.get_webview_window("main").map(|w| w.set_focus());get_webview_window("main") returns None if the window has been closed (which is normal in the app — the tray icon is the persistent entry point). In that case map is a no-op and the window is never shown, leaving the user with no visual feedback after activating the app.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/lib.rs
Line: 3319-3328
Comment:
**Main window not shown when second instance is activated with no arguments**
The previous implementation always brought up the main window when a second instance was launched with no `.cap` file:
```rust
// old code
ShowCapWindow::Main { init_target_mode: None }.show(&app).await;
```
The new code replaces this with:
```rust
let _ = app.get_webview_window("main").map(|w| w.set_focus());
```
`get_webview_window("main")` returns `None` if the window has been closed (which is normal in the app — the tray icon is the persistent entry point). In that case `map` is a no-op and the window is never shown, leaving the user with no visual feedback after activating the app.
How can I resolve this? If you propose a fix, please make it concise.
apps/desktop/src/utils/auth.ts
Outdated
| stopListening = (await listen<string[]>("tauri://deep-link", (event) => { | ||
| for (const urlString of event.payload) { | ||
| if (signal.aborted) return; | ||
| settle(parseAuthParams(new URL(urlString))); | ||
| } | ||
| }); | ||
| })) as unknown as () => void; |
There was a problem hiding this comment.
as unknown as () => void double-cast bypasses type safety
listen<string[]> returns Promise<UnlistenFn> where UnlistenFn = () => void. After await, the value should already be () => void. The as unknown as () => void double-cast suppresses TypeScript's type checker rather than fixing the underlying mismatch. The stopListening variable should be typed as UnlistenFn to align with the Tauri API's return type.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/auth.ts
Line: 178-183
Comment:
**`as unknown as () => void` double-cast bypasses type safety**
`listen<string[]>` returns `Promise<UnlistenFn>` where `UnlistenFn = () => void`. After `await`, the value should already be `() => void`. The `as unknown as () => void` double-cast suppresses TypeScript's type checker rather than fixing the underlying mismatch. The `stopListening` variable should be typed as `UnlistenFn` to align with the Tauri API's return type.
How can I resolve this? If you propose a fix, please make it concise.| TogglePauseRecording, | ||
| TakeScreenshot, | ||
| SetCamera { id: String }, | ||
| SetMicrophone { label: String }, | ||
| ListCameras, | ||
| ListMicrophones, |
There was a problem hiding this comment.
New enum variants are unreachable and unimplemented
SetCamera, SetMicrophone, ListCameras, and ListMicrophones are added to DeepLinkAction but:
- The URL parser has no matching
domain()arm for them, so they can never be constructed from a URL. - The
executeimplementation routes them to a_ =>catch-all that only logs a warning.
These variants are dead code as written. Either add the corresponding URL parsing and execution logic, or remove them until they are needed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 28-33
Comment:
**New enum variants are unreachable and unimplemented**
`SetCamera`, `SetMicrophone`, `ListCameras`, and `ListMicrophones` are added to `DeepLinkAction` but:
1. The URL parser has no matching `domain()` arm for them, so they can never be constructed from a URL.
2. The `execute` implementation routes them to a `_ =>` catch-all that only logs a warning.
These variants are dead code as written. Either add the corresponding URL parsing and execution logic, or remove them until they are needed.
How can I resolve this? If you propose a fix, please make it concise.b73d2bc to
e280c0e
Compare
d35aec1 to
b194e16
Compare
b194e16 to
cdfe952
Compare
|
Work is ready for review. Could a maintainer please authorize the Vercel deployment check so we can verify the preview build? |
Implement URI remote control via direct actor dispatch for desktop orchestration.
This improvement refactors the URI handling logic to communicate directly with the Rust-side actor system, ensuring reliable command execution and window restoration from the background.
Key Changes