diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
index 63771cc..fe6c268 100644
--- a/build/entitlements.mac.plist
+++ b/build/entitlements.mac.plist
@@ -7,5 +7,9 @@
com.apple.security.cs.allow-unsigned-executable-memory
+ com.apple.security.device.camera
+
+ com.apple.security.device.audio-input
+
diff --git a/docs/dev/release-builds.md b/docs/dev/release-builds.md
index 47af1a0..1551e01 100644
--- a/docs/dev/release-builds.md
+++ b/docs/dev/release-builds.md
@@ -67,7 +67,9 @@ xattr -cr /Applications/RokDock.app
A properly signed and notarized build generally launches without this.
-`electron-builder.json` references `build/entitlements.mac.plist` for the `hardenedRuntime` entitlements. That file is committed to the repository (it is force-tracked past the `build/*` rule in `.gitignore` via a `!build/entitlements.mac.plist` negation), so `dist:mac` resolves it. It is a minimal plist granting `com.apple.security.cs.allow-jit` and `com.apple.security.cs.allow-unsigned-executable-memory`, which is what an Electron hardened-runtime build needs.
+`electron-builder.json` references `build/entitlements.mac.plist` for the `hardenedRuntime` entitlements. That file is committed to the repository (it is force-tracked past the `build/*` rule in `.gitignore` via a `!build/entitlements.mac.plist` negation), so `dist:mac` resolves it. It grants `com.apple.security.cs.allow-jit` and `com.apple.security.cs.allow-unsigned-executable-memory` (what an Electron hardened-runtime build needs), plus `com.apple.security.device.camera` and `com.apple.security.device.audio-input` for the HDMI capture feature. The same file is used for both `entitlements` and `entitlementsInherit`, so the renderer/GPU helper processes that actually open the capture stream inherit the camera and audio-input entitlements too. Without that inheritance, capture is blocked even though the top-level app is entitled.
+
+Under `hardenedRuntime` the entitlements alone are not enough: macOS also requires usage-description strings, or it silently denies access with no prompt. `mac.extendInfo` in `electron-builder.json` injects `NSCameraUsageDescription` and `NSMicrophoneUsageDescription` into the packaged `Info.plist`. Note that `npm run dev` does not exercise these: the dev build runs under Electron's own bundle id (`com.github.Electron`), which ships its own camera string, so a signed `dist:mac` build is the only way to validate the capture permission path. Windows and Linux need no packaging changes for capture: Windows gates desktop apps behind a global camera/microphone privacy toggle, and Linux governs device access through `/dev/video*` ownership (the `video` group) and PulseAudio/PipeWire. See the user-facing [HDMI Capture Preview](../user/capture-preview.md) doc for the per-OS troubleshooting steps.
Windows and Linux builds are unsigned.
diff --git a/docs/user/capture-preview.md b/docs/user/capture-preview.md
index 164c9ca..778cd48 100644
--- a/docs/user/capture-preview.md
+++ b/docs/user/capture-preview.md
@@ -7,6 +7,29 @@ RokDock can display a live video feed from an HDMI capture device (a USB or HDMI
- An HDMI capture card or USB capture device connected to your computer
- The device must be recognized by your operating system before RokDock launches
+## Camera and Microphone Permissions
+
+Capture devices appear to the operating system as a camera (video) and, when they carry audio, a microphone. The first time RokDock opens a stream it may need permission to use them. If the preview stays black, the device list is empty, or audio never plays, an OS-level permission is usually the cause.
+
+### macOS
+
+macOS prompts for **Camera** and **Microphone** access the first time RokDock opens the stream. If you dismissed or denied the prompt, re-enable RokDock under **System Settings > Privacy & Security > Camera** (and **Microphone**), then restart the app.
+
+If the prompt never appeared and the toggles are absent, quit RokDock and reset its permission state so the prompt can fire again:
+
+```bash
+tccutil reset Camera com.rokdock.app
+tccutil reset Microphone com.rokdock.app
+```
+
+### Windows
+
+Windows has no per-app prompt for a desktop app like RokDock. Access is controlled by a single system toggle. Open **Settings > Privacy & security > Camera**, make sure **Camera access** is on, and enable **Let desktop apps access your camera**. Repeat under **Settings > Privacy & security > Microphone**. No per-app entry for RokDock will appear in these lists; the desktop-apps toggle is what governs it.
+
+### Linux
+
+Linux has no permission prompt. Access is governed by device ownership: your user must be able to read the capture device (typically membership in the `video` group for `/dev/video*`), and audio flows through PulseAudio or PipeWire with no extra permission. If the device is present but RokDock cannot open it, confirm your user is in the `video` group (`groups`), then log out and back in.
+
## Selecting a Capture Device
Configure the capture device in **Settings > Capture**, under the **Live Capture** section. The dropdown lists all video input devices detected by your system. If no devices appear, the dropdown shows "No capture devices detected."
diff --git a/electron-builder.json b/electron-builder.json
index 3521df1..2aa769f 100644
--- a/electron-builder.json
+++ b/electron-builder.json
@@ -108,7 +108,11 @@
"notarize": true,
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist",
- "entitlementsInherit": "build/entitlements.mac.plist"
+ "entitlementsInherit": "build/entitlements.mac.plist",
+ "extendInfo": {
+ "NSCameraUsageDescription": "RokDock uses the camera to capture live video from a connected Roku HDMI capture device.",
+ "NSMicrophoneUsageDescription": "RokDock uses the microphone input to capture audio from a connected Roku HDMI capture device."
+ }
},
"dmg": {
"artifactName": "RokDock-${version}-mac-${arch}.dmg",
diff --git a/package.json b/package.json
index 0c94507..2277510 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rokdock",
- "version": "1.6.0",
+ "version": "1.6.1",
"description": "Cross-platform desktop app for Roku development: device discovery, terminal sessions, remote control, sideloading, screenshot capture, automation scripting, and a built-in AI assistant with Gemini, Claude, Codex, and Copilot",
"main": "./out/main/main.js",
"scripts": {
diff --git a/src/main/ipc/handlers/updates.ts b/src/main/ipc/handlers/updates.ts
index a7ad6f7..7a44317 100644
--- a/src/main/ipc/handlers/updates.ts
+++ b/src/main/ipc/handlers/updates.ts
@@ -63,8 +63,8 @@ export function checkForUpdates(context: IpcContext): Promise
notes: releaseNotesText(info.releaseNotes),
})
const onNotAvailable = (): void => finish({ status: 'up-to-date', version: app.getVersion() })
- // The dialog shows a friendly message; keep the raw HTTP-layer error in the log for support.
- const onError = (err: Error): void => { logError('updates:check', err); finish({ status: 'error', error: err.message }) }
+ // The dialog shows a friendly message. The raw HTTP-layer error is kept in the log for support.
+ const onError = (err: Error): void => { logError('updates:check', err); finish({ status: 'error' }) }
autoUpdater.once('update-available', onAvailable)
autoUpdater.once('update-not-available', onNotAvailable)
@@ -75,7 +75,7 @@ export function checkForUpdates(context: IpcContext): Promise
// finish() is idempotent (resolve is a no-op after the first call).
autoUpdater.checkForUpdates()
.then(result => { if (!result) finish({ status: 'up-to-date', version: app.getVersion() }) })
- .catch((err: unknown) => { logError('updates:check', err); finish({ status: 'error', error: err instanceof Error ? err.message : String(err) }) })
+ .catch((err: unknown) => { logError('updates:check', err); finish({ status: 'error' }) })
})
}
diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx
index 8ee9b1d..2ee599b 100644
--- a/src/renderer/app.tsx
+++ b/src/renderer/app.tsx
@@ -101,9 +101,10 @@ export default function App() {
setUpdatesOpen(true)
void window.rokdock.updates.check()
.then(setUpdateResult)
- .catch((err: unknown) =>
- setUpdateResult({ status: 'error', error: err instanceof Error ? err.message : String(err) })
- )
+ .catch((err: unknown) => {
+ console.error('Update check failed:', err)
+ setUpdateResult({ status: 'error' })
+ })
}
// Sync CSS custom properties used by index.html global styles (non-React elements).
// These legacy aliases (no --rokdock- prefix) are kept for index.html compatibility and
diff --git a/src/renderer/components/updatesDialog.css b/src/renderer/components/updatesDialog.css
new file mode 100644
index 0000000..757cc2a
--- /dev/null
+++ b/src/renderer/components/updatesDialog.css
@@ -0,0 +1,68 @@
+/* Release-notes block in the Software Update dialog. The notes arrive as HTML from
+ the GitHub release and are rendered (sanitized) by react-markdown, so this styles
+ the resulting elements compactly for a small dialog. Monospace inside the notes is
+ sized down (0.85em) to match the app's code sizing elsewhere, since JetBrains Mono
+ renders larger than the proportional UI font at the same px. */
+
+.update-notes {
+ margin: 0;
+ max-height: 200px;
+ overflow-y: auto;
+ color: var(--rokdock-text-dim);
+ font-size: var(--rokdock-font-sm);
+ line-height: 1.5;
+}
+
+.update-notes h1,
+.update-notes h2,
+.update-notes h3,
+.update-notes h4 {
+ margin: 10px 0 4px;
+ font-size: var(--rokdock-font-base);
+ font-weight: 600;
+ color: var(--rokdock-text-primary);
+}
+
+.update-notes > :first-child {
+ margin-top: 0;
+}
+
+.update-notes p {
+ margin: 4px 0;
+}
+
+.update-notes ul,
+.update-notes ol {
+ margin: 4px 0;
+ padding-left: 18px;
+}
+
+.update-notes li {
+ margin: 2px 0;
+}
+
+.update-notes code {
+ font-family: var(--rokdock-font-mono);
+ font-size: 0.85em;
+ background: var(--rokdock-bg-terminal);
+ padding: 1px 4px;
+ border-radius: 4px;
+}
+
+.update-notes pre {
+ margin: 6px 0;
+ padding: 8px;
+ background: var(--rokdock-bg-terminal);
+ border: 1px solid var(--rokdock-border);
+ border-radius: 6px;
+ overflow-x: auto;
+}
+
+.update-notes pre code {
+ background: transparent;
+ padding: 0;
+}
+
+.update-notes a {
+ color: var(--rokdock-brand-primary-light);
+}
diff --git a/src/renderer/components/updatesDialog.tsx b/src/renderer/components/updatesDialog.tsx
index 3c45220..fa57dd4 100644
--- a/src/renderer/components/updatesDialog.tsx
+++ b/src/renderer/components/updatesDialog.tsx
@@ -10,9 +10,13 @@ import React, { useEffect, useState } from 'react'
import type { CSSProperties } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
+import ReactMarkdown from 'react-markdown'
+import rehypeRaw from 'rehype-raw'
+import rehypeSanitize from 'rehype-sanitize'
import DialogFrame from './common/dialogFrame'
import type { UpdateCheckResult } from '@shared/updates'
import type { IpcResult } from '@shared/types'
+import './updatesDialog.css'
const DIALOG_CLOSE_BTN: CSSProperties = {
width: 24,
@@ -31,26 +35,30 @@ const DIALOG_CLOSE_BTN: CSSProperties = {
const MESSAGE_STYLE: CSSProperties = {
margin: 0,
color: 'var(--rokdock-text-primary)',
- fontSize: 'var(--rokdock-font-xs)',
+ fontSize: 'var(--rokdock-font-sm)',
lineHeight: 1.45,
}
const SUBTLE_STYLE: CSSProperties = {
margin: 0,
color: 'var(--rokdock-text-dim)',
- fontSize: 'var(--rokdock-font-xs)',
+ fontSize: 'var(--rokdock-font-sm)',
}
-const NOTES_STYLE: CSSProperties = {
- margin: 0,
- maxHeight: 160,
- overflowY: 'auto',
- whiteSpace: 'pre-wrap',
- color: 'var(--rokdock-text-dim)',
- fontSize: 'var(--rokdock-font-xs)',
- lineHeight: 1.45,
+// Header row for the downloading state: title on the left, percent right-aligned.
+const DOWNLOAD_HEADER_STYLE: CSSProperties = {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'baseline',
+ gap: 8,
}
+// Rehype pipeline for the release notes: rehypeRaw reparses the raw HTML the GitHub
+// release ships, then rehypeSanitize (default schema) strips anything unsafe. Order
+// matters: raw MUST run before sanitize. Defined once at module scope so the array
+// identity is stable across renders.
+const NOTES_REHYPE_PLUGINS = [rehypeRaw, rehypeSanitize]
+
const PROGRESS_TRACK_STYLE: CSSProperties = {
width: '100%',
height: 6,
@@ -95,11 +103,14 @@ export default function UpdatesDialog({ result, onClose, onRetry }: {
if (downloading) {
body = (
<>
-
Downloading update...
+
+
Downloading update...
+ {percent}%
+
-
{percent}%. RokDock will restart to install when the download completes.
+
RokDock will restart to install when the download completes.