Skip to content

Clickable orca:// deep-links to focus a terminal tab#4386

Open
BorjaLL wants to merge 3 commits into
stablyai:mainfrom
BorjaLL:BorjaLL/orca-links
Open

Clickable orca:// deep-links to focus a terminal tab#4386
BorjaLL wants to merge 3 commits into
stablyai:mainfrom
BorjaLL:BorjaLL/orca-links

Conversation

@BorjaLL

@BorjaLL BorjaLL commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Closes #4384.

What

Adds clickable orca://focus/<terminal-handle> deep-links that focus a specific Orca terminal tab — clickable inside the embedded terminal and from anywhere on the OS. No notifications.

<terminal-handle> is the runtime-issued handle from orca terminal list --json (e.g. term_<uuid>).

How

Everything reuses the existing canonical focus action — no new focus logic:

runtime.focusTerminal(handle)ui:focusTerminalactivateTabAndFocusPane (the same path as orca terminal focus/switch and the notification-click handler).

A single router (src/main/startup/orca-deep-link-router.ts) serves every entry point:

Entry point Path
In-terminal OSC 8 click xterm linkHandler (already allowNonHttpProtocols: true) → handleOscLinkwindow.api.ui.openOrcaDeepLink → router
macOS (running / cold) app.on('open-url') → router
Windows/Linux (running) second-instance argv → router
Windows/Linux (cold) initial process.argv, deferred until the window is ready

Parsing is a pure shared module (src/shared/orca-deep-link.ts) so main and renderer classify links identically. Unknown orca:// URLs (e.g. the web-only orca://pair flow) are ignored; the window is only focused for a valid action, so a hostile terminal can't steal focus with garbage strings.

Scheme registration: setAsDefaultProtocolClient (with the dev-exe variant) + an electron-builder protocols entry that injects CFBundleURLTypes (macOS) / the Windows scheme.

Feasibility note — the "chat surface" is the terminal

In Orca, Claude Code (and every agent) runs inside the xterm terminal — there's no separate React-rendered markdown chat surface — so the delivery vehicle is OSC 8 hyperlinks, which is exactly how a markdown link like [label](orca://focus/term_…) renders into a terminal. Plain-text orca://… will not auto-linkify (WebLinksAddon only autodetects http/https); the link must be emitted as an OSC 8 hyperlink. Documented in docs/orca-deep-links.md.

Scope

  • No notifications (by design).
  • orca://worktree/<id> deferred — worktree ids are repoId::worktreePath and need encoding; focus-by-handle fully delivers the goal.

Tests

  • src/shared/orca-deep-link.test.ts — parse + argv extraction
  • src/main/startup/orca-deep-link-router.test.ts — routing, defer/replay, error path, ignore-unknown
  • src/renderer/.../terminal-osc-link-routing.test.ts — orca: branch forwards; non-modifier click ignored; http still routes to browser

pnpm typecheck (node/web/cli), oxlint, and the new tests all pass locally.

🤖 Generated with Claude Code

Register the orca:// URL scheme and route orca://focus/<handle> to the
existing terminal-focus action (runtime.focusTerminal), reused by both
in-terminal OSC 8 link clicks and OS-level open-url/argv deep-links.
No notifications.

- shared/orca-deep-link.ts: pure parse + argv extraction (+ tests)
- main/startup/orca-deep-link-router.ts: single router for every entry
  point — open-url, second-instance/argv, and renderer-forwarded OSC 8
  clicks; defers cold-launch links until the window is ready (+ tests)
- main/index.ts: register scheme, open-url, second-instance, initial
  argv, ipc bridge, flush on window ready
- renderer terminal-osc-link-routing.ts: orca: branch forwards to main
  via window.api.ui.openOrcaDeepLink (+ test)
