Custom Jump Rules
Vibe Island shows every AI coding agent session in the notch. Clicking a session jumps to the terminal that runs it. For terminals Vibe Island knows natively (iTerm2, Ghostty, Warp, VS Code, Cursor, and others) that jump lands on the exact tab, pane, or window. For any other terminal or IDE, the default is to bring the app window to the front.
A jump rule closes that gap. Your app registers a URL scheme, you drop one JSON file on disk, and Vibe Island opens your URL with the hints it has about the session. Your app maps those hints to its own tab, pane, or window.
Vibe Island 1.0.22 or newer. No changes to how your app runs the agent.
How it fits together
Section titled “How it fits together”- Vibe Island brings your app to the front.
- Once focus has settled, it opens your URL with the session hints filled in.
- Your app receives the URL and switches to the matching tab, pane, or window.
If no rule matches the app’s bundle identifier, behaviour is unchanged: built-in terminals keep their native jump, everything else is activated as a window.
Step 1: Register a URL scheme handler
Section titled “Step 1: Register a URL scheme handler”Your app owns the mapping from hints to its internal targets. Vibe Island never needs your tab tree, pane ids, or socket protocol.
Electron
// main processapp.setAsDefaultProtocolClient('yourterm');
app.on('open-url', (event, url) => { const parsed = new URL(url); if (parsed.host !== 'focus') return;
routeFocusRequest({ sessionId: parsed.searchParams.get('session'), tty: parsed.searchParams.get('tty'), pid: Number(parsed.searchParams.get('pid') || 0) || null, cwd: parsed.searchParams.get('cwd'), tmuxPane: parsed.searchParams.get('tmuxPane'), });});Native macOS (Swift)
// Info.plist: CFBundleURLTypes -> CFBundleURLSchemes = ["yourterm"]
struct TerminalFocusRequest { let sessionId: String? let tty: String? let pid: Int? let cwd: String? let tmuxPane: String?}
func application(_ app: NSApplication, open urls: [URL]) { guard let url = urls.first, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), components.host == "focus" else { return }
let query = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") })
TerminalFocusRouter.shared.focus(TerminalFocusRequest( sessionId: query["session"], tty: query["tty"], pid: query["pid"].flatMap(Int.init), cwd: query["cwd"], tmuxPane: query["tmuxPane"] ))}focus has one job: resolve the public hints to your own target and switch to it.
Step 2: Add one JSON file
Section titled “Step 2: Add one JSON file”Create ~/.vibe-island/integrations/<your-app-name>.json:
{ "displayName": "YourTerm", "bundleIdentifier": "com.example.yourterm", "jumpRule": { "method": "urlScheme", "template": "yourterm://focus?session={session_id}&tty={tty}&pid={pid}&cwd={cwd}&tmuxPane={tmux_pane}" }}Vibe Island loads the folder at launch. When a session’s bundle identifier matches, the rule is used for the jump. An explicit rule always wins over built-in kernel detection: if your terminal reuses a Ghostty, WezTerm, or xterm.js core but manages its own tabs and panes, the rule is what makes the jump precise.
Fields
Section titled “Fields”| Field | Required | Meaning |
|---|---|---|
displayName | No | Name shown in the notch panel |
bundleIdentifier | Yes | Your app’s macOS bundle identifier (the matching key) |
jumpRule.method | Yes | "urlScheme" is the only supported method today |
jumpRule.template | Yes | URL template with the variables below |
Find your bundle identifier:
mdls -name kMDItemCFBundleIdentifier /Applications/YourApp.appTemplate variables
Section titled “Template variables”Each variable is replaced and encoded as a URL query value. The examples show the decoded value your handler receives.
| Variable | Meaning | Example |
|---|---|---|
{session_id} | The agent session id | ses_abc123def456 |
{cwd} | Working directory | /Users/foo/my project |
{tty} | Terminal TTY device | /dev/ttys003 |
{pid} | Agent CLI process id | 12345 |
{bundle_id} | Terminal bundle identifier | com.example.yourterm |
{tmux_pane} | tmux pane id, only inside tmux | %59 |
Empty variables become empty strings. Nothing is left as a literal placeholder.
Single-window tools can match on cwd. Terminals with tabs or panes should not
rely on it alone: several panes in the same directory is the normal case.
Pane-aware terminals
Section titled “Pane-aware terminals”Your terminal owns its pane graph, so it should own the resolution too. The minimal contract:
- Every pane, tab, or window is uniquely addressable inside your app.
- Your handler accepts the public hints:
session,tty,pid,cwd,tmuxPane. - Your app resolves those hints to an internal target using its own index.
- When nothing matches, activate the app or do nothing. Never open a new pane or guess a similar one.
Suggested resolution order: session if you already correlate agent sessions
with targets, then tty if you record each pane’s PTY path, then pid by
walking up the process tree, and cwd only as the last resort.
Vibe Island passes a fixed set of public variables. Private pane identities are not part of this contract. If your product needs deeper integration, get in touch about a built-in adapter.
Debugging
Section titled “Debugging”# Fire the URL by hand and watch your app reactopen "yourterm://focus?session=test-123&cwd=/tmp"Vibe Island records the jump path in its diagnostic report: Settings > About > Export Diagnostic Report.
My app is not a terminal, but users run agents inside it. Does this work? Yes. When a user runs Claude Code, Codex, or another supported CLI inside your app, Vibe Island records your app’s bundle identifier for that session. With a rule in place the jump is precise.
Do I also need to send events to Vibe Island? No. A jump rule is independent of event delivery. If the CLI inside your app is a supported one, its events are already handled and only the jump was missing. Apps that also ship their own agent can add event mapping to the same file; see the integration guide linked from the app’s Settings.
Can I pass my own environment variables into the URL?
Not in this version. The variable set is fixed. Map session_id, tty, or
pid back to your target inside your app instead.
My terminal runs in a browser tab. Can it use this? Packaged apps such as Electron or Tauri builds can: they have their own bundle identifier and can register a URL scheme. Pure browser tabs cannot: macOS URL schemes bind to a bundle identifier, not a tab, and Vibe Island watches agent CLIs running as local processes.
Several forks share a bundle identifier prefix. How are they matched? By the full bundle identifier, never by prefix. Each fork ships its own file.
We are tmux-aware. What should we do with {tmux_pane}?
Focus the right tab first, then run tmux select-pane -t <pane> for the exact
pane.
Need help?
Section titled “Need help?”Open an issue or discussion at github.com/vibeislandapp/vibe-island.