---
title: Shares view (master/detail sidebar + in-app webview viewer for shared links)
---
# Shares view (master/detail sidebar + in-app webview viewer for shared links)

## What it is

Shares is a top-level entry in the Omniscio sidebar (under the **Omniscio** group, alphabetically between **Settings** and **Skills**) that opens a Cron-Jobs-shaped **master/detail** layout: a 268 px sidebar on the left lists every shared link you've created, and the right pane shows the currently-selected share with a three-mode **in-app webview viewer** (edit / view / fullscreen) and **inline** controls for label / expiration / view-cap / notify / password / send / revoke / delete / republish.

It supersedes a previous single-pane view (`SharesView`) and two modal dialogs (`SharesEditModal`, `SharesSendModal`) — every editing surface is now inline in the right pane; the only modal left is the **Publish pasted content** dialog, which is a launcher for new pasted-text shares.

Threads, artifacts, and daily digests all show up in this one place. From here you can: filter, search, copy a link, rename it, change its expiration, set a view cap, enable view-notifications, set/change/remove a password, send via Gmail/SMS/Slack, revoke an active link, delete an inactive one, republish a revoked/expired link at a new URL, or publish brand-new pasted content. It's the same `share_links` table as before — see [artifact-sharing.md](artifact-sharing.md) and [share-artifacts.md](share-artifacts.md) for the publish-side details — but with a dedicated home that doesn't live inside Settings.

## Where to find it

**Shares**, a top-level entry in the **Omniscio** group of the sidebar, between Settings and Skills.

## How it behaves

### How to use it

### The master pane (Shares sidebar)

1. **Open the view.** Click **Shares** in the left Omniscio sidebar. The integration mounts the **Shares sidebar** in the normal sub-sidebar slot and an empty-state card in the right pane.
2. **Header.** "Shares (N)" count on the left + a `+ Publish` button on the right. Clicking the button opens the **Publish pasted content** modal — the same modal that's always handled paste-publishes.
3. **Search.** Below the header, a single search input matches by **label**, **URL token**, **ISO `createdAt`**, **ISO `expiresAt`**, and the human-readable expiry string (so `5 days left` / `Expired` are searchable too). Placeholder: `Search by label, URL, or date`.
4. **Filter by Kind.** Segmented tab strip with four buttons: **All / Threads / Artifacts / Digests.** "Threads" covers `conversation` / `message` / `selection` scopes (`artifactKind` is null or `thread`); "Artifacts" covers HTML, MD, SVG, Image, PDF, Text, Code, React, pasted-text, mc-board (Mission Control board/dashboard snapshots); "Digests" covers the daily-digest scope.
5. **Two sections (no status pills).** The list always shows everything, split into two collapsible sections: **Live** (active shares — neither revoked nor expired) and **Archived & expired** (revoked OR expired). **Archived & expired is collapsed by default** so dead links stay out of the way; each section header shows a `(count)` and a chevron, and the archived rows render dimmed (`opacity-50`) when expanded. When every share is live, the section headers are omitted and the list renders flat. The split composes with Kind + search — those filter first, then each surviving row falls into Live or Archived. (The old **Active / Revoked / All** pill row is gone; the collapsible sections replace it.)
6. **Each row** is a full-width button (selectable target) laid out like a Cron-jobs row so the label never gets squeezed: line 1 is the **label on its own line** (truncates with an ellipsis, never to a single letter) plus a small amber lock icon when password-gated, plus a small muted-bell icon (`BellOff`) when notifications are muted; line 2 is one muted metadata line — `Kind · scope · expiry` (e.g. `HTML · conversation · 5 days left`) — followed by a small eye-icon `N/CAP` view counter when a view cap is set. A single `Revoked` (red) or `Expired` (grey) badge is pinned to the right edge on archived rows; live rows carry no status badge. The loud per-row scope/kind/`Protected` pills of the old layout are gone — kind and scope are now plain text, and the truncated URL was dropped from the row (it's still searchable and shown in full in the detail pane). Selected row gets the standard inbox-row "active" styling.
7. **Click to select.** Clicking a row updates `selectedShareId` in the store; the right pane swaps to the new share. On mobile, the same click also drives a drill-down via `useMobileNavStore.getState().navigateTo('shares-detail')` so the detail pane fills the screen.
8. **Middle-click opens in the browser.** Middle-clicking a row opens the share's published page in your system default browser (via `openExternalUrlForce`) — the same non-destructive action as the detail pane's "Open in browser" button, with no confirm dialog. Uses the inbox-row gesture (`onMouseDown` + `e.button === 1` + `e.preventDefault()`, which also keeps the middle-click autoscroll circle from appearing). Revoke stays available in the detail pane.
9. **Drag and drop to publish.** Drop one or more files anywhere onto the sidebar (the whole sidebar lights up with a dashed accent border and a "Drop files to publish" overlay). Release and Omniscio publishes each file as its own share, then refreshes the list. Files must live inside a session workdir, `~/Claude`, or `~/.amc` — drop something from your Desktop or Downloads and you'll see a single error toast naming the file and the reason. Mixed batches surface a single warning toast like "Published 3, 1 failed: foo.docx — …" instead of one toast per file. Web / mobile shows a "Drag-and-drop publishing requires the desktop app" failure (the renderer can't resolve OS paths there).
10. **Auto-refresh.** Two subscribers keep the store's share list current. `SharesLiveSync` (mounted app-wide in `GlobalModals`) hydrates on mount and re-pulls on every `SHARE_UPDATED` push, so the list reflects your shares on **every** view — even when this panel is closed. The sidebar itself ALSO re-pulls on `SHARE_UPDATED` while open (a benign extra refresh). The detail pane reads its current share out of the store and never subscribes. The app-wide `SharesLiveSync` is what makes an agent-published share **of your own** resolve as one of your shares, so it opens in the in-app viewer with its full metadata (label, staleness) rather than the minimal build-from-token card a non-owned share gets — when you're not on the Shares panel (the "shares still open in Chrome" fix). A share you don't own also opens in-app now (built from its token — owner decision 2026-08-11); `SharesLiveSync` just adds the rich metadata for your own. See **Share URL interception** below.
11. **Empty states.** First-time visit (no shares yet): centered card with "No shares yet" + a "Publish pasted content" CTA button. Filtered to nothing: "No shares match this filter" with a hint about adjusting the search / filter chips plus a one-click **"Clear search & filters"** link that empties the search box and resets the type filter to All.