- preload: openOrcaDeepLink bridge + type + web stub
- electron-builder: protocols entry -> CFBundleURLTypes
- docs/orca-deep-links.md: link format + OSC 8 emission requirement

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BorjaLL BorjaLL force-pushed the BorjaLL/orca-links branch from 56036a5 to a07be53 Compare June 15, 2026 10:11
Current main added registerHttpLinkStoreAccessor to http-link-routing.ts,
which store/index.ts calls at module import. The osc link routing test
mocked the whole module without this export, so the import chain threw at
load. Add the missing export to the vi.mock factory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BorjaLL BorjaLL marked this pull request as ready for review June 15, 2026 10:54
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d066167e-da02-4671-924b-c14e365d3d14

📥 Commits

Reviewing files that changed from the base of the PR and between b12fc61 and 69d8aaa.

📒 Files selected for processing (1)
  • config/electron-builder.config.cjs
✅ Files skipped from review due to trivial changes (1)
  • config/electron-builder.config.cjs

📝 Walkthrough

Walkthrough

This PR adds end-to-end orca:// deep-link handling. Shared parsing utilities and tests were added for Orca URLs and argv extraction. The main process now registers the scheme, creates a deep-link router, and wires OS, argv, IPC, and startup paths into it. Preload and renderer APIs gained openOrcaDeepLink, and the terminal OSC link handler forwards Orca links through that bridge. The electron-builder config also registers the protocol for packaged builds.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning It includes a solid summary, scope, and testing notes, but it omits required Screenshots, AI Review Report, Security Audit, and Notes sections. Add the missing template sections, including screenshots or 'No visual change', the AI review summary, security audit, and any platform-specific notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: clickable orca:// deep-links that focus a terminal tab.
Linked Issues check ✅ Passed The PR implements the requested orca://focus/ deep-links, scheme registration, routing, and OSC 8 terminal click handling while reusing the existing focus path.
Out of Scope Changes check ✅ Passed The changes stay focused on deep-link registration, routing, and tests; no unrelated code paths or features stand out.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 36786141-eb06-496b-b755-0fd836500991

📥 Commits

Reviewing files that changed from the base of the PR and between 668c711 and b12fc61.

📒 Files selected for processing (11)
  • config/electron-builder.config.cjs
  • src/main/index.ts
  • src/main/startup/orca-deep-link-router.test.ts
  • src/main/startup/orca-deep-link-router.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/terminal-pane/terminal-osc-link-routing.test.ts
  • src/renderer/src/components/terminal-pane/terminal-osc-link-routing.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/orca-deep-link.test.ts
  • src/shared/orca-deep-link.ts

Comment on lines +54 to +55
const link = parseOrcaDeepLink(url)
if (!link) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the parseOrcaDeepLink implementation
fd -t f "orca-deep-link" --extension ts --extension tsx | head -20

Repository: stablyai/orca

Length of output: 209


🏁 Script executed:

# First, let's find and examine the parseOrcaDeepLink implementation
fd -t f "orca-deep-link" --extension ts --extension tsx | head -20

Repository: stablyai/orca

Length of output: 209


🏁 Script executed:

# Read the shared orca-deep-link module
cat -n src/shared/orca-deep-link.ts 2>/dev/null || echo "File not found, searching..."

Repository: stablyai/orca

Length of output: 2598


🏁 Script executed:

# Find all orca-deep-link files
find . -name "*orca-deep-link*" -type f 2>/dev/null

Repository: stablyai/orca

Length of output: 217


🏁 Script executed:

# Search for parseOrcaDeepLink implementation
rg "parseOrcaDeepLink" --type ts --type tsx -A 15 | head -80

Repository: stablyai/orca

Length of output: 85


🏁 Script executed:

# Examine the router file completely
cat -n src/main/startup/orca-deep-link-router.ts 2>/dev/null | head -100

Repository: stablyai/orca

Length of output: 3574


Contain parser exceptions so malformed deep-links don't crash routing.

At Line 54, parseOrcaDeepLink(url) can throw an unhandled exception. While the parser's URL constructor is wrapped in try-catch, the decodeURIComponent call on line 35 of src/shared/orca-deep-link.ts is not. Malformed percent-encoding (e.g., orca://focus/%) triggers a URIError that escapes to the caller, breaking the router instead of treating the link as invalid. The existing deps.onError callback should handle this case.

