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

Peek viewer (look at a file without leaving what you were doing)

A slide-over panel that shows a file in place — text, code, images — so you can read what an agent is talking about without opening an editor or losing your place. Covers opening one from a link or a path, finding text inside it, how it works on a phone, and how a path is resolved to the right copy of a file.

What it is

What it is

A side panel (drawer on desktop, full-screen sheet on mobile) that previews a single file without leaving the chat. You open it by clicking a file path or image link that an agent printed in its message. Code files render with syntax highlighting; images render through the shared ImageCanvas (pinch-to-zoom on mobile, click-to-zoom on desktop).

For files whose extension maps to a renderable format, a Type toggle in the header flips between the raw source and a rendered preview. The peek viewer renders four formats through its shared PreviewRenderer: Markdown (.md / .mdx), SVG (.svg, sanitized markup), Mermaid diagrams (.mmd / .mermaid, lazy-loaded), and LaTeX / KaTeX (.tex / .latex, sanitized HTML). The toggle is hidden for any other extension. HTML is the deliberate exception: .html / .htm peeks render as raw code only, never through an iframe — the toggle is suppressed for them so the peek viewer's security posture is unchanged. (The sandboxed-iframe HTML render path lives in the separate File Explorer file viewer, not the peek overlay.)

PDFs are the other special case. A .pdf peek hands the whole body to Omniscio's built-in PDF viewer (paginated, zoom, selectable text, find, convert-to-text, Print — see media-link-open.md). A PDF has no raw view, so the header shows no Type toggle and no lines · chars · tokens line for it — those only make sense for text, and for a PDF they would describe its base64 encoding rather than the document. The header keeps the file name, path copy, Share / Send-to-agent and Close.

The peek viewer also has sibling-image navigation. When you click an image link inside an agent message that contains multiple image links, those other images become "siblings" of the one you opened. You can flip through them in place — left/right arrow buttons flank the image, a small N / M counter sits at the bottom-center, and ArrowLeft / ArrowRight on the keyboard work too. On mobile you swipe left to advance and right to go back. Navigation wraps at edges, so the last image's "next" goes back to the first. When the agent message has only one image, the arrows, counter, and swipe handlers stay hidden entirely — the viewer behaves exactly like the pre-navigation peek viewer.

Where to find it

From a file link or path wherever it appears in the app — the peek opens as a slide-over on top of what you were already reading, so you close it and you are back. It opens in the window you clicked from, so a link in a popped-out session window, a project pop-out, the KMS window, the Writer window or a project-document window shows the file right there rather than nothing happening.

A link that is relative opens too, wherever you click it from a document rather than a session: it resolves the way markdown says it should, against the folder of the document that carries it — so [DECISIONS.md](DECISIONS.md) inside a project doc opens its neighbour, not something in the project root. And a link to a folder opens in the in-app file list only where that list exists (the main window); anywhere else it hands the folder to your computer's own file manager, which is where folder links already go for folders outside a project.

How it behaves

How to use it

  1. Open a chat session with an agent that produces file paths or image links in its replies (any provider).

  2. Click an image link in an agent message. The peek viewer slides in from the right (desktop) or up from the bottom (mobile) showing that image.

  3. If the same agent message has more image links, three things appear on top of the image:

    • Left chevron at the middle-left edge — previous image.
    • Right chevron at the middle-right edge — next image.
    • Position counter at the bottom-center, shaped like 2 / 5.

    The counter stays hidden when the message has only one image link.

  4. Navigate the siblings any of three ways:

    • Click the on-screen left / right chevron buttons.
    • Press ArrowLeft / ArrowRight on the keyboard. (No modifier — plain arrows. Holding Ctrl/Cmd/Alt/Shift falls through to OS / app shortcuts as normal.)
    • Swipe horizontally on the image area on mobile. Swipe left to advance to the next image, swipe right to go back. Vertical scrolls and pinch-to-zoom are unaffected.
  5. Wrap-around is automatic. The first image's "previous" jumps to the last; the last image's "next" jumps back to the first. There's no edge stop and no edge dimming — it's a circular ring.

  6. Close the viewer with the X button, the desktop backdrop click, the Android back gesture (mobile), or Escape on the keyboard. Sibling state is forgotten — opening a different image link starts a fresh sibling group.

