Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Shares view (master/detail sidebar + in-app webview viewer for shared links) (part 2)

Sidebar entry. Registered in INTEGRATIONREGISTRY (left of the existing Settings / Skills rows in the Omniscio group) as a virtual project with sentinel SHARESPROJECTID (from src/shared/virtual-project-ids.ts). UI registry shape.

What it is

This is part 2 of the Shares view (master/detail sidebar + in-app webview viewer for shared links) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.

Where to find it

Reach this part through Shares view (master/detail sidebar + in-app webview viewer for shared links) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.

How it behaves

Everything below is the behaviour, detail and edge cases that belong to this stretch of the Shares view (master/detail sidebar + in-app webview viewer for shared links) page.

How it works

  • Sidebar entry. Registered in INTEGRATION_REGISTRY (left of the existing Settings / Skills rows in the Omniscio group) as a virtual project with sentinel SHARES_PROJECT_ID (from src/shared/virtual-project-ids.ts).
  • UI registry shape. The Shares entry in src/renderer/src/integrations/ui-registry.ts is a two-component integration: panelComponent: lazy(() => import('../features/shares/SharesDetailPane')) + sidebarComponent: lazy(() => import('../features/shares/SharesSidebar')). Both components are lazy-loaded — the integration-ui-registry-lazy lint test enforces no static imports from the entry chunk. There is no panelOwnsLayout flag on Shares; the standard RoutedSubSidebar mounts SharesSidebar in the normal sidebar slot. Mirrors the Cron Jobs two-pane shape.
  • Activation. All navigation that wants to land on this view MUST route through activateVirtualProject(SHARES_PROJECT_ID, source) from src/renderer/src/lib/activate-virtual-project.ts — that helper resolves the sentinel to the project-row UUID before calling setActiveProject (passing the sentinel directly to setActiveProject silently no-ops, per the virtual-project activation contract).
  • Share URL interception. When any call to openExternalUrl encounters a URL matching a share host (https://shares.omniscio.com/s/<token> — the brand default — or the legacy https://agentmc-shares.web.app/s/<token>; regex SHARE_URL_RE, built from the shared SHARE_HOSTING_DOMAINS list, in src/renderer/src/lib/platform.ts), the share is opened in-app instead of launching an external browser. The function calls resolveShareUrlToken(token, opts) from the registration-seam module src/renderer/src/lib/share-url-intercept-access.ts, which looks up the token in the local share list. A #comments deep link takes the detail-pane path (select the row, arm the comment scroll, activate the Shares virtual project) and needs the loaded row, so an unsynced token there falls through to the system browser. Otherwise it opens the full-size in-app share popup — a large centered card over a dim, click-to-close backdrop (Escape, Ctrl+W, or the header X close it too — and Escape/Ctrl+W close it even when focus is inside the share's <webview>, forwarded from the main process via the WEBVIEW_GUEST_DISMISS push; see keyboard-shortcut-dispatch-contract.md webview-close-gesture-closes-the-overlay), dropped below the 50px CustomTitlebar strip (topOffset) so the window's min/maximize/close controls stay un-dimmed and clickable; it mirrors the ImageLightboxShell media-modal pattern and matches ShareViewerFullscreen's a11y (portal + role="dialog" + Escape, no aria-modal/focus-trap so it never fights the embedded webview's focus) — via openFloatingShare(token). Any valid share token opens in-app when sharesOpenInApp is on — owned OR a teammate's (owner decision 2026-08-11: shares open in-app everywhere they appear — team chat, AI chat, inbox — not just your own). An OWNED token renders with its full metadata (label, staleness, reload-on-republish); a NON-owned token has no local row, so FloatingShareViewer synthesizes a minimal card whose <webview> src is built from the token via buildShareUrl (src/shared/share-hosting-domains.ts) — the canonical, fixed-domain https://shares.omniscio.com/s/<token>, never the link's raw href. openFloatingShare still fetches the owned list on demand, so an owned-but-unsynced just-published token upgrades to its rich metadata once it lands. When sharesOpenInApp is OFF the resolver returns false and the caller opens the system browser (unchanged); a #comments deep link still needs the loaded row, so an unsynced token there falls through to the browser. Security invariant (build-from-token — this REPLACES the former own-only rule): the viewer's <webview> src is ALWAYS built from the VALIDATED token, so it can only ever load a well-formed Omniscio share page — a malformed token makes buildShareUrl return null and the viewer simply stays closed, and a raw/hostile URL can never be coerced into the privileged viewer. Locked by tests/unit/shared/share-hosting-domains.test.ts (buildShareUrl charset/length + null-on-malformed), tests/unit/features/shares/FloatingShareViewer.test.tsx (a non-owned token opens by building the canonical URL; a malformed token stays closed), and tests/unit/stores/share-store.test.ts (the ambient path opens in-app when the setting is on, browser when off). The app-wide freshness that makes agent-published shares resolve reliably comes from SharesLiveSync (below). A bypass function openExternalUrlForce(url) skips interception entirely — used by the viewer's own "Open in browser" buttons, the right-click "Open in browser" item on an in-app share link in chat (CopyableLink → CopyableLinkMenu; the escape hatch that opens the real share URL in the system browser instead of the in-app popup, shown for a share link that renders in-app — owned or a teammate's — since a plain external link already opens externally on click), and by DeckPublishedDialog (which shows a just-published share URL that would otherwise be recursively intercepted). The registration-seam pattern (mirrors session-nav-store-access.ts) prevents an import cycle: platform.ts is a foundational leaf that ipc.ts imports at module scope, so it cannot statically import share-store.ts (which imports ipc.ts).
  • Share hosting domain (brand + legacy). New share links mint on the brand domain https://shares.omniscio.com (the product-wide default); the original https://agentmc-shares.web.app stays fully served, so every already-sent link keeps resolving — both hostnames point at the SAME Firebase Hosting site, so content is byte-identical and the serveShare Cloud Function needs no per-host logic (the unlock cookie is Path=/s/<token>, host-only — no redeploy required). The two hostnames live in ONE module, src/shared/share-hosting-domains.ts (PRIMARY_SHARE_HOSTING_DOMAIN + LEGACY_SHARE_HOSTING_DOMAIN + the SHARE_HOSTING_DOMAINS allowlist), which the URL generator (firebase-publisher) and every matcher (interception regex, CSP frame-src, framing-header strip, team-chat link classifier, broadcast-embed origin allowlist) import — so a future domain change lands in one place. The per-user firebaseHostingDomain setting still overrides the default. The /t/* relay endpoints are NOT share links and deliberately stay on agentmc-shares.web.app.
  • Deep-link route for shares. omniscio://share/<token> (or agentmc://share/<token>) opens a share in the built-in viewer. Parsed by parseDeepLinkUrl() as { action: 'open-share-in-viewer', token } — a bare token (no sub-action) triggers the viewer route; the existing share/fork/<token> sub-action is unaffected. The renderer's routeDeepLinkAction dispatches to openShareInViewer(token) (wired in useDeepLinkListener to resolveShareUrlToken(token, { openInViewer: true })), which navigates to the Shares panel and opens the share in view mode. If the token is unknown locally, the renderer shows an error toast. Builder: shareViewerDeepLink(token) in src/main/services/deep-link.ts.
  • CSP frame-src. The webview viewer and any remaining iframe usage requires BOTH share hosts in the renderer CSP frame-src (https://shares.omniscio.com + the legacy https://agentmc-shares.web.app) in src/renderer/index.html. Without them, framed content from the share host is blocked. Pinned by tests/unit/lib/shares-iframe-csp.test.ts so a future CSP hardening pass can't drop the directive silently.
  • Response-header override. The Cloud Function correctly serves share shell pages with X-Frame-Options: DENY and CSP frame-ancestors 'none' (anti-clickjacking for external browsers). The Electron main process strips these two framing headers from share-host responses loaded as sub-frames via session.webRequest.onHeadersReceived (src/main/services/share/share-preview-headers.ts, registered in src/main/app/window.ts). Only subFrame resource types on /s/<token> and /s/<token>/run paths are affected — main-frame navigations and all other resource types pass through unmodified. Both the frame-src CSP allowlist AND this header override are required for the webview to load share content.
  • Links inside a share open in your browser. A link a user clicks inside a rendered share's artifact — a sub-frame navigation to an off-origin site — opens in the system browser instead of dying inside the sandboxed <iframe> (a bare in-artifact link previously loaded into the tiny frame and silently failed against the target's X-Frame-Options). The main process wires a will-frame-navigate handler in src/main/webcontents-hardening.ts that externalizes off-origin http(s) navigations originating from a share-owned frame (the artifact itself + the full-bleed about:srcdoc fit-host, decided by the exported isShareOwnedFrame helper) through the validated shell.openExternal path — while a nested off-origin embed the artifact legitimately included (e.g. a YouTube iframe) keeps loading/navigating in place, same-origin sub-resource loads (the shell's own /run frame) pass through untouched, and same-document #anchor links (which never fire will-frame-navigate) are unaffected. That handler covers the common plain link (will-navigate fires for the main frame only, so it never saw the sub-frame click). A target="_blank" link — which is what the report template's source link and the "Made with Omniscio" badge emit — takes a different path: it is a window.open, externalized by setWindowOpenHandler, and Electron refuses a <webview> guest's window.open outright unless the tag carries allowpopups="true". The refusal happens below the app, so until the share viewer opted in (src/renderer/src/features/shares/ShareViewerWebview.tsx) setWindowOpenHandler was never consulted and clicking a _blank link inside a share did nothing at all. Opting in grants no privilege — the handler still returns deny for every popup, so no window opens in-app; it only gets the chance to hand the URL to shell.openExternal. The same shared hardening also benefits the shared-partition Gmail link viewer. Locked by tests/unit/main/webcontents-hardening.test.ts (the externalize path) + tests/unit/features/shares/ShareViewerWebview.test.tsx (the allowpopups opt-in).
  • In-page #anchor nav works inside srcdoc-hosted shares. A separate issue from the off-origin externalization above: a responsive/fixed-width full-bleed artifact renders in an about:srcdoc frame (src/main/services/share/share-artifact-builder.ts / buildFitHost), whose base URL is inherited from the parent /run URL — so the browser resolved a bare #section link to …/run#section, a cross-document navigation that yanked the artifact off screen (blank). The fix injects a tiny click interceptor (SRCDOC_ANCHOR_SCROLL_SCRIPT in src/main/services/share/share-artifact-scripts.ts) into ONLY the two srcdoc hosts that preventDefaults a same-page # click and scrolls in-document instead. A directly-served (/run, non-srcdoc) artifact keeps its own real URL as the base, so its #id links already work and it is deliberately left untouched. Existing shares recover via the viewer's Refresh button; they get the permanent fix on re-publish. Locked by tests/unit/services/srcdoc-anchor-scroll.test.ts (click behavior) + the srcdoc-anchor wiring cases in tests/unit/services/share-artifact-builder-presentation.test.ts.
  • Store. All master + detail state lives in src/renderer/src/stores/share-store.ts. On top of the legacy shares / isCreating / error slices, the store owns selectedShareId, searchQuery, kindFilter ('all' | 'thread' | 'artifact' | 'digest'), collapsedGroups (a Set<'live' | 'archived'> seeded with 'archived' so that section starts collapsed; in-memory only, resets each launch — mirrors the Cron sidebar), and pendingViewMode ('view' | null — set by the share URL resolver when openInViewer is true, consumed by SharesDetailPane on share.id change to open in view mode instead of edit mode, then cleared), plus setSearch / setKindFilter / toggleGroupCollapsed. A derived selector useGroupedShares() applies the kind + search filters and partitions the survivors into { live, archived } by active-vs-(revoked-or-expired). fetchShares clears selectedShareId only when the selected row has vanished server-side (so revoke/delete from another window doesn't strand the detail pane); a status change no longer clears it, because the row simply moves into the Archived section. revokeShare, deleteShare, republishShare all do an optimistic local write, then await get().fetchShares().catch(() => {}) in finally so a missed SHARE_UPDATED push doesn't leave the UI desynced. updateShareInPlace(id) re-reads the share's sourcePath and invokes SHARE_PUBLISH_ARTIFACT with updateToken (the share's own token), refetching in finally; it resolves { url, id, updatedInPlace } so the pane stays on the same row on a same-link update and lands on the new row on the relay fallback.
  • Data source. SharesLiveSync (mounted app-wide in GlobalModals) calls fetchShares() (which invokes ipc.invoke(IPC.SHARE_LIST, { includeRevoked: true })) on mount and on every SHARE_UPDATED push, so the store's shares list is hydrated on every view — not just when the Shares panel is open. SharesSidebar ALSO calls fetchShares() on mount and on SHARE_UPDATED while it's open (a benign extra refresh). SharesDetailPane reads the currently-selected share out of the store and does NOT subscribe. The handler is src/main/ipc/share-handlers.ts.
  • SHARE_UPDATE IPC. Channel registered in share-handlers.ts next to SHARE_REVOKE / SHARE_DELETE. Schema is shareUpdateSchema in src/shared/ipc-schemas.ts — { id: string, patch: { label?: string, expiresAt?: string | null, liveUpdating?: boolean, viewCap?: number | null, notifyOnView?: boolean, commentsEnabled?: boolean, muteNotifications?: boolean } }. Handler reads the existing row (returns { success: false, error: 'Share not found: ...' } if missing), calls shareQueries.updateShare(id, patch), mirrors the patch to Firestore, emits SHARE_UPDATED with action: 'updated', returns { success: true, data: { share } }. The SHARE_UPDATED push channel includes 'updated' alongside the existing 'created' | 'revoked' | 'deleted' | 'expired' actions. Password protection is a separate channel.
  • SHARE_SET_PROTECTION IPC. Dedicated channel for the password hash/salt handshake so plaintext never round-trips through the patch object. Schema is shareSetProtectionSchema in src/shared/ipc-schemas.ts — { id: string, passwordHash: string(64-hex) | null, passwordSalt: string(32-hex) | null }. Both fields must move together (both set, or both null — partial sets are rejected). Handler updates the SQLite row + Firestore mirror, emits SHARE_UPDATED with action: 'updated', returns { success: true, data: { share } }. passwordHash / passwordSalt are stripped from every IPC response — only the renderer-facing hasPassword: boolean is exposed.
  • Share view poller. src/main/services/share/share-view-poller.ts runs as a periodic task (60 s interval, 30 s initial delay, jittered) via createPeriodicTask. Each tick pulls all shares.* docs from the Firestore mirror, compares view_count / last_viewed_at against the local row, writes the deltas into the local shares row + appends new share_view_events rows, then fires a desktop notification for each share with notify_on_view = 1 AND mute_notifications = 0 whose count advanced. The row write commits before the notification fires for crash-safe ordering.
  • Schema. Migration v173 added password_hash, password_salt, view_cap, view_count, last_viewed_at, notify_on_view to the shares table + a share_view_events audit log. Migration v180 added share_send_log. Migration v181 added per-event privacy hashes (country, ip_hash) on share_view_events (see src/main/db/queries-share-view-events.ts).
  • Mobile push coverage. SHARE_UPDATED is desktop-only — it's listed in DESKTOP_ONLY_CHANNELS in tests/unit/lint/mobile-push-listener-coverage.test.ts. Both renderer listeners (the app-wide SharesLiveSync and the sidebar) subscribe via useIpcListener, but the WS bridge never forwards SHARE_UPDATED to the mobile/web renderer, so the listeners simply no-op there. The mobile inbox reconciles via the standard inbox push path. The viewer's keyboard-dismiss push WEBVIEW_GUEST_DISMISS (Escape/Ctrl+W forwarded out of the popup's <webview>) is likewise desktop-only in the same list — mobile web has no <webview> guest, and the mobile viewer closes via its X button.
  • Modal lazy-loading. Only PastePublishModal remains as a modal in this integration. It's React.lazy()-imported inside SharesSidebar.tsx per the heavy-modal contract — the chunk only downloads when the user clicks + Publish. (SharesEditModal and SharesSendModal no longer exist — both flows are now inline in SharesDetailPane.)
  • SHARE_SEND IPC. Channel registered in share-handlers.ts. Schema is shareSendSchema in src/shared/ipc-schemas.ts — { shareId, channelKind: 'gmail'|'sms'|'slack', recipient: string(1..1000), message?: string(0..5000) }. Handler validates the share is published / not revoked / not expired, inserts a share_send_log pending row (so a thrown channel call still leaves an audit row), dispatches through gmailApiService.sendEmail, pushbulletService.sendSms, or slackService.sendMessage. Flips the log row to sent / failed, emits SHARE_UPDATED with action 'sent' or 'send-failed', and tracks a 'share', 'send' event with the channel kind. Returns { success: true, data: { entry } } even on channel-level failure — the renderer keys off entry.status === 'failed' so the inline panel stays open.
  • Data layer. share_send_log table added in migration v180 (DDL + indexes in src/main/db/database.ts). Query layer is src/main/db/queries-share-send-log.ts: insertSendLog, updateSendLogStatus, getSendLogById, listSendLogForShare. CHECK constraints enforce channel_kind IN ('gmail','sms','slack') and status IN ('pending','sent','failed'). Indexes on share_id and created_at keep the list query fast.
  • Drag-and-drop fan-out. The HTML5 drop events are owned by useFileDropZone from src/renderer/src/hooks/useFileDropZone.ts (handles the drag-counter, Files-type filter, and preventDefault on dragover). The per-file IPC fan-out lives in publishDroppedFiles from src/renderer/src/features/shares/publish-dropped-files.ts, which: (a) resolves each File's OS path through preload's webUtils.getPathForFile (Electron 39 dropped the renderer-side File.path), (b) calls IPC.SHARE_PUBLISH_ARTIFACT once per file, (c) maps the seven structured share-artifact-service error contract strings (outside-allowlist, unsupported-type, magic-byte-mismatch, empty-file, size-cap, firebase-upload-failed, firebase-quota) to user-friendly sentences, (d) returns a single PublishOutcome = { ok, failed[] } the sidebar collapses into one toast. Synthetic Files (no backing path) and browser mode both short-circuit without ever invoking IPC.

For agents

Component layout

  • src/renderer/src/features/shares/SharesSidebar.tsx — master pane. Header, search, segmented Kind tabs, the two collapsible Live / Archived & expired sections (a local SharesSectionHeader mirrors the Cron group-header look without importing it — a static import would drag cron-group-label's store graph into this lazy chunk), scrollable list, drop zone, PastePublishModal launcher. Owns the SHARE_UPDATED listener.
  • src/renderer/src/features/shares/SharesDetailPane.tsx — detail pane. Header band with View/Open buttons, three-mode viewer (viewMode state), inline edit form, inline password sub-form, collapsible Send panel, destructive footer. In edit mode its live preview guest is silenced while it is scrolled out of view — the preview sits at the top of the pane's scrolling body and the edit form lives below it, so reaching the form scrolls a playing guest away; useInViewport (the pane's scroll container as the observer root) drives the webview's visible prop, and the signal is live in BOTH directions so scrolling back un-mutes it. Locked by tests/unit/features/shares/SharesDetailPane-viewer.test.tsx + tests/unit/hooks/useInViewport.test.tsx.
  • src/renderer/src/features/shares/ShareViewerWebview.tsx — the Electron <webview> component that renders a share in a separate renderer process. Handles loading, error (did-fail-load), and crash (render-process-gone) states with appropriate overlays. Its content identity is (url, contentHash), not the url alone: an in-place republish (the Update button / updateToken) REUSES the token, so share.url is byte-identical while contentHash changes — the component reloads the guest (reloadIgnoringCache, guarded with a typeof check so the mobile / non-Electron webview never throws) when contentHash changes under a stable url, so the viewer shows the NEW content instead of the stale kept page. A bare re-render or a new share object with the SAME identity is a no-op (deps are the primitive url/hash, not the share object), preserving instant-open and the no-stuck-spinner invariant. The decision is the pure nextViewerLoad(prevKey, url, contentHash) helper, unit-tested in tests/unit/features/shares/ShareViewerWebview.test.tsx. Reveal timing: it lifts the loading cover at first-contentful-paint via a guest paint probe (executeJavaScript on dom-ready, resolving on a buffered PerformanceObserver paint entry; a cleanup timeout resolves not-painted, which never reveals), guarded to the current (url, contentHash) so a superseded navigation can't reveal the wrong page, and keeps did-finish-load as the backstop. The probe's observer is long-lived, so a background-prewarmed slot that only paints once it is promoted (a hidden→visible CSS flip, not a reload) is still caught — the instant-open path now holds even for font-heavy pages, not just light ones. Locked by the "reveals at first contentful paint" / backstop / stale-nav tests in tests/unit/features/shares/ShareViewerWebview.test.tsx. Off-screen guests are SILENCED. visible (default true) is the host's "this guest is on screen to the user right now" signal: when it goes false the guest's media is paused AND the guest audio is muted, and the mute is lifted when it comes back. It exists because the floating viewer closes by moving its stage off-screen (position: fixed; left: -99999; visibility: hidden) rather than unmounting — CSS visibility is a paint instruction and does not stop media, so a share playing a video kept playing after the viewer was closed (reported 2026-09-27). The pause stops the video itself; the mute is the guarantee, because a sweep of the guest's top document cannot reach a cross-origin iframe player or Web Audio, and cannot stop a page that resumes on a timer. Playback is NEVER auto-resumed — a page may start playing only from a user gesture, and reopening a viewer is not a request to resume. Every call is guarded: a <webview> delegates setAudioMuted / executeJavaScript to its guest and that delegation THROWS before dom-ready (measured on real Electron: "The WebView must be attached to the DOM and the dom-ready event emitted…"), which is the state of every hidden prewarm on its first render — and a throw inside a passive effect reaches React's error boundary, so the applier can never be allowed to throw. A guest that attaches late is covered by a dom-ready re-assert. Locked by the "silences the guest the moment it goes off screen" / "lifts the mute … never resumes" / "only attaches AFTER it was already hidden" / shim-guard tests in tests/unit/features/shares/ShareViewerWebview.test.tsx + tests/unit/features/shares/SharePrewarmSlot.test.tsx.
  • src/renderer/src/features/shares/ShareViewerToolbar.tsx — floating auto-hide toolbar overlaying the webview. Mode-dependent actions (Edit + Maximize in view mode; Exit in fullscreen mode) plus the always-present actions (Open in browser, Copy URL, and the desktop-only Screenshot full-page capture via ShareScreenshotButton). Takes the viewed webview's id (lifted from its parent) so Screenshot targets the right page. ARIA role="toolbar", fades after 3 seconds.
  • src/renderer/src/features/shares/ShareViewerFullscreen.tsx — fullscreen viewer portal (createPortal to document.body). Opaque fixed inset-0 z-50 overlay with its own webview + toolbar. Escape key exits.
  • src/renderer/src/features/shares/SharesEmptyDetail.tsx — empty state for when no row is selected.
  • src/renderer/src/features/shares/shares-display-helpers.tsx — extracted helpers: kindBucket, expiryDisplay, scopeBadge, artifactBadge (shared between the two components).
  • src/renderer/src/features/shares/share-viewer.ui-anchors.ts — co-located UI-anchor metadata for the viewer webview, toolbar, and all viewer buttons.
  • src/renderer/src/lib/share-url-intercept-access.ts — registration-seam that breaks the import cycle between platform.ts and share-store.ts for share URL interception.
  • src/renderer/src/features/shares/SharesLiveSync.tsx — app-wide share-list live-sync: a null-rendering component (mounted once in GlobalModals beside FloatingShareViewer) whose useSharesLiveSync hook hydrates the store on mount and refetches on SHARE_UPDATED, so an agent-published share is recognized as your own and routes to the in-app viewer on any view.
  • src/renderer/src/features/shares/FloatingShareViewer.tsx — the full-size in-app share popup (portal to document.body). Its header carries Copy URL (desktop + mobile, via CopyConfirmButton), a desktop-only Screenshot (ShareScreenshotButton), and a Refresh button (reloads the visible share via useShareWebviewReload → UI_RELOAD_SHARE_WEBVIEW, disabled until the page reports a webContentsId) alongside the open-in-browser + close controls; it tracks a url → webContents id map for the promoted/visible slot so Screenshot targets the page you're actually looking at. Since the 2026-08-05 instant-open rework it is ALSO the single owner of the warm slots: it renders an always-mounted off-screen STAGE (the card shell + webview host) so the slots' React positions never change, creates a SharePrewarmSlot per URL in the store's backgroundPrewarmQueue (capped SHARE_PREWARM_MAX_SLOTS, read through useDeferredValue so warming yields to the user's paint — never-slow-the-user), and on open PROMOTES the slot matching the open share by a pure style flip (same DOM element → the already-loaded page renders instantly, no reload). A share with no warm slot yet (a cold click, or a stale-risk share — revoked / expired / live-updating, where a kept copy could be out of date) loads fresh via a plain ShareViewerWebview; warm slots persist after they load and are evicted oldest-first on cap pressure. Each warm slot also passes its share's contentHash to the webview, so an in-place republish (which bumps the hash) reloads the warmed copy in the background — a promoted republished share shows the new content, never the stale warm.
  • src/renderer/src/features/shares/SharePrewarmSlot.tsx — one share <webview> slot: either a hidden off-screen warm (loaded while the share link is on screen) or the promoted one filling the card. Settles on a terminal load status or an 8s timeout, which advances the warm queue (markBackgroundPrewarmed); an evicted slot is un-warmed (dropBackgroundPrewarm) so a later re-render can warm it again. It passes visible={promoted} to its webview, so an off-screen slot is silenced — which covers BOTH a hidden preload (a warming share never makes noise) and the page the viewer just closed (the element stays mounted for the instant reopen rather than unmounting, so without that prop the sound simply carried on). Locked by tests/unit/features/shares/SharePrewarmSlot.test.tsx.
  • src/renderer/src/features/shares/use-background-share-prewarm.ts — the hook agent-markdown-helpers calls at each rendered in-app share link (rendersAsInAppShare — your own OR a teammate's): it enqueues that link's URL for the background preload above (idempotent — deduped against the queue + a warm-once set), gated on isElectron (desktop-only; the mobile web bundle can't render <webview>). Locked by tests/unit/features/shares/use-background-share-prewarm.test.ts.
  • src/renderer/src/features/shares/ShareScreenshotButton.tsx — the shared desktop-only Screenshot button dropped into both the quick opener and the manager toolbar. Renders nothing off isElectron; takes the viewed webview's id, disabled until it arrives; drives the busy state from the capture hook. Locked by tests/unit/features/shares/ShareScreenshotButton.test.tsx.
  • src/renderer/src/features/shares/useShareWebviewCapture.ts — the shared hook behind that button: fires IPC.UI_CAPTURE_SHARE_FULL_PAGE, guards re-entrancy (a rapid second click can't fire a second capture), and surfaces the copied / trimmed / failed toast — the capture+clipboard+toast contract in ONE place. Locked by tests/unit/features/shares/useShareWebviewCapture.test.tsx.
  • src/main/services/share/capture-share-full-page.ts — main-side captureShareWebviewFullPage(webContentsId): resolves the target through the resolveHumanTabWebContents() boundary (share/browser webview only), CDP full-page capture (captureBeyondViewport, clamped to MAX_CAPTURE_EDGE), attach-only-if-needed + detach-only-what-it-attached, failure-as-a-value. Governed by ui-screenshot-contract.md full-page share-capture rules; locked by tests/unit/services/capture-share-full-page.test.ts.

Related

The overview, the other parts, and everything else worth reading next all sit on Shares view (master/detail sidebar + in-app webview viewer for shared links).

Last verified 2026-09-28