🔧 Proposed fix
   const handle = (url: string): void => {
-    const link = parseOrcaDeepLink(url)
+    let link: ReturnType<typeof parseOrcaDeepLink> | null = null
+    try {
+      link = parseOrcaDeepLink(url)
+    } catch (error) {
+      deps.onError?.(error, url)
+      return
+    }
     if (!link) {
       return
     }

Comment on lines +58 to +60
deps.focusWindow()
void Promise.resolve(deps.focusTerminalByHandle(link.handle)).catch((error) => {
deps.onError?.(error, url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent focus-steal by focusing the window only after terminal resolution succeeds.

At Lines 58-60, the window is focused before focusTerminalByHandle succeeds. A syntactically valid but non-existent handle still brings Orca to foreground, which conflicts with the “ignore unknown/malicious links” intent.

🔧 Proposed fix
-    deps.focusWindow()
-    void Promise.resolve(deps.focusTerminalByHandle(link.handle)).catch((error) => {
-      deps.onError?.(error, url)
-    })
+    void Promise.resolve()
+      .then(() => deps.focusTerminalByHandle(link.handle))
+      .then(() => {
+        deps.focusWindow()
+      })
+      .catch((error) => {
+        deps.onError?.(error, url)
+      })

onSplitTerminal: () => noopUnsubscribe,
onRenameTerminal: () => noopUnsubscribe,
onFocusTerminal: () => noopUnsubscribe,
openOrcaDeepLink: () => {},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document why openOrcaDeepLink is a no-op in web fallback.

Line 2004 adds a non-obvious no-op behavior without rationale. Add a short why-comment so this doesn’t get “fixed” accidentally later.

Suggested patch
-    openOrcaDeepLink: () => {},
+    // Why: web clients do not have an Electron main-process deep-link router;
+    // keep this as a no-op to preserve preload API parity.
+    openOrcaDeepLink: () => {},

As per coding guidelines, "When writing or modifying code driven by a design doc or non-obvious constraint, add a comment explaining why the code behaves the way it does. Keep comments short — one or two lines, capturing only the non-obvious reason".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
openOrcaDeepLink: () => {},
// Why: web clients do not have an Electron main-process deep-link router;
// keep this as a no-op to preserve preload API parity.
openOrcaDeepLink: () => {},

Source: Coding guidelines

Comment on lines +35 to +36
const handle = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''))
if (!TERMINAL_HANDLE_PATTERN.test(handle)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In JavaScript, does decodeURIComponent throw a URIError for malformed percent-encoded input (e.g. "%E0%A4%A"), and is try/catch the standard way to handle untrusted URL path decoding?

💡 Result:

Yes, decodeURIComponent throws a URIError when it encounters malformed percent-encoded input, such as an incomplete sequence (e.g., "%E0%A4%A" or a lone "%") [1][2][3]. Using try/catch is considered the standard and necessary approach for handling untrusted or potentially malformed URI input when using legacy global functions like decodeURIComponent or decodeURI [4][3]. Because these functions throw a synchronous URIError that can crash execution if unhandled, wrapping them in a try/catch block is essential to safely manage invalid sequences [5][6][3]. However, for modern JavaScript development, it is often recommended to use the URL and URLSearchParams APIs whenever possible [4][3]. These APIs provide more structured, robust, and safer ways to handle URL parsing and decoding automatically, which reduces the need for manual try/catch blocks and mitigates common pitfalls associated with manual percent-decoding [4][3].

Citations:


🏁 Script executed:

cat -n src/shared/orca-deep-link.ts | head -50

Repository: stablyai/orca

Length of output: 2400


🏁 Script executed:

cat -n src/shared/orca-deep-link.ts | tail -20

Repository: stablyai/orca

Length of output: 722


🏁 Script executed:

find . -name '*orca-deep-link-router*' -type f

Repository: stablyai/orca

Length of output: 150


🏁 Script executed:

cat -n src/main/startup/orca-deep-link-router.ts | head -60

Repository: stablyai/orca

Length of output: 2917


🏁 Script executed:

cat -n src/main/startup/orca-deep-link-router.ts | sed -n '53,75p'

Repository: stablyai/orca

Length of output: 795


Guard decodeURIComponent to prevent unhandled URIError on malformed percent-encoding.

At line 35, decodeURIComponent throws a URIError for malformed percent-encoded sequences (e.g., %E0%A4%A), breaking the intended contract where invalid links are silently ignored. The URL constructor is already guarded (line 24), but the decode call is not. Wrap it in a try/catch block and return null on decode failure.

Proposed patch
-  const handle = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''))
+  let handle: string
+  try {
+    handle = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''))
+  } catch {
+    return null
+  }
   if (!TERMINAL_HANDLE_PATTERN.test(handle)) {
     return null
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handle = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''))
if (!TERMINAL_HANDLE_PATTERN.test(handle)) {
let handle: string
try {
handle = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''))
} catch {
return null
}
if (!TERMINAL_HANDLE_PATTERN.test(handle)) {
return null
}