What "sibling" means

A sibling is another image link in the same agent message bubble. The viewer walks the DOM up from the link you clicked to the nearest message-bubble container ([data-msg-summary]) and collects every other image link inside that bubble in document order. Image links in different messages are never siblings, even if they appear adjacent on screen. This matches what users intuitively expect: "show me the rest of the images this agent message mentioned."

Operator-uploaded images (the chips you attach in the composer) already had their own lightbox with its own arrow nav — that lightbox is unchanged. Sibling-image navigation lives only in the peek viewer that opens from agent-message links.

When navigation is suppressed

The arrow buttons, counter, keyboard handler, and swipe handler are all gated together so the viewer never gets caught in a half-state where one input works and another doesn't:

  • Single image. Hidden entirely (not greyed out / disabled). A one-image preview looks identical to the pre-Phase-7 peek viewer.
  • Editing the file. When you click the pencil icon to edit a text file's contents, arrow keys belong to the textarea (cursor movement). The keyboard handler yields. The arrow buttons + counter still show because images aren't editable, but in practice editing only applies to text files where there are no sibling controls anyway.
  • A confirmation dialog is open. If you have unsaved edits and hit "Discard?", the inner confirm dialog owns the keys until you answer.
  • Multi-touch / pinch. If you put a second finger down at the start of a touch, or add it mid-gesture, the swipe is aborted — pinch-to-zoom belongs to the ImageCanvas zoom engine, not nav. The next single-finger touch starts cleanly.
  • Zoomed in. While the image is zoomed past fit, sibling-swipe is suspended entirely — a one-finger drag pans the zoomed picture instead of flipping to the next image. ImageCanvas reports rest↔zoomed crossings via onZoomedChange, which clears swipeNavEnabled; it resets to fit (and re-enables swipe) on each key={filePath} remount.
  • Vertical drag dominance. A swipe must travel more than 50 pixels horizontally and more horizontally than vertically. A 50px-exact drag stays in the no-op band. This means scrolling the page on mobile (mostly vertical) doesn't accidentally flip images.
  • Modifier keys. Plain ArrowLeft / ArrowRight only. Ctrl+ArrowLeft, Shift+ArrowRight, etc. fall through to the OS / app — they're never intercepted.
  • Focus inside an <input>, <textarea>, or [contenteditable]. Arrow keys belong to the focused field for text editing; the peek handler yields.

Counter accessibility

The N / M counter at the bottom-center is wrapped in role="status" aria-live="polite" with aria-label="Image N of M". Screen readers will announce the new position when you flip to a sibling.

Find in the file (Ctrl+F)

When you're viewing a text or Markdown file (not an image or PDF), press Ctrl+F (Cmd+F on macOS) to open a browser-style find bar at the top-right of the content. Type to highlight every match with a "N of M" counter; Enter jumps to the next match and Shift+Enter to the previous one (or use the up/down chevrons), scrolling the active match into view. Esc or the ✕ closes the find bar and clears the highlights — and while it's open, Esc closes the find bar first, not the whole viewer.

Find works in both the raw view and the rendered Markdown preview, and it searches only the file that's currently open. It's a desktop feature (a phone has no Ctrl key) and is available while viewing, not while editing. When the peek viewer is open, its Ctrl+F takes priority — the Settings find bar never opens behind it. Under the hood it shares the same find engine as the Settings and in-session find, so highlighting and matching behave identically across the app.

Mobile loading + caching