### The detail pane (Shares detail)

The right pane is empty (with a friendly explainer) until a row is selected. Once selected, the pane is a stacked column inside an `max-w-3xl` container, vertically scrollable on overflow.

1. **Header band.** Title (truncated `<h2>` of `share.label`), a status pill (`Active` green / `Revoked` red / `Expired` grey / `View cap reached` amber), scope chip, artifact-kind chip, `Protected` amber lock badge if password-gated, and a relative "Created N minutes/hours/days ago" timestamp. On the right: a **View** button (Eye icon) that switches the pane into in-app view mode (see mode system below), a secondary **Open in browser** icon button that launches the URL in the system browser via `openExternalUrlForce` (bypasses share URL interception), and a **Copy URL** button (uses `CopyConfirmButton` for the icon-swap visual confirmation).
2. **Three-mode viewer.** The detail pane has three modes, tracked via a `viewMode` state (`'edit' | 'view' | 'fullscreen'`):
   - **Edit mode** (default). Shows the header band, the inline edit form, password sub-form, Send panel, comments, and destructive footer — all the editing controls. No webview is mounted.
   - **View mode.** Replaces the edit form with a full-height `ShareViewerWebview` — an Electron `<webview>` tag that renders the share in a separate renderer process using the `SHARED_WEB_PARTITION` (`'persist:browser'`), `HARDENED_WEBVIEW_PREFERENCES`, and a `CHROME_USER_AGENT`. The webview supports loading, error, and crash states: a **fully-opaque** loading surface (spinner) overlays during load and the `<webview>` itself is held invisible (`opacity-0`) until the page draws its **first contentful paint** — detected by a guest paint probe (`executeJavaScript` on `dom-ready`, a long-lived buffered `PerformanceObserver`), with `did-finish-load` kept as a backstop — and then fades in. Revealing at first paint rather than the full `load` event is what stops a font-heavy share (external Google/Fontshare fonts, `display=swap`) from sitting behind the spinner for the extra hundreds of ms while those fonts finish, over a page that has already drawn — so the guest's default white paint never flashes before the page draws (observing `first-contentful-paint`, never a blank `first-paint`, keeps that no-white-flash guarantee; mirrors the quick-launch modal's opaque-surface + reveal-when-ready approach; the opaque cover is the real guarantee since Electron doesn't reliably fade a `<webview>` guest via CSS opacity); a `did-fail-load` event (filtered to main-frame only, excluding `ERR_ABORTED`) shows an error overlay with an "Open in browser" fallback; a `render-process-gone` event shows a crashed overlay with a "Reload" button. A floating auto-hide `ShareViewerToolbar` overlays the top of the webview with mode-dependent actions: **Edit** (return to edit mode) + **Maximize** (enter fullscreen) in view mode, plus the always-present **Refresh**, **Open in browser**, **Copy URL**, and **Screenshot** buttons. **Refresh** reloads the share webview's TOP frame (the share shell), re-embedding the whole nested artifact tree fresh — it recovers a view an in-artifact link navigated off, or any share that rendered wrong (desktop-only, disabled until the page has a `webContentsId`; via the `UI_RELOAD_SHARE_WEBVIEW` route through the same vetted `persist:browser`-webview boundary as Screenshot). **Screenshot** captures the ENTIRE scrolling shared page (not just the visible part) and copies the PNG to the clipboard — desktop-only, and inert until the page has finished loading (see [Full-page screenshot](#full-page-screenshot) below). The toolbar fades out after 3 seconds of no interaction and reappears on hover.
   - **Fullscreen mode.** A `ShareViewerFullscreen` portal (`createPortal` to `document.body`) covers the entire window as a `fixed inset-0 z-50` opaque layer — the same full-window treatment as `TosConsentGate`. Contains its own `ShareViewerWebview` + `ShareViewerToolbar` with `mode="fullscreen"` (shows an **Exit full screen** button instead of Edit/Maximize). Exits via **Escape** key or the toolbar's Exit button, both routing through `onExit` which sets `viewMode` back to `'view'`. The inline view-mode webview stays mounted underneath so the persistent `<webview>` guest survives a fullscreen round-trip without a reload.
     The `viewMode` resets to `'edit'` when the selected share changes (`useEffect` on `share.id`). The `useCtrlEnterSubmit` hook is gated with `enabled: viewMode === 'edit'` so Ctrl+Enter doesn't fire in view/fullscreen mode.
3. **Inline metadata + edit form** (replaces the old `SharesEditModal`). One `<form>`-like region inside the pane, wired with `useCtrlEnterSubmit` scoped to the body — Ctrl+Enter / Cmd+Enter submits the patch. Fields:
   - **Label.** Text input, max 200 chars. Required (empty value disables Save).
   - **Expires.** A "Never expires" checkbox + a `datetime-local` input. Toggling the checkbox disables the input. Setting the picker to a past time immediately marks the share Expired on the next refresh.
   - **View cap.** A "Limit total views" checkbox + a number input (1 – 999,999). Below the input, when `viewCount > 0`, a hint reads `Currently N views` or `Currently N views of CAP`. The Cloud Function refuses to serve the share once `view_count >= view_cap`.
   - **Notify on view.** A "Notify me when someone views this share" checkbox. When enabled, the desktop fires a notification ("Share viewed: <label>") the first time the poller observes a `view_count` increase since the last poll.
   - **Mute notifications.** A "Mute notifications for this share" checkbox. When enabled, the per-share mute overrides BOTH the "notify on view" flag AND the global comment inbox alerts for this share. The data (view counts, comment counts, `SHARE_COMMENTS_UPDATED` push) still updates normally; only the user-facing alerts (OS notifications and inbox items) are suppressed. Stored locally in SQLite (`mute_notifications` column), not in Firestore.
   - **Save / Cancel footer.** **Save** is disabled until the form is **dirty AND valid** (`hasChanges && !isLabelInvalid && !isDateInvalid && !isViewCapInvalid && !submitting`). **Cancel** reverts every field to the authoritative server state. Neither button closes the pane — the pane is always open.
4. **Password protection** (replaces the old SharesEditModal protection section). A dedicated sub-card with three states:
   - No password set → **Set password** button. Click to reveal a password input + a "Save password" button. The plaintext is hashed in the renderer via SubtleCrypto (PBKDF2-SHA256, 600,000 iterations, 16-byte random salt, 32-byte derived key) and only the hex-encoded `{ passwordHash, passwordSalt }` ship across IPC. Plaintext never crosses the IPC boundary — the explainer below the input says so verbatim.
   - Password already set → **Change password** (same flow as above) + a **Remove** button (red text) that sends `{ passwordHash: null, passwordSalt: null }` to clear the gate. An amber `Protected` lock badge appears beside the section header.
   - Save fires `ipc.invoke(IPC.SHARE_SET_PROTECTION, { id, passwordHash, passwordSalt })` separately from the label/expiration patch — protection changes apply immediately, independent of the main Save button. There's an eye-icon toggle to show/hide the plaintext while typing.
5. **Send share** (replaces the old `SharesSendModal`). Collapsed by default — a `Send to…` row at the top expands a 3-tile channel picker (Gmail / SMS / Slack), recipient input, optional message textarea, and a `Send` / `Cancel` footer. The channel picker is a 3-column grid of tile buttons; the recipient label, placeholder, and hint adjust per channel:
   - **Gmail** wants an email address. Sent as a new thread.
   - **SMS** wants an E.164 phone number.
   - **Slack** wants a raw channel ID (starts with C, D, or G).
     Recipient is required (1000-char cap); message is optional (5000-char cap, blank-line-separated above the URL). Submit calls `ipc.invoke(IPC.SHARE_SEND, { shareId, channelKind, recipient, message? })`. On success the panel collapses and a toast fires. On failure the panel stays open with the channel's error string surfaced verbatim so the user can fix the recipient and retry. While the Send panel is open, the form-level Ctrl+Enter submit is disabled so it doesn't fight the channel submit.
6. **Comments section.** When the share has `commentsEnabled`, a collapsible **"Comments"** section appears showing mirrored comment activity from the linked public share page. The inline edit form also exposes an **"Allow comments"** toggle to enable or disable commenting per share.
7. **Destructive actions footer.** A final card with four buttons:
   - **Republish** — only visible when the share is revoked OR expired. Opens a confirm dialog; on approve, mints a fresh URL via `republishShare`, revokes the old row server-side, and clears `selectedShareId` so the sidebar's push reconciliation re-surfaces the new row.
   - **Update** — only visible for an ACTIVE artifact share with an on-disk source (`!isInactive && sourcePath && artifactKind !== 'thread'`). Confirms (it overwrites what the live link serves), then refreshes the share's content IN PLACE from its source file via `updateShareInPlace` → `SHARE_PUBLISH_ARTIFACT` with `updateToken` — the **same URL** now serves the new content and the toast reads "Share updated (same link)". On the rare relay fallback it publishes a new link, retires the old, lands on the new row, and the toast reads "Published a new link…". Distinct from **Republish**, which deliberately mints a _fresh_ URL. See [share-artifacts.md § Update in place](share-artifacts.md).
   - **Revoke** — only visible while the share is still active. Confirms via `useConfirmDialog`, calls `revokeShare`, marks the row Revoked.
   - **Delete** — always visible, red-bordered. Confirms (danger variant), calls `deleteShare`, navigates back to projects on mobile so the deleted row isn't stuck on screen.

### Sending a share via channel

The inline **Send to…** sub-panel hands the share URL off to one of three channels: **Gmail**, **SMS**, or **Slack**. Omniscio builds the body (optional note + share URL), dispatches via the matching service, and writes a row to `share_send_log` so you can audit every send — successful or failed.

- **Outcome.** A success toast fires and the sub-panel collapses on a successful dispatch ("Share sent via Gmail"). A failure toast surfaces the channel's error verbatim if the channel returns a structured failure (e.g. Slack `channel_not_found`) — the sub-panel stays open so you can fix the recipient and retry. Revoked, expired, or unpublished shares fail the pre-flight check with `Share is revoked` / `Share has expired` / `Share has not been published` before any channel call.
- **Audit.** Every send writes a `share_send_log` row — pending on insert, then flipped to `sent` (stamps `sentAt`, clears error) or `failed` (stamps the channel's error string, leaves `sentAt` null). The pending row is inserted **before** the channel dispatch so a thrown channel call still leaves an audit trail. List via `ipc.invoke(IPC.SHARE_SEND_LOG_LIST, { shareId })` — returns rows newest-first.

### How protection enforcement works

The Cloud Function that serves shares (the same one that mints the OG unfurl + renders the public page) reads the `password_hash`, `password_salt`, `view_cap`, and `view_count` columns out of the Firestore mirror on every request:

- **View cap.** If `view_cap` is non-null and `view_count >= view_cap`, the CF returns a 410 "share view cap reached" page without writing a view event.
- **Password gate.** If `password_hash` is set, the CF returns a password-prompt page that asks the visitor for the plaintext. On submit, the CF runs the same PBKDF2 derivation (600,000 iter, SHA-256, the same salt) and constant-time-compares the result to `password_hash`. Mismatch → re-render the prompt with an error; match → set a short-lived signed cookie + serve the share. The plaintext never leaves the verifier function.
- **View counter.** Every successful render writes a `share_view_events` row to Firestore (`shareId`, `viewedAt`, IP-derived geo hint), atomically increments `share.view_count`, and stamps `last_viewed_at`. The Omniscio desktop poller pulls these every 60 seconds (jittered, 30-second initial delay) and mirrors the deltas back into the local SQLite `shares` row + appends to the local `share_view_events` audit table. After each poll, every share with `notify_on_view = 1` whose view count advanced since the last poll fires a desktop notification — the row write is committed before the notification fires so a crash mid-fire costs at most one missed notification, never a double-fire.

### Smart Share Reaper

An opt-in background sweep (setting `smartShareReaperEnabled`) that keeps the Shares list from accumulating forgotten "never expires" links. It finds shares and screen recordings that are **over a year old AND have never been viewed**, gives each a **90-day grace expiry** instead of deleting anything outright, and drops a **single inbox card** asking you to **Keep** (clear the new expiry) or **let it expire**. Nothing is removed without that confirmation, so a still-wanted link is one click from permanent again. Runs quietly on a schedule when enabled; the shipped default is off.

### Full-page screenshot

Both in-app share viewers — the **quick opener** (`FloatingShareViewer`, the full-size popup that opens share links in-app — your own OR a teammate's) and the **manager viewer** (`ShareViewerToolbar`, the detail-pane view/fullscreen overlay) — carry a **Screenshot** button that copies the ENTIRE scrollable shared page to the clipboard as a PNG.

- **Why it's a separate capture path.** Electron's `capturePage()` (the AMC-window screenshot path) only captures the visible viewport, so it can't image a long scrolling artifact. The Screenshot button instead drives a Chrome DevTools-protocol capture (`Page.captureScreenshot` with `captureBeyondViewport`, clip sized from `Page.getLayoutMetrics`) against the share's `<webview>`, so the whole page is captured in one image — a distinct path from the window-capture helper. See [ui-screenshot-contract.md](../../.claude/memory/contracts/ui-screenshot-contract.md) (the full-page share-capture rules).
- **Desktop-only.** The button renders only under `isElectron` — the mobile/web renderer has no embedded `<webview>` to capture — and stays inert (disabled) until the viewed page has reported its `webContents` id. The Copy URL button (below) works everywhere.
- **Copies to the clipboard.** On success a toast confirms "Full-page screenshot copied to clipboard"; a very long page is clamped to a safe maximum (12,000 px/edge) and the toast reads "…(very long page — trimmed)". Any failure surfaces a friendly message, never a raw error. The capture + clipboard write happen in the main process — the image bytes never round-trip back to the renderer.
- **Security.** The capture only ever targets the share's own `<webview>` (a live `persist:browser` guest); a forged or foreign target id, the main window, and agent tabs are all refused (ui-screenshot-contract `only-a-share-webview-is-capturable`).
- **Copy URL.** The quick opener's header also carries a **Copy URL** button (the app's canonical `CopyConfirmButton` with the "Copied ✓" confirmation), on desktop and mobile — matching the Copy URL the manager viewer's header band + toolbar already had.

Implemented by the shared `useShareWebviewCapture` hook + `ShareScreenshotButton` component (one unit dropped into both surfaces); the main-process capture is `captureShareWebviewFullPage()` behind the `IPC.UI_CAPTURE_SHARE_FULL_PAGE` channel.

## For agents

### Settings → Sharing — what's still there

The Settings → Sharing page is **no longer the list view**. It now contains:

- A migration banner explaining that the list moved, with a primary **Go to Shares** button that activates the Shares virtual project (`activateVirtualProject(SHARES_PROJECT_ID, 'settings-sharing-banner')`).
- An **Open shares in app** toggle (`sharesOpenInApp` setting, **default ON**; desktop-only — the in-app viewer is Electron-gated, so web/mobile always open in the browser). When ON, share links open in the built-in viewer instead of the system browser. `POST /share/publish` swaps `data.url` to a deep-link URL (`omniscio://share/<token>`) and always includes `data.publicUrl` for the HTTP URL. Existing installs are moved to the new default once by a one-time migration (`migrate-shares-open-in-app-default`), then any later opt-out is respected. The ShareDialog "Open" button also respects the setting: when ON it navigates in-app via `interceptShareUrl(url, { openInViewer: true })` instead of `window.open`. See [share-cli.md](share-cli.md) for the CLI response shape.

  #### Show new shares automatically (`sharesAutoOpenOnCreate`)

  A sub-toggle nested under **Open shares in app** — only visible and active when `sharesOpenInApp` is ON. Defaults to **OFF** (opt-in). When both toggles are ON:

  - **Shares you create yourself** (from the Share dialog or the Digest Share dialog) open immediately in the built-in viewer the moment they're published, via `interceptShareUrl(url, { openInViewer: true })` — no need to click "Open" manually. The viewer mounts and starts loading at the earliest signal; the normal brief loading spinner is still visible while the share page loads. (Every in-app share link that appears on screen — your own OR a teammate's — is background-preloaded into a kept-alive hidden `<webview>` slot — capped at `SHARE_PREWARM_MAX_SLOTS`, desktop-only — and clicking one PROMOTES that already-loaded page into the floating viewer, so it renders instantly instead of a 1-2s network load. The share server serves every page `Cache-Control: no-store`, so a cache-warm can't help; keeping the loaded page alive is what makes the click instant. See `FloatingShareViewer` / `SharePrewarmSlot` under Component layout.)
  - **Shares an agent publishes** via `POST /share/publish` raise a one-click **"Go there"** toast through the shared `suggestAgentNavigation()` path — never a forced foreground switch or view change. The toast carries only the share **token** (never a raw URL); the viewer resolves it from your local share list when it's your own, otherwise builds the canonical `shares.omniscio.com/s/<token>` URL from the token — so an arbitrary or hostile URL still cannot be coerced into the viewer this way. Clicking the toast opens the share in the built-in viewer via the `{ action: 'open-share-in-viewer', token }` deep-link route. If you're in another app when the toast would fire, it becomes a durable inbox card instead — the same behaviour as any other agent nav-suggestion. Toasts coalesce per source-session (a looping agent bumps one toast, latest label wins).

- The **Firebase Configuration** collapsible section, which lives there because it's _backing config_ — the service-account key, bucket name, etc. that the share publisher reads at upload time. Configuration belongs in Settings; the list of shared links does not.

This split is intentional: a share is content (lives in its own view); the Firebase service-account key is configuration (lives in Settings).

## Related

- [Shares view (master/detail sidebar + in-app webview viewer for shared links) (part 2)](shares-view-part-2.md) — the continuation of this page.

- [artifact-sharing.md](artifact-sharing.md) — the publish side for conversations / messages / selections (the Threads bucket in this view)
- [share-artifacts.md](share-artifacts.md) — the publish side for local files + pasted content (the Artifacts bucket in this view)
- [share-cli.md](share-cli.md) — the CLI control server's publish / list / revoke / delete endpoints; everything they create shows up in this view
- [share-comments.md](share-comments.md) — the Comments section in the detail pane (inline and general commenting on shares)
- [daily-digest.md](daily-digest.md) — the Digests bucket comes from this feature's "Share digest" action