@BorjaLL

BorjaLL commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main and moved out of draft. Conflicts resolved, and validated locally on a clean install: pnpm typecheck, pnpm lint, and the touched test suites all pass. Mergeable now. Review appreciated when you get a chance. cc @nwparker

@Jinwoo-H

Copy link
Copy Markdown
Contributor

This is neat, thanks for putting it together. I think I understand the routing piece now: once an orca://focus/<handle> link is opened, Orca can route it through the existing terminal focus path.

One thing I’m still trying to connect is the adoption path for agents. Since the motivating use case depends on agents emitting OSC 8 hyperlinks, how are you imagining agents will learn to produce those links in practice? Would that be through prompt instructions, a small CLI helper, orchestration output, or something else?

@BorjaLL

BorjaLL commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@Jinwoo-H good question, and it's deliberately the part this PR doesn't hardcode. The PR only builds the routing (scheme registration + the focus action) and stays agnostic about who emits the link, because emission itself is already trivial: any [label](orca://focus/<handle>) an agent writes renders as a clickable OSC 8 link in the xterm terminal. Plain-text orca://... won't autolinkify, only the OSC 8 form does. So there's nothing new for an agent to "learn" at the protocol level; the only input it needs is the terminal handle, which is the same term_... handle the orca CLI already uses for terminal focus/switch.

How I imagine adoption, smallest-first:

  1. Orchestration output is the natural first emitter. The orchestrator already knows the task DAG and the terminal handles, and already writes to terminals, so a coordinator reporting worker_done can drop a [open terminal](orca://focus/<handle>) link with zero agent-side changes. That covers the motivating multi-agent case directly.
  2. A prompt snippet + the agent's own handle. For a single agent, a one-line instruction ("to point the user at a terminal, emit [label](orca://focus/<handle>)") plus exposing its handle is enough. No code, just docs.
  3. A thin CLI helper if we want it ergonomic - something like orca terminal focus-link <handle> that prints the OSC 8 sequence, so a shell script doesn't hand-roll the escape.

I'd start with (1) since it needs no agent cooperation at all, and add the docs from (2). docs/orca-deep-links.md in this PR already documents the link format; happy to extend it with emission examples if that makes the path clearer for review.

@BorjaLL

BorjaLL commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@Jinwoo-H when you get a chance: this one (#4386) is green and mergeable again after a rebase on latest main, and it now closes #4384, which picked up an external +1 today ("very much needed"). Your earlier question about the agent adoption path is answered in the thread above.

Two other PRs you're the requested reviewer on are also green and ready whenever it suits: #4475 (keep the agent-hook payload off the curl command line) and #3337 (close the hidden background tab/PTY when an automation completes, your earlier UX question is addressed there too).

No pressure on any of them, and I'm happy to trim scope or make changes. If any aren't on the roadmap, just say and I'll close them out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clickable orca:// deep-links to focus a terminal/worktree

2 participants