---
title: Link hover web-title preview (hover a chat link → see the page title)
---

# Link hover web-title preview (hover a chat link → see the page title)

## What it is

When an agent's message contains a normal web link (an `http://` or `https://`
URL), **hovering the link** shows a small tooltip with that web page's actual
title — the headline of the article, the name of the repo, the title of the
video — instead of just the bare URL. So a link rendered as
`[read this](https://example.com/some/long/article-slug)` reveals
"Example — The Real Article Headline" on hover, and you know what you're about to
open before you click.

The title is fetched the moment your pointer **settles on** the link (a brief
dwell, not the instant you sweep past it), and remembered, so each link is looked
up at most once. While the title is still loading, the tooltip shows the link's
**domain** (e.g. `example.com`) so it's never blank. If the page can't be reached
or has no title, the tooltip just shows the domain — no error, no noise.

This is a desktop feature. On a phone or tablet there is no "hover", so the
preview never appears and Omniscio makes **no web requests** for it on mobile.

The separate **"Open with Ctrl+Shift+Q"** keyboard hint still appears — but only
on the **first** link of a reply, because that shortcut opens the first link.
Every link shows its web title; only the first one also shows the shortcut hint.

## Where to find it

Anywhere Omniscio renders an agent's message containing an `http://` or `https://` link — hover the link and a tooltip appears. It is a **desktop** feature: a phone or tablet has no hover, so the preview does not appear there.

## How it behaves

### How to use it

1. Open a chat session where the agent's reply contains a web link.
2. **Move your mouse onto the link and pause** (about a fifth of a second).
3. A tooltip appears: first the domain, then — a moment later — the page's real
   title once Omniscio has fetched it.
4. Move off and back on, or hover a different link, and each is fetched once and
   cached; re-hovering is instant.

Keyboard users: focusing a link with Tab arms the same preview immediately (no
dwell needed).

### What gets a preview (and what doesn't)

- **External web links** (`http://` / `https://`) → web-title preview. ✅
- **In-app links** — file paths the agent wrote (`src/main/index.ts`),
  attachments you dropped, `omniscio://` deep links, saved-prompt links — these are
  NOT web pages, so they get no web-title preview (they keep their existing click
  behavior: opening a file, jumping to a session, etc.). ✅ (see
  [media-link-open.md](media-link-open.md))

### Why the fetch happens "behind the app" (and is safe)

The renderer (the UI) is deliberately not allowed to reach out to the internet
on its own — only Omniscio's trusted main process does that. So when you hover a link,
the UI asks the main process "what's the title of this page?", and the main
process does the fetch. That matters because the link text comes from chat
content, which Omniscio treats as untrusted. Every fetch is run through Omniscio's shared
network-safety guard, which:

- only allows `http`/`https` (no `file:`, `ftp:`, etc.);
- **refuses links that point at your own machine or private network** (localhost,
  `192.168.x`, the cloud-metadata address, and even a public-looking hostname that
  secretly resolves to a private address) — a standard defense against malicious
  links;
- gives up after 8 seconds and reads only the first ~100 KB of the page (the title
  lives at the very top), so a giant or slow page can't bog things down;
- reads only the page's title — nothing else is stored or sent anywhere.

The fetched title is shown as plain text, so a web page can't use its title to
inject anything into Omniscio.

### Limitations and v1 notes

- **Desktop only.** No hover on touch devices, so no preview and no fetch on mobile.
- **Best-effort title.** Pages behind a login, pages that render the title in
  JavaScript only, or pages that block the fetch show just the domain.
- **og:title preferred.** Omniscio uses the social-share title (`og:title`) when present,
  otherwise the browser-tab `<title>`.
- **No content beyond the title.** This is a title preview, not a full link-preview
  card (no description, no thumbnail).

## For agents

### How it works

Three layers:

1. **Renderer** — `CopyableLink` in
   [`agent-markdown-helpers.tsx`](/src/renderer/src/components/ui/agent-markdown-helpers.tsx)
   computes `canFetchTitle = isExternalWebLink && supportsHover()`. On pointer
   dwell (`LINK_TITLE_DWELL_MS`) or focus it arms
   [`useLinkTitlePreview`](/src/renderer/src/hooks/useLinkTitlePreview.ts), which
   calls `IPC.LINK_PREVIEW_FETCH_TITLE` and caches the result per-URL. The title
   renders as plain text inside the shared
   [`<Tooltip>`](/src/renderer/src/components/ui/Tooltip.tsx); the hostname stands
   in until it resolves.
2. **IPC** — the `link-preview` domain (channel
   [`ipc-channels/link-preview.ts`](/src/shared/ipc-channels/link-preview.ts),
   Zod [`ipc-schemas/link-preview.ts`](/src/shared/ipc-schemas/link-preview.ts),
   handler [`link-preview-handlers.ts`](/src/main/ipc/link-preview-handlers.ts)),
   mirroring the `screenshot` domain.
3. **Main** —
   [`link-title-fetcher.ts`](/src/main/services/link-preview/link-title-fetcher.ts):
   `fetchLinkTitle(url)` fetches through the SSRF-hardened
   [`redirectAwareFetch`](/src/main/services/redirect-aware-fetch.ts) (per-hop
   private-IP + DNS-rebinding checks), with an 8 s deadline + a ≤100 KB streamed
   read cap, extracts og:title then `<title>`, cleans it (decoding HTML entities to a
   bounded fixed point, so a double-encoded title such as Vimeo's `&amp;#x27;` renders
   as a real apostrophe), caches it (positive 6 h /
   negative 5 min), and returns `{ title }` — never throwing, so any failure shows
   the hostname instead.

The title extractor scans each `<meta>` tag in isolation so it stays linear-time
(a single hovered link to a hostile page can never freeze the main thread). The
invariants — SSRF reuse, ReDoS-safety, desktop-only, dwell-gating, caching,
plain-text rendering, first-link-only hint — are locked by tests and documented in
`.claude/memory/contracts/link-title-preview-contract.md`.

## Related

### Related pages

- [media-link-open.md](media-link-open.md) — clicking **file/media** links the agent
  wrote (opens in your OS default app); explicitly NOT about `http(s)` URLs.
- [deep-links.md](deep-links.md) — `omniscio://` in-app deep links.