On mobile / web (any browser context — no Electron preload) image bytes stream over the /file?path=...&token=... HTTP route on the Omniscio web server, not over the WebSocket IPC channel that desktop uses. Two things follow:

  • The spinner shows for the entire fetch. Until the underlying <img> reports complete, the peek body keeps isLoading = true. A semi-opaque LoadingPanel overlays the image area so you have continuous visual feedback during the download — multi-MB photos over a slow Tailscale link no longer look frozen. Once the image decodes, ImageCanvas fires its onLoadComplete callback and the store flips isLoading = false, which removes the spinner overlay. If the load fails (404, network drop, decode error), the same path fires onLoadError and you see the red error UI instead.
  • Sibling navigation reuses the browser cache. The /file route returns Cache-Control: private, max-age=300, must-revalidate plus a weak ETag (W/"<size>-<mtimeMs>") and a Last-Modified stamp. On every subsequent view the browser issues a conditional GET (If-None-Match or If-Modified-Since) and the server short-circuits to a bodyless 304 when the file hasn't changed. So flipping back-and-forth across siblings touches the network for headers only — the bytes come from the disk cache. First-pass nav still pays the network cost; the win is on the second visit per sibling and on every revisit during a single peek session.

If-None-Match takes precedence over If-Modified-Since (per RFC 7232 §6) when both headers arrive on the same request — so you don't need to worry about the two cache validators racing. The HTTP-date format only has 1-second resolution, so the server truncates mtimeMs to whole seconds before comparing it against If-Modified-Since to avoid spurious 200s on sub-second-modified files.

Stale-resolution guards prevent rapid sibling flicks from showing the wrong image: the store's openEpoch counter increments on every open and the IPC path discards in-flight resolutions whose epoch no longer matches; ImageCanvas's onLoad / onError callbacks pass the exact src they observed and the store ignores the call when state.content !== loadedSrc. So the previous image's late-firing onload after you've already navigated past it is a no-op — never a UI flicker.

Mobile text viewing + editing

