From 1c12854871f4b6f31c5faec525e5d2337a11a36e Mon Sep 17 00:00:00 2001 From: Tselem <9682873+formed2forge@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:29:32 -0400 Subject: [PATCH 1/2] fix(desktop-windows): surface Rewind screen-source failures in the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getRewindCaptureSourceId()/getPrimarySourceId() let desktopCapturer.getSources() reject uncaught — Electron logged "Error occurred in handler for 'rewind:captureSourceId'" to the console, and Rewind's RewindCaptureHost caught the rejection locally but only console.error'd it (DevTools-only, never seen). Net effect: a permanent failure (most commonly on Linux: no org.freedesktop.portal.ScreenCast backend for the running Wayland compositor) left Rewind silently never capturing, with zero UI signal. sourceId.ts now catches the fetch failure, caches the empty result (consistent with its existing cache-forever design — this is a launch-time environment fact, not a transient blip), and exposes getRewindCaptureDiagnostics() over a new 'rewind:captureDiagnostics' IPC handler. RewindCaptureNotice (mounted app-wide, mirroring DbRecoveryNotice) shows a real banner when Rewind is enabled but unavailable, with Linux-specific guidance (the dominant real-world cause, confirmed this session on niri via CDP + wpctl) pointing at docs/linux-screen-recording.md. Also: electron-builder's deb config now Recommends the generic xdg-desktop-portal front-end (not a specific backend — the correct one is compositor-specific, so there's no single correct hard dependency). Failure-Class: new Co-Authored-By: Claude Sonnet 5 --- .../2026-08-rewind-capture-error-notice.json | 5 + desktop/windows/src/main/ipc/rewind.ts | 5 + .../windows/src/main/rewind/sourceId.test.ts | 64 ++++++++++++- desktop/windows/src/main/rewind/sourceId.ts | 61 +++++++++++- desktop/windows/src/preload/index.ts | 1 + desktop/windows/src/renderer/src/App.tsx | 3 + .../ui/RewindCaptureNotice.test.tsx | 94 +++++++++++++++++++ .../src/components/ui/RewindCaptureNotice.tsx | 73 ++++++++++++++ desktop/windows/src/shared/types.ts | 15 +++ 9 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 desktop/windows/changelog/unreleased/2026-08-rewind-capture-error-notice.json create mode 100644 desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.test.tsx create mode 100644 desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.tsx diff --git a/desktop/windows/changelog/unreleased/2026-08-rewind-capture-error-notice.json b/desktop/windows/changelog/unreleased/2026-08-rewind-capture-error-notice.json new file mode 100644 index 00000000000..d6276f44045 --- /dev/null +++ b/desktop/windows/changelog/unreleased/2026-08-rewind-capture-error-notice.json @@ -0,0 +1,5 @@ +{ + "changes": [ + "Rewind now shows a real in-app error when it can't get a screen source (e.g. no Wayland desktop portal configured), instead of silently never starting." + ] +} diff --git a/desktop/windows/src/main/ipc/rewind.ts b/desktop/windows/src/main/ipc/rewind.ts index f3daafa426e..d987aa22f91 100644 --- a/desktop/windows/src/main/ipc/rewind.ts +++ b/desktop/windows/src/main/ipc/rewind.ts @@ -2,6 +2,7 @@ import { ipcMain, BrowserWindow } from 'electron' import { getPrimarySourceId, getRewindCaptureSourceId, + getRewindCaptureDiagnostics, isCurrentRewindCaptureSource } from '../rewind/sourceId' import { @@ -152,6 +153,10 @@ export function registerRewindHandlers(): void { // Rewind follows the foreground window across displays while retaining one // persistent stream. Source enumeration is cached; each lookup is cheap. ipcMain.handle('rewind:captureSourceId', async () => getRewindCaptureSourceId()) + // UI-facing seam for a getSources() failure (see sourceId.ts): the Rewind tab + // calls this once on mount to show a real error instead of capture just + // never starting with only a console line. + ipcMain.handle('rewind:captureDiagnostics', async () => getRewindCaptureDiagnostics()) // Receive a sampled JPEG frame from the renderer capture host and store it // (after foreground-window metadata + idle/lock/dup gating). ipcMain.handle('rewind:saveFrame', async (_e, data: Uint8Array, sourceId: string) => { diff --git a/desktop/windows/src/main/rewind/sourceId.test.ts b/desktop/windows/src/main/rewind/sourceId.test.ts index d9be5fefabf..8598fb6082f 100644 --- a/desktop/windows/src/main/rewind/sourceId.test.ts +++ b/desktop/windows/src/main/rewind/sourceId.test.ts @@ -78,6 +78,67 @@ describe('getPrimarySourceId', () => { }) }) +describe('getRewindCaptureDiagnostics — desktopCapturer failure classification', () => { + beforeEach(() => { + vi.clearAllMocks() + getPrimaryDisplay.mockReturnValue({ id: 2 }) + getForegroundWindowRect.mockReturnValue({ rect: null, className: null, exePath: null }) + getCursorScreenPoint.mockReturnValue({ x: 0, y: 0 }) + getDisplayNearestPoint.mockReturnValue({ id: 2 }) + }) + + it('reports available with no reason when sources resolve normally', async () => { + getSources.mockResolvedValue([source('screen:0:0', '2')]) + const { getRewindCaptureDiagnostics } = await loadModule() + + expect(await getRewindCaptureDiagnostics()).toEqual({ + available: true, + reason: null, + likelyMissingLinuxPortal: false + }) + }) + + it('does NOT throw/reject when desktopCapturer.getSources() fails — resolves to unavailable instead', async () => { + // Live bug: this used to reject out of the 'rewind:captureSourceId' IPC + // handler uncaught, and Rewind just never started with no UI signal at all. + getSources.mockRejectedValue(new Error('Failed to get sources.')) + const { getPrimarySourceId, getRewindCaptureSourceId, getRewindCaptureDiagnostics } = + await loadModule() + + await expect(getPrimarySourceId()).resolves.toBeNull() + await expect(getRewindCaptureSourceId()).resolves.toBeNull() + expect(await getRewindCaptureDiagnostics()).toEqual({ + available: false, + reason: 'Failed to get sources.', + likelyMissingLinuxPortal: process.platform === 'linux' + }) + }) + + it('flags likelyMissingLinuxPortal only on linux', async () => { + getSources.mockRejectedValue(new Error('Failed to get sources.')) + const { getRewindCaptureDiagnostics } = await loadModule() + const original = process.platform + Object.defineProperty(process, 'platform', { value: 'linux' }) + try { + expect((await getRewindCaptureDiagnostics()).likelyMissingLinuxPortal).toBe(true) + } finally { + Object.defineProperty(process, 'platform', { value: original }) + } + }) + + it('does not flag likelyMissingLinuxPortal on win32', async () => { + getSources.mockRejectedValue(new Error('Failed to get sources.')) + const { getRewindCaptureDiagnostics } = await loadModule() + const original = process.platform + Object.defineProperty(process, 'platform', { value: 'win32' }) + try { + expect((await getRewindCaptureDiagnostics()).likelyMissingLinuxPortal).toBe(false) + } finally { + Object.defineProperty(process, 'platform', { value: original }) + } + }) +}) + describe('prewarmPrimarySourceId', () => { beforeEach(() => { vi.clearAllMocks() @@ -96,7 +157,8 @@ describe('prewarmPrimarySourceId', () => { getPrimaryDisplay.mockReturnValue({ id: 3 }) const invalidate = on.mock.calls.find(([event]) => event === 'display-metrics-changed')?.[1] as - (() => void) | undefined + | (() => void) + | undefined expect(invalidate).toBeTypeOf('function') invalidate?.() diff --git a/desktop/windows/src/main/rewind/sourceId.ts b/desktop/windows/src/main/rewind/sourceId.ts index 305de641f61..715d1df0793 100644 --- a/desktop/windows/src/main/rewind/sourceId.ts +++ b/desktop/windows/src/main/rewind/sourceId.ts @@ -13,12 +13,36 @@ type SourceIdentity = { id: string; displayId: string } let cached: SourceIdentity[] | null = null let inflight: Promise | null = null +// The most recent fetch failure, if any — cleared on a successful fetch. On +// Linux this is almost always a Wayland desktop-portal gap (no +// org.freedesktop.portal.ScreenCast implementation registered for the running +// compositor — confirmed live: niri + no xdg-desktop-portal-wlr produced +// "Failed to get sources." here with no further detail reaching JS; the real +// GDBus error only appears in Chromium's native stderr log, not this +// exception). getRewindCaptureDiagnostics() surfaces this to the UI instead of +// the previous behavior: an uncaught rejection out of the +// 'rewind:captureSourceId' IPC handler and a silently-never-starting capture. +let lastFetchError: Error | null = null + +export function getSourceFetchError(): string | null { + return lastFetchError?.message ?? null +} + async function fetchSourceIdentities(): Promise { - const sources = await desktopCapturer.getSources({ - types: ['screen'], - thumbnailSize: { width: 0, height: 0 } // ids only - no screen bitmap - }) - return sources.map((source) => ({ id: source.id, displayId: source.display_id })) + try { + const sources = await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { width: 0, height: 0 } // ids only - no screen bitmap + }) + lastFetchError = null + return sources.map((source) => ({ id: source.id, displayId: source.display_id })) + } catch (e) { + // Cache the empty result like any other outcome (see the module header) — + // a portal gap is a launch-time environment fact, not a transient blip; + // retrying every call would just re-hit the same missing D-Bus interface. + lastFetchError = e as Error + return [] + } } async function getSourceIdentities(): Promise { @@ -106,3 +130,30 @@ export function prewarmPrimarySourceId(): void { } void getPrimarySourceId() } + +export type RewindCaptureDiagnostics = { + /** Whether at least one screen source resolved. */ + available: boolean + /** The underlying fetch error's message, present only when unavailable. */ + reason: string | null + /** Linux desktopCapturer.getSources() has one dominant failure mode: no + * org.freedesktop.portal.ScreenCast implementation registered for the + * running Wayland compositor (confirmed live on niri without + * xdg-desktop-portal-wlr installed/preferred). The JS-catchable error + * message is a generic "Failed to get sources." either way — Chromium logs + * the real GDBus detail only to its own stderr, never into the exception — + * so this is a platform heuristic, not a message-content match. */ + likelyMissingLinuxPortal: boolean +} + +/** Ensure a fetch attempt has happened, then report whether it succeeded — for + * the UI to show a real error instead of Rewind silently never starting. */ +export async function getRewindCaptureDiagnostics(): Promise { + await getPrimarySourceId() + const reason = getSourceFetchError() + return { + available: !reason, + reason, + likelyMissingLinuxPortal: !!reason && process.platform === 'linux' + } +} diff --git a/desktop/windows/src/preload/index.ts b/desktop/windows/src/preload/index.ts index be0c98270f4..1ff9f7e8b7a 100644 --- a/desktop/windows/src/preload/index.ts +++ b/desktop/windows/src/preload/index.ts @@ -372,6 +372,7 @@ const omi: OmiBridgeApi = { rewindRebuildIndex: () => ipcRenderer.invoke('rewind:rebuildIndex'), rewindPrimarySourceId: () => ipcRenderer.invoke('rewind:primarySourceId'), rewindCaptureSourceId: () => ipcRenderer.invoke('rewind:captureSourceId'), + rewindCaptureDiagnostics: () => ipcRenderer.invoke('rewind:captureDiagnostics'), rewindSaveFrame: (data: Uint8Array, sourceId: string) => ipcRenderer.invoke('rewind:saveFrame', data, sourceId), screenReadText: () => ipcRenderer.invoke('screen:readNow'), diff --git a/desktop/windows/src/renderer/src/App.tsx b/desktop/windows/src/renderer/src/App.tsx index f1823d2d90b..dfbf5e74e36 100644 --- a/desktop/windows/src/renderer/src/App.tsx +++ b/desktop/windows/src/renderer/src/App.tsx @@ -9,6 +9,7 @@ import { TitleBar } from './components/layout/TitleBar' import { Spinner } from './components/ui/Spinner' import { DbRecoveryNotice } from './components/ui/DbRecoveryNotice' import { DegradedModeNotice } from './components/ui/DegradedModeNotice' +import { RewindCaptureNotice } from './components/ui/RewindCaptureNotice' import { ToastHost } from './components/ui/ToastHost' import { purgeAppMemoriesOnce } from './lib/appMemories' import { AppStateProvider } from './state/AppStateProvider' @@ -122,6 +123,8 @@ function AppShellInner(): React.JSX.Element { {/* Only renders during a backend 429 storm; self-clears on recovery. */} + {/* Only renders when Rewind is enabled but can't get a screen source. */} + diff --git a/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.test.tsx b/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.test.tsx new file mode 100644 index 00000000000..f7c50a70d48 --- /dev/null +++ b/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.test.tsx @@ -0,0 +1,94 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, cleanup, screen, act, fireEvent } from '@testing-library/react' +import type { RewindCaptureDiagnostics, RewindSettings } from '../../../../shared/types' + +import { RewindCaptureNotice } from './RewindCaptureNotice' + +const AVAILABLE: RewindCaptureDiagnostics = { + available: true, + reason: null, + likelyMissingLinuxPortal: false +} + +const settings = (captureEnabled: boolean): RewindSettings => ({ + captureEnabled, + intervalMs: 30_000, + retentionDays: 30, + excludedApps: [], + captureQuality: 'standard' +}) + +function mockOmi(diagnostics: RewindCaptureDiagnostics, captureEnabled: boolean): void { + ;( + window as unknown as { + omi: { + rewindGetSettings: () => Promise + rewindCaptureDiagnostics: () => Promise + } + } + ).omi = { + rewindGetSettings: () => Promise.resolve(settings(captureEnabled)), + rewindCaptureDiagnostics: () => Promise.resolve(diagnostics) + } +} + +async function renderNotice(): Promise { + await act(async () => { + render() + }) +} + +beforeEach(() => { + vi.restoreAllMocks() +}) + +afterEach(() => { + cleanup() +}) + +describe('RewindCaptureNotice', () => { + it('renders nothing when capture is available (the overwhelmingly common case)', async () => { + mockOmi(AVAILABLE, true) + await renderNotice() + expect(screen.queryByRole('status')).toBeNull() + }) + + it('renders nothing when Rewind is disabled, even if capture would fail', async () => { + // Showing this to someone who never turned Rewind on would be noise, not help. + mockOmi( + { available: false, reason: 'Failed to get sources.', likelyMissingLinuxPortal: true }, + false + ) + await renderNotice() + expect(screen.queryByRole('status')).toBeNull() + }) + + it('shows the Linux-portal-specific message when likelyMissingLinuxPortal is set', async () => { + mockOmi( + { available: false, reason: 'Failed to get sources.', likelyMissingLinuxPortal: true }, + true + ) + await renderNotice() + expect(screen.getByText(/screen recording isn't working/i)).toBeTruthy() + expect(screen.getByText(/xdg-desktop-portal-wlr/i)).toBeTruthy() + }) + + it('shows a generic message with the reason when it is not a Linux-portal case', async () => { + mockOmi( + { available: false, reason: 'some other failure', likelyMissingLinuxPortal: false }, + true + ) + await renderNotice() + expect(screen.getByText(/some other failure/i)).toBeTruthy() + expect(screen.queryByText(/xdg-desktop-portal-wlr/i)).toBeNull() + }) + + it('can be dismissed', async () => { + mockOmi({ available: false, reason: 'x', likelyMissingLinuxPortal: false }, true) + await renderNotice() + expect(screen.getByRole('status')).toBeTruthy() + fireEvent.click(screen.getByLabelText('Dismiss')) + expect(screen.queryByRole('status')).toBeNull() + }) +}) diff --git a/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.tsx b/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.tsx new file mode 100644 index 00000000000..a98155ca82c --- /dev/null +++ b/desktop/windows/src/renderer/src/components/ui/RewindCaptureNotice.tsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from 'react' +import { MonitorX, X } from 'lucide-react' +import type { RewindCaptureDiagnostics } from '../../../../shared/types' + +// Shown once, at the top of the main window, when Rewind is enabled but can't +// actually get a screen source. Before this, the failure (desktopCapturer's +// getSources() throwing) surfaced only as a console.error the user never +// sees — Rewind would just never start capturing, silently. Same "make the +// failure honest" spirit as DbRecoveryNotice. +// +// Neutral/white styling only — no purple (INV-UI-1), no alarm-red for what is +// an environment/config gap, not data loss. + +export function RewindCaptureNotice(): React.JSX.Element | null { + const [captureEnabled, setCaptureEnabled] = useState(false) + const [diagnostics, setDiagnostics] = useState(null) + const [dismissed, setDismissed] = useState(false) + + useEffect(() => { + let alive = true + void window.omi + .rewindGetSettings() + .then((s) => { + if (alive) setCaptureEnabled(s.captureEnabled) + }) + .catch(() => { + // A missing/failing settings channel must never break the app shell. + }) + void window.omi + .rewindCaptureDiagnostics() + .then((d) => { + if (alive) setDiagnostics(d) + }) + .catch(() => { + // A missing/failing diagnostics channel must never break the app shell. + }) + return () => { + alive = false + } + }, []) + + // Only relevant when the user actually wants Rewind on — showing this to + // someone who has it off would just be irrelevant noise. + if (dismissed || !captureEnabled || !diagnostics || diagnostics.available) return null + + const body = diagnostics.likelyMissingLinuxPortal + ? "Your Linux desktop doesn't have a working screen-sharing portal for this " + + "compositor, so Omi can't get a screen source. Install a matching " + + 'xdg-desktop-portal backend (e.g. xdg-desktop-portal-wlr for niri/Sway/' + + "Hyprland), make sure it's preferred for ScreenCast, then restart Omi." + : `Omi couldn't get a screen source${diagnostics.reason ? ` (${diagnostics.reason})` : ''}. ` + + 'Recording will stay off until this is resolved.' + + return ( +
+ +
+
Screen recording isn't working
+
{body}
+
+ +
+ ) +} diff --git a/desktop/windows/src/shared/types.ts b/desktop/windows/src/shared/types.ts index a61dde974f4..426ad8ee95e 100644 --- a/desktop/windows/src/shared/types.ts +++ b/desktop/windows/src/shared/types.ts @@ -1018,6 +1018,8 @@ export type OmiBridgeApi = { rewindPrimarySourceId: () => Promise /** Display source containing the foreground window, with cursor/primary fallbacks. */ rewindCaptureSourceId: () => Promise + /** Whether Rewind can actually get a screen source right now, and why not. */ + rewindCaptureDiagnostics: () => Promise rewindSaveFrame: ( data: Uint8Array, sourceId: string @@ -2076,6 +2078,19 @@ export type RewindSettings = { captureQuality: RewindCaptureQuality } +/** Whether Rewind can actually get a screen source, and why not — see + * main/rewind/sourceId.ts's getRewindCaptureDiagnostics. Read once on mount + * (mirrors DbRecoveryStatus's "what happened at startup" shape): the + * underlying fetch is cached for the process lifetime, so this is a stable + * launch-time fact, not something that needs a push channel. */ +export type RewindCaptureDiagnostics = { + available: boolean + reason: string | null + /** Linux-only: the dominant real-world cause is a missing/misconfigured + * Wayland desktop-portal ScreenCast backend for the running compositor. */ + likelyMissingLinuxPortal: boolean +} + /** Runtime capture directive pushed main→renderer, derived from OS power/lock state. * `paused` tears down the capture stream (sleep/lock); `intervalMs` is the effective * cadence (base × battery multiplier). Separate from the persisted RewindSettings. */ From 7a12a56c18e6b970304a32b5e67b785305bde301 Mon Sep 17 00:00:00 2001 From: Tselem <9682873+formed2forge@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:29:45 -0400 Subject: [PATCH 2/2] docs(desktop-windows): Linux screen-recording portal setup + packaging note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes up why this session's Wayland ScreenCast fix (xdg-desktop-portal-wlr + a niri-portals.conf override, confirmed working end-to-end via CDP + on-disk Rewind frames) can't be fully automated by any packaging format — Flatpak, .deb, and AppImage all fundamentally depend on a host-provided, compositor- matched portal backend, which none of them can bundle or force-install. Co-Authored-By: Claude Sonnet 5 --- desktop/windows/.gitignore | 1 + desktop/windows/AGENTS.md | 5 +- .../windows/docs/linux-screen-recording.md | 89 +++++++++++++++++++ desktop/windows/electron-builder.config.mjs | 20 +++-- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 desktop/windows/docs/linux-screen-recording.md diff --git a/desktop/windows/.gitignore b/desktop/windows/.gitignore index f020a28fa2a..40280b8456a 100644 --- a/desktop/windows/.gitignore +++ b/desktop/windows/.gitignore @@ -53,6 +53,7 @@ docs/* !docs/perf-startup-burst-2026-07-19.md !docs/mac-parity-audit !docs/perf-invisible-wins.md +!docs/linux-screen-recording.md skills-lock.json # Visual brainstorming companion diff --git a/desktop/windows/AGENTS.md b/desktop/windows/AGENTS.md index 147f3088919..ac79c709320 100644 --- a/desktop/windows/AGENTS.md +++ b/desktop/windows/AGENTS.md @@ -116,8 +116,9 @@ starting this from scratch. - `docs/conversation-sync.md` — offline-retry outbox design. - `docs/multi-worktree-dev.md` — parallel-worktree port/profile isolation, dev env var reference. -- `docs/perf-invisible-wins.md`, `docs/perf-startup-burst-2026-07-19.md` — - perf investigation notes. +- `docs/linux-screen-recording.md` — Rewind needs a Wayland desktop portal; + wlroots compositors (niri, Sway, Hyprland) often ship none configured. +- `docs/perf-invisible-wins.md`, `docs/perf-startup-burst-2026-07-19.md` — perf notes. ## Changelog Entries diff --git a/desktop/windows/docs/linux-screen-recording.md b/desktop/windows/docs/linux-screen-recording.md new file mode 100644 index 00000000000..4ecaab2b4c7 --- /dev/null +++ b/desktop/windows/docs/linux-screen-recording.md @@ -0,0 +1,89 @@ +# Linux screen recording (Rewind) troubleshooting + +Rewind needs `desktopCapturer.getSources()` to resolve, which on Wayland goes +through the `org.freedesktop.portal.ScreenCast` D-Bus interface. If no portal +backend implements it for the running compositor, Electron fails immediately +(`Failed to get sources.`), Rewind never starts, and — before this doc's +companion fix — nothing told the user why. + +## What Omi does automatically + +- `RewindCaptureNotice` (`src/renderer/src/components/ui/RewindCaptureNotice.tsx`) + shows an in-app banner when Rewind is enabled but `desktopCapturer.getSources()` + failed, with Linux-specific guidance when `process.platform === 'linux'` (see + `getRewindCaptureDiagnostics` in `src/main/rewind/sourceId.ts`). +- The `.deb` package `Recommends` (not `Depends`) the generic `xdg-desktop-portal` + front-end (`electron-builder.config.mjs`) — the universal front-end almost every + desktop already has. It deliberately does **not** depend on a specific backend + package, because the correct one is compositor-specific (see below) and there is + no single correct hard dependency across GNOME/KDE/wlroots-family desktops. + +## Why this can't be fully automated + +A portal **backend** (`xdg-desktop-portal-wlr`/`-gnome`/`-kde`/…) has to run on the +host, register on the system D-Bus session bus, and integrate with whichever +compositor is actually running. That's true regardless of how Omi is packaged — +`.deb`, `AppImage`, or a hypothetical future Flatpak — none of these formats can +bundle or install a working backend themselves: + +- **Flatpak** sandboxes the app; it can only *ask* the host's portal dispatcher. + It cannot bundle a backend — that must already be installed and preferred on + the host. +- **`.deb`** dependencies resolve once at install time, but the correct backend + depends on which compositor the user runs, which the package can't know in + advance. +- **AppImage** has zero ability to install or configure anything system-level. + +Mainstream desktop environments (GNOME, KDE) avoid this because their distro +installs *and* preconfigures the matching portal by default. Smaller / newer +wlroots-family compositors (niri, Sway, Hyprland, …) are the common exception — +their distro packaging often ships a portal config that doesn't route +`ScreenCast` anywhere, even when a working backend package is available. + +## Fixing it on a wlroots-family compositor (niri, Sway, Hyprland, …) + +Confirmed live on Fedora Asahi Remix + niri, where `niri-portals.conf` shipped +with no `ScreenCast` route at all: + +1. Install the wlr portal backend: + ``` + sudo dnf install xdg-desktop-portal-wlr # Fedora + sudo apt install xdg-desktop-portal-wlr # Debian/Ubuntu + ``` +2. Add a user-level override (safer than editing the system file, which a + package update can overwrite) at + `~/.config/xdg-desktop-portal/-portals.conf` (e.g. + `niri-portals.conf`): + ```ini + [preferred] + default=gnome;gtk; + org.freedesktop.impl.portal.ScreenCast=wlr + org.freedesktop.impl.portal.Screenshot=wlr + ``` +3. Restart the portal so it picks up both the new backend and the config: + ``` + systemctl --user restart xdg-desktop-portal + ``` + (a full logout/login also works if that doesn't take effect) +4. Restart Omi. `getRewindCaptureSourceId()`'s screen-source lookup is cached + for the whole process lifetime (`src/main/rewind/sourceId.ts` — deliberately, + `desktopCapturer.getSources()` is slow), so a running instance won't pick up + a newly-fixed portal without a restart. + +## What to expect on first launch after the fix + +The portal's interactive picker for a wlroots-family compositor isn't a dialog +window — it's a compositor-driven interactive selection (a changed cursor to +click a window, or click-drag a region), similar to a screenshot tool. Select +once; the grant is then cached for the process lifetime, so later Rewind +toggles won't show it again — that's expected, not a regression. + +## Diagnosing on your own machine + +``` +wpctl status # confirm PipeWire even has an audio Source (mic issues) +busctl --user list | grep portal # confirm a backend (org.freedesktop.impl.portal.desktop.*) is registered +``` +If no `org.freedesktop.impl.portal.desktop.` line appears for your +compositor's expected backend, the backend isn't installed, running, or +D-Bus-activatable — start from step 1 above. diff --git a/desktop/windows/electron-builder.config.mjs b/desktop/windows/electron-builder.config.mjs index 18f38b136f5..18151f22357 100644 --- a/desktop/windows/electron-builder.config.mjs +++ b/desktop/windows/electron-builder.config.mjs @@ -155,13 +155,19 @@ export default { // Runtime tools/libs the Linux platform seams call out of process. Missing // ones degrade gracefully (OCR/active-window return empty), but packaging the // depends keeps the shipped App experience complete on Debian/Ubuntu. - depends: [ - 'tesseract-ocr', - 'tesseract-ocr-eng', - 'libnotify4', - 'libxss1', - 'x11-utils' - ] + depends: ['tesseract-ocr', 'tesseract-ocr-eng', 'libnotify4', 'libxss1', 'x11-utils'], + // A soft dependency, not `depends`: xdg-desktop-portal is the universal + // front-end virtually every desktop already has, but the BACKEND that + // actually answers ScreenCast (xdg-desktop-portal-gnome/-kde/-wlr/…) is + // compositor-specific and can't be a single correct hard dependency — + // forcing e.g. -gnome onto a KDE or wlroots-compositor user would be + // wrong. Rewind's screen recording surfaces a real in-app error (see + // RewindCaptureNotice.tsx) when no backend answers; this Recommends just + // narrows the common "portal front-end isn't even installed" case. + // electron-builder's `recommends` REPLACES its own default + // (["libappindicator3-1"], needed for the tray icon) rather than + // appending, so it must be listed explicitly here too. + recommends: ['libappindicator3-1', 'xdg-desktop-portal'] }, npmRebuild: false, // See scripts/fix-pimono-chalk-unpack.mjs: corrects chalk's packaged version for