Shares view (master/detail sidebar + in-app webview viewer for shared links)
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.
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 and 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)
- 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.
- Header. "Shares (N)" count on the left + a
+ Publishbutton on the right. Clicking the button opens the Publish pasted content modal — the same modal that's always handled paste-publishes. - Search. Below the header, a single search input matches by label, URL token, ISO
createdAt, ISOexpiresAt, and the human-readable expiry string (so5 days left/Expiredare searchable too). Placeholder:Search by label, URL, or date. - Filter by Kind. Segmented tab strip with four buttons: All / Threads / Artifacts / Digests. "Threads" covers
conversation/message/selectionscopes (artifactKindis null orthread); "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. - 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.) - 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-iconN/CAPview counter when a view cap is set. A singleRevoked(red) orExpired(grey) badge is pinned to the right edge on archived rows; live rows carry no status badge. The loud per-row scope/kind/Protectedpills 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. - Click to select. Clicking a row updates
selectedShareIdin the store; the right pane swaps to the new share. On mobile, the same click also drives a drill-down viauseMobileNavStore.getState().navigateTo('shares-detail')so the detail pane fills the screen. - 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. - 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). - Auto-refresh. Two subscribers keep the store's share list current.
SharesLiveSync(mounted app-wide inGlobalModals) hydrates on mount and re-pulls on everySHARE_UPDATEDpush, so the list reflects your shares on every view — even when this panel is closed. The sidebar itself ALSO re-pulls onSHARE_UPDATEDwhile open (a benign extra refresh). The detail pane reads its current share out of the store and never subscribes. The app-wideSharesLiveSyncis 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);SharesLiveSyncjust adds the rich metadata for your own. See Share URL interception below. - 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.
- Header band. Title (truncated
<h2>ofshare.label), a status pill (Activegreen /Revokedred /Expiredgrey /View cap reachedamber), scope chip, artifact-kind chip,Protectedamber 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 viaopenExternalUrlForce(bypasses share URL interception), and a Copy URL button (usesCopyConfirmButtonfor the icon-swap visual confirmation). - Three-mode viewer. The detail pane has three modes, tracked via a
viewModestate ('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 theSHARED_WEB_PARTITION('persist:browser'),HARDENED_WEBVIEW_PREFERENCES, and aCHROME_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 (executeJavaScriptondom-ready, a long-lived bufferedPerformanceObserver), withdid-finish-loadkept as a backstop — and then fades in. Revealing at first paint rather than the fullloadevent 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 (observingfirst-contentful-paint, never a blankfirst-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); adid-fail-loadevent (filtered to main-frame only, excludingERR_ABORTED) shows an error overlay with an "Open in browser" fallback; arender-process-goneevent shows a crashed overlay with a "Reload" button. A floating auto-hideShareViewerToolbaroverlays 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 awebContentsId; via theUI_RELOAD_SHARE_WEBVIEWroute through the same vettedpersist: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 below). The toolbar fades out after 3 seconds of no interaction and reappears on hover. - Fullscreen mode. A
ShareViewerFullscreenportal (createPortaltodocument.body) covers the entire window as afixed inset-0 z-50opaque layer — the same full-window treatment asTosConsentGate. Contains its ownShareViewerWebview+ShareViewerToolbarwithmode="fullscreen"(shows an Exit full screen button instead of Edit/Maximize). Exits via Escape key or the toolbar's Exit button, both routing throughonExitwhich setsviewModeback to'view'. The inline view-mode webview stays mounted underneath so the persistent<webview>guest survives a fullscreen round-trip without a reload. TheviewModeresets to'edit'when the selected share changes (useEffectonshare.id). TheuseCtrlEnterSubmithook is gated withenabled: viewMode === 'edit'so Ctrl+Enter doesn't fire in view/fullscreen mode.
- Inline metadata + edit form (replaces the old
SharesEditModal). One<form>-like region inside the pane, wired withuseCtrlEnterSubmitscoped 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-localinput. 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 readsCurrently N viewsorCurrently N views of CAP. The Cloud Function refuses to serve the share onceview_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_countincrease 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_UPDATEDpush) still updates normally; only the user-facing alerts (OS notifications and inbox items) are suppressed. Stored locally in SQLite (mute_notificationscolumn), 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.
- 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 amberProtectedlock 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.
- 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
- Send share (replaces the old
SharesSendModal). Collapsed by default — aSend to…row at the top expands a 3-tile channel picker (Gmail / SMS / Slack), recipient input, optional message textarea, and aSend/Cancelfooter. 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.
- 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. - 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 clearsselectedShareIdso 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 viaupdateShareInPlace→SHARE_PUBLISH_ARTIFACTwithupdateToken— 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. - Revoke — only visible while the share is still active. Confirms via
useConfirmDialog, callsrevokeShare, 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.
- Republish — only visible when the share is revoked OR expired. Opens a confirm dialog; on approve, mints a fresh URL via
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 withShare is revoked/Share has expired/Share has not been publishedbefore any channel call. - Audit. Every send writes a
share_send_logrow — pending on insert, then flipped tosent(stampssentAt, clears error) orfailed(stamps the channel's error string, leavessentAtnull). The pending row is inserted before the channel dispatch so a thrown channel call still leaves an audit trail. List viaipc.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_capis non-null andview_count >= view_cap, the CF returns a 410 "share view cap reached" page without writing a view event. - Password gate. If
password_hashis 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 topassword_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_eventsrow to Firestore (shareId,viewedAt, IP-derived geo hint), atomically incrementsshare.view_count, and stampslast_viewed_at. The Omniscio desktop poller pulls these every 60 seconds (jittered, 30-second initial delay) and mirrors the deltas back into the local SQLitesharesrow + appends to the localshare_view_eventsaudit table. After each poll, every share withnotify_on_view = 1whose 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.captureScreenshotwithcaptureBeyondViewport, clip sized fromPage.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 (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 itswebContentsid. 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 livepersist:browserguest); a forged or foreign target id, the main window, and agent tabs are all refused (ui-screenshot-contractonly-a-share-webview-is-capturable). - Copy URL. The quick opener's header also carries a Copy URL button (the app's canonical
CopyConfirmButtonwith 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 (
sharesOpenInAppsetting, 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/publishswapsdata.urlto a deep-link URL (omniscio://share/<token>) and always includesdata.publicUrlfor 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 viainterceptShareUrl(url, { openInViewer: true })instead ofwindow.open. See 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
sharesOpenInAppis 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 atSHARE_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 pageCache-Control: no-store, so a cache-warm can't help; keeping the loaded page alive is what makes the click instant. SeeFloatingShareViewer/SharePrewarmSlotunder Component layout.) - Shares an agent publishes via
POST /share/publishraise a one-click "Go there" toast through the sharedsuggestAgentNavigation()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 canonicalshares.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).
- 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
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) — the continuation of this page.
artifact-sharing.md — the publish side for conversations / messages / selections (the Threads bucket in this view)
share-artifacts.md — the publish side for local files + pasted content (the Artifacts bucket in this view)
share-cli.md — the CLI control server's publish / list / revoke / delete endpoints; everything they create shows up in this view
share-comments.md — the Comments section in the detail pane (inline and general commenting on shares)
daily-digest.md — the Digests bucket comes from this feature's "Share digest" action
Last verified 2026-09-23