On a phone the peek viewer is a full read-and-edit surface, not a read-only sheet:

  • Raw text wraps for prose. In Raw mode the file body wraps to the panel width (whitespace-pre-wrap + break-words) for markdown and plain text — any file with no detected highlight language — so long lines and long paths / URLs stay readable without sideways scrolling. This holds on both desktop and mobile. Code and data files (a detected highlight language other than markdown — .ts, .json, .yaml, .tex, …) keep horizontal scroll (overflow-x-auto) on desktop so indentation / alignment stays intact and you scroll to read, which the wider drawer affords; on a phone every file wraps (a small screen can't scroll sideways comfortably). The wrap-vs-scroll choice keys off the file's detected language (isMobile || !language || language === 'markdown'). The Type preview (Markdown / SVG / …) is unaffected; it already reflows.
  • Editing is available on mobile. The Edit pencil and the Save / Cancel controls show on phones, not just desktop. Saving goes through the same FILES_WRITE_ABSOLUTE path desktop uses, which already works over the mobile web route (e.g. a Tailscale tunnel). The editor textarea uses a 16px font on mobile (vs the denser desktop size) so iOS Safari doesn't auto-zoom the page when you tap into the field, and the pencil gets a larger tap target.
  • The Android back-gesture protects unsaved edits. Backing out of an in-progress edit with unsaved changes shows the same "Discard changes?" confirmation the X button and Escape use, instead of silently dropping the edit. The mobile back handler re-arms its history sentinel on "keep editing" so a later back still dismisses the viewer.

Editing stays gated to raw, non-truncated text files on every platform — a truncated file (>1 MB) stays view-only so a partial save can't overwrite the real file, and .html peeks stay raw-only for the iframe-security reason above.

Concurrent edits don't clobber each other. The two globally-editable docs (CLAUDE.md / MEMORY.md) are editable from desktop and a paired phone, so two surfaces can have the same file open at once. To stop one save from silently wiping the other's, the editor records the file's on-disk timestamp when it loads and echoes it back on Save; if the file changed on disk in between (the other surface saved first), the save is refused with "This file changed on disk since you opened it. Copy your edits, then reopen it to load the latest version." — your unsaved text stays in the editor so you can copy it and reopen the newest version. A first-ever save with no recorded timestamp still goes straight through.

Mobile crash recovery (stale-chunk reload)

FilePeekOverlay is React.lazy()-loaded — its module is fetched on first open. On a long-lived mobile tab, that fetch can fail after a desktop rebuild has replaced hashed chunk filenames: the tab is still running yesterday's entry chunk that references hash XYZ, but the server only has ABC now. Three layers cooperate so the user-facing tap "just works" without showing a loading state or perceived crash:

Layer 1 — boot-time chunk prefetch (primary)

Both bootstrap paths in App.tsx call prefetchFilePeekChunk() from mobile-prefetch.ts during requestIdleCallback (with setTimeout(200) fallback for Safari). This fire-and-forget dynamic import warms the FilePeekOverlay chunk before the user ever taps a link.

On a fresh tab where the chunk hashes still match the server, the prefetch hits the cache and the first tap is instant. On a stale tab where the chunk hashes have changed, the prefetch is exactly what trips vite:preloadError — but it trips at idle time (seconds into the page lifecycle, while the user is reading the inbox), which routes the reload through Layer 2 silently in the background. By the time the user actually taps a markdown link, the page has already reloaded and the fresh chunk is in cache.

This is the primary fix for the "tap → blank → reload" perceived crash. The user never sees a tap-then-reload sequence because the reload happens at boot, not at tap.

Layer 2 — preload-reload-guard (background recovery)

When vite:preloadError fires (from the boot-time prefetch OR from any other lazy boundary that runs before the prefetch warmed the chunk), the preload-reload-guard catches it and reloads the page once per page lifecycle so the fresh index.html picks up the new chunk hashes. The store also persists the click intent before any async work so a tap that does race the prefetch can be replayed after the reload — gated by three independent safeguards so a stale entry never spuriously re-opens a peek hours later. See "Write / Reload / Restore paths" below.

Layer 3 — visible Suspense fallback (edge-case safety net)

If the user somehow taps a markdown link before the prefetch resolves (extremely fast tap within the first ~200ms of boot, or before idle-callback fires), the React.lazy boundary briefly mounts FilePeekLoadingFallback.tsx — a panel-shaped placeholder matching the eventual viewer's shape: same right-slide drawer on desktop, same full-screen sheet on mobile, same close button, plus a spinner and the file name pulled from the store. Tapping a link registers within one frame, before the chunk fetch even starts.

The fallback uses only entry-chunk-resident dependencies (framer-motion, lucide-react, Zustand, useIsMobile) so adding it pulls nothing new into the startup bundle. Adding markdown-vendor weight here would defeat the lazy-load purpose. In practice the fallback is rarely visible because the prefetch wins the race, but it's the floor of the user experience when the prefetch hasn't finished yet.

Layer 2 internals — write / reload / restore

Write path (open)

useFilePeekStore.openFile(filePath, lineNumber, siblings) writes {filePath, lineNumber, siblings, ts} to sessionStorage under peek-intent synchronously, BEFORE the IPC await and BEFORE the React.lazy import boundary. (close clears the entry so a clean close doesn't trigger restore.) ts is the millisecond timestamp at write time — used by the TTL gate on restore.

Reload path (preload-reload-guard)

When vite:preloadError fires, the guard:

  1. Writes a last-preload-error breadcrumb ({url, message, timestamp}) — fired even when the guard suppresses the reload, so a SECOND chunk failure on the same lifecycle is still recorded.
  2. Checks the once-per-page chunk-reload cookie. If set, returns without reloading (loop cap).
  3. Otherwise: sets chunk-reload, sets peek-restore-armed=1, and calls location.reload(). The arm flag is set ONLY when reload actually fires — a suppressed second failure must NOT re-arm restoration.

The chunk-reload cookie is cleared in the next page's load listener so a later (post-success) chunk failure can reload again.

Restore path (boot, browser-only)

main.tsx calls restorePeekIntent() after installPreloadReloadGuard(). The function walks five gates, each self-clearing the state it consumes so nothing carries forward to the next lifecycle:

  1. Diagnostic breadcrumb — read+log+delete last-preload-error. console.warn('[peek-restore] preload error from previous lifecycle:', diag) so the next investigation can correlate the reload with the failed chunk URL. Cleared regardless of the rest of the gates.
  2. Arm flag — read+delete peek-restore-armed. Manual F5 / Cmd+R / tab-restore preserves sessionStorage but never sets the flag, so a manual refresh must NOT replay. The flag is read AND deleted before the intent gate so a stale flag from a browser quirk can't haunt the session.
  3. Intent entry — read+delete peek-intent. If absent, return. Anti-loop: delete BEFORE attempting replay so a chunk that persistently fails self-clears instead of restore-fail-restore-fail forever. The arm-flag enforcement (armed !== '1' → return) runs AFTER the intent delete so a manual F5 still one-shot-clears the stale intent rather than carrying it forward into a future reload-driven boot.
  4. Shape validation — filePath must be a non-empty string. Malformed entries are dropped silently.
  5. TTL — entries with ts older than 60 seconds are dropped. Defends against an arm flag that survives a quirky navigation flow we didn't anticipate.

Replay calls openFile(filePath, lineNumber, siblings) with a module-scoped suppressNextPersist flag set so the synchronous writePeekIntent inside doesn't re-write the entry we just deleted. The flag is single-shot — a subsequent user action (sibling nav, fresh open) writes a new intent normally.

The whole path is no-op in Electron (isElectron short-circuits both write and restore) — chunks load from disk on desktop and the failure mode does not exist.

Limitations and v1 notes

  • Sibling group is captured at click time. If the agent appends a new image to the message after you opened the peek viewer (rare but possible during streaming), the peek viewer won't see it — close and reopen to refresh.
  • No "open all in a gallery view" — sibling nav is in-place only. There's no contact-sheet UI.
  • Image links in code blocks aren't siblings. The walker scopes to the message bubble (data-msg-summary) but skips inert markdown that doesn't render as anchors. If an image path appears as fenced code text rather than an image link, it's not part of the sibling set.
  • No telemetry for sibling-nav input methods. Existing peek viewer events don't track input modality, so adding only sibling-nav events would be inconsistent. If we later add peek viewer telemetry generally, sibling-nav method (button / keyboard / swipe) is the natural attribute to include.

For agents

How it works

Three layers cooperate:

  1. Discovery — findSiblingImageHrefs walks up from the clicked anchor to its [data-msg-summary] ancestor (set on every agent / operator bubble in MessageBubble.tsx), then collects every <a> inside that bubble whose data-href (or href) ends in a renderable image extension (png, jpg, jpeg, gif, webp, svg, bmp, ico). Pure DOM read — no IPC, no store subscription. Returns siblings in document order including the clicked one (so the click handler doesn't have to think about "where am I in this list").

  2. Click wiring — agent-markdown-helpers.tsx's CopyableLink onClick captures e.currentTarget (the anchor element) and, if the link looks like an image, calls findSiblingImageHrefs(anchorEl) and forwards the result as the third arg to useFilePeekStore.openFile(absPath, lineNumber, siblings). Non-image links never call the helper, so text-file peeks stay zero-cost.

  3. Store + navigation — useFilePeekStore tracks siblings: string[], siblingIndex: number | null, and an epoch counter that increments on every open. nextSibling / prevSibling resolve the next href, increment epoch, and call the same internal load path as openFile — so the arrow-button click, the keyboard handler, and the swipe handler all funnel through one code path. The epoch guard means a fast-flick (next, next, next before the first load resolves) drops every stale load result; only the latest wins.

  4. UI overlay — FilePeekOverlay reads siblings + siblingIndex and gates canNavigateSiblings = isImage && siblings.length > 1 && siblingIndex !== null. When false, the entire block of arrows + counter + swipe handlers is unmounted (not just disabled). The keyboard listener registers conditionally on the same gate; the touch handlers no-op via swipeNavEnabled.

Path resolution: scope gate + worktree discovery

Which project does the click scope against? For an absolute link the path names its OWN project, so the click resolves the owning project from the path itself — the registered project whose folder contains it — even when you are viewing a different session — and scopes the read there. This is what lets a link to a file that lives in another registered project open from THAT project instead of being rejected against the session you happen to be looking at (the cross-project card-link fix). Only when the path is under no known project does it fall back to the active session, then any real project. A relative link has no owning project, so it stays scoped to the active session. See a-file-click-never-requires-an-active-session in the agent-markdown link-detection contract.

When an agent's message links to an absolute path (most commonly a file inside a git worktree the agent created), Omniscio has to decide whether that path is "in scope" — i.e. close enough to the active project that the user shouldn't have to manually grant access. The decision happens in scopeAbsolutePathToProject and runs four layers from cheapest to most authoritative:

  1. Project workdir + Omniscio-recorded worktrees + ~/.claude/. Always allowed. Standard case.
  2. Convention carve-out. Three workdir-anchored conventions for sibling/in-repo worktree storage are trusted implicitly, even when Omniscio has no DB row for the worktree:
    • <workdir>-worktrees/<branch>/ — Omniscio's own dash-suffix sibling
    • <workdir>.worktrees/<branch>/ — Claude Code period-suffix sibling
    • <workdir>/.worktrees/<branch>/ — Claude Code in-repo dotfile dir
  3. Cross-convention read fallback. If the original path scope-passes but the file doesn't exist on disk, Omniscio swaps the convention at the first matching segment and tries up to two alternates — so a link the agent wrote with the dash convention still opens when the file actually lives under the period convention.
  4. git worktree list discovery (Layer 3). For arbitrary layouts the convention list doesn't anticipate (e.g. <parent-of-workdir>/.worktrees/<branch>/ — a 4th convention some agents invented), Omniscio shells out to git -C <projectWorkDir> worktree list --porcelain and trusts every worktree path git itself reports. Results are cached for 30 seconds per project so the shell-out doesn't hammer git on every click. Silent fallback if git is missing on PATH, the project isn't a repo, or the spawn throws — Layers 1–3 still apply.

The directory has to actually be a git-listed worktree for Layer 3 to allow it — directory existence alone is not enough. A folder literally named <project>.worktrees-malicious/ is rejected because path.relative enforces segment boundaries; bare prefix matching would be a security hole.

Windows: a WSL / Git-Bash /mnt/<drive>/… link is normalized first. An agent that derives a link from a Bash pwd (the audit orchestrators do) emits e.g. /mnt/m/Agent Orchestrator/… for a file that physically lives at M:\Agent Orchestrator\…. The Windows main process can never stat the POSIX form, so FILES_READ_ABSOLUTE runs wslPathToWindows (/mnt/<drive>/… → <DRIVE>:\…, win32 only, single-letter mounts only) before the layers above — the real file then resolves through the SAME scope gate. It widens nothing (the translated path is still scope- and existence-checked, exactly like every other alternate). Without it a /mnt/… link scope-failed and, even after a grant, dead-ended at "Failed to read file" because the granted /mnt/… path still doesn't exist on disk.

The user does not need to grant access to agent-created worktrees following any of these layouts. The grant-on-the-fly confirm banner ("This file is outside your project's allowed folders — always allow folder?") only appears for paths outside every layer.

On mobile / a paired phone the outside-scope banner is read-only. The grant IPC (FILES_GRANT_AD_HOC_ACCESS) is blocked on the web/WS bridge by design (a paired phone must not widen the desktop's file-access scope), so the "Open this once" / "Always allow folder" buttons render only in the desktop app. On a phone the banner shows the path plus an "open it from the desktop app" note and a single Close — before this, tapping the grant buttons on a phone hit the blocked channel and re-armed the banner every tap, an infinite loop.

When the resolved path scope-passes but the file isn't there

Scope passing is not the same as the file existing. The renderer commits to one base when it turns a relative link into an absolute path (session.worktreePath ?? agentDetectedWorkdir ?? project.folderPath), and if it picks the "wrong" base for how the agent wrote the href, the absolute path is syntactically valid (scope-passes) but points at nothing.

The two session bases stand on their own. They are real absolute paths, so a project row that hasn't loaded into the renderer yet does not stop the click. Previously the project was checked first, so a missing project row discarded a perfectly good worktree and the click fell back to a project-folder-only search that can never see a sibling worktree — dead-ending at "Failed to read file" with the file sitting in the worktree the whole time (the chapter-9 postmortem). A project that IS loaded but is a virtual one — a sentinel like __inbox__, with no folder on disk — still stops resolution, and the click says it can't be opened from here rather than guessing a path. The read handler (FILES_READ_ABSOLUTE) recovers by trying alternates — first match that scope-passes and exists wins:

  1. Convention-swap — swap -worktrees ↔ .worktrees ↔ /.worktrees.
  2. Main→worktree rebase — a path under the main checkout re-based onto each git-reported worktree (covers a worktree-relative href resolved against the main root).
  3. Worktree→main unnest — the mirror of (2). When the link was written relative to the main root (.worktrees/<branch>/docs/x.md, how the file looks from the project root) but the base was the worktree, the join doubles the segment: <worktree>/.worktrees/<branch>/docs/x.md. This re-anchors the worktree-relative tail back at the project root, collapsing the duplication.
  4. Reaped-worktree → main checkout. When the worktree the link points into was landed and then cleaned up (a merge auto-lander removes the worktree folder + branch after merging its work to the main branch), that folder is gone from disk and from git worktree list — so alternates (2) and (3), which only trust worktrees git still reports, skip it entirely. This one recognizes a path inside any known worktree-storage folder — the three conventions next to the project, or a relocated worktree folder configured in worktree-locations.json (Omniscio can keep worktrees on a dedicated drive, e.g. L:\AMC-Worktrees, and it reads that same config so recovery can't drift from where worktrees really live) — and re-anchors the tail at the main project folder (<container>/<name>/<tail> → <workdir>/<tail>), where the merged file now lives. It re-anchors only to a path inside the project folder and is tried last, so a live worktree always resolves first. This is why a link into a since-cleaned-up worktree opens the file's current copy instead of dead-ending on "Failed to read file" (or, before this, the "outside your folders" banner). Read only — saving a file is never redirected this way (a save to a dead worktree path fails rather than silently overwriting the main copy).
  5. Project memory note. A link like [Route builds through the pipeline](route-builds-through-the-pipeline.md) inside a project's MEMORY.md (the AI's memory index) points at a bare note name. When an agent echoes that link into a chat message and you tap it, Omniscio joins it against the project folder — but those notes don't live in the repo at all. They live in the AI's per-project memory folder (~/.claude/projects/<project>/memory/). When every worktree alternate above misses, Omniscio looks up the note there by its filename and opens it. This only fires for markdown (.md) notes, only when the link was already pointing inside the project (an unrelated outside link is never redirected to a same-named note), and only as the last resort — so a real project file always wins. It needs no new permission: the AI memory folder is under ~/.claude/, which the peek viewer already trusts. Read only.

Every alternate is re-checked through the same scope gate (no bypass — alternates (2)/(3) trust only git-reported worktrees, (4) only the known worktree-storage folders (the conventions under the project or a configured relocated one) and always re-anchors inside the project folder, (5) only the project's own ~/.claude/ memory folder and only for links already in project scope, and every candidate re-anchors inside a folder Omniscio already trusts), and the handler returns the resolved path — the viewer adopts it, so the header shows the real file and a later Edit→Save writes to it rather than the ghost double. If nothing resolves, the failure is logged (it used to be silent, which is why this class of bug historically left no trace) and the viewer shows "Failed to read file."

The relative-path read (FILES_READ_FILE, used when the renderer had no base to build an absolute path from) now behaves the same way: it logs the path it wanted and the folder it searched, and it reports "Couldn't read the file — it no longer exists (moved or deleted)" instead of the bare "Failed to read file". That handler only ever searches the project folder and its subfolders, so it cannot reach a sibling worktree — which is exactly why the renderer must produce the absolute path first.

Related

Related pages

Last verified 2026-10-04