Deep Linking URL Scheme Handler Implementation in Craft Agents OSS

Craft Agents OSS implements deep linking through a custom craftagents:// URL scheme that parses incoming links in the server-core RPC layer, routes navigation commands via inter-process communication, and renders the appropriate workspace or view in the Electron main process.

Craft Agents OSS (lukilabs/craft-agents-oss) uses a custom craftagents:// protocol to enable deep linking from external applications and web browsers into specific workspaces, views, or actions within the Electron application. The deep linking URL scheme handler implementation spans three architectural layers: URL parsing in the server-core, RPC routing between processes, and Electron main-process navigation.

Three-Layer Architecture

The implementation consists of three cooperating layers:

Layer Responsibility Key Source Location
URL Parsing Converts craftagents:// URLs into structured navigation objects packages/server-core/src/handlers/rpc/system.ts (lines 69-133)
RPC Routing Receives OPEN_URL requests and pushes deeplink.NAVIGATE commands to renderers packages/server-core/src/handlers/rpc/system.ts (lines 80-110)
Electron Navigation Forwards navigation RPCs to the appropriate window via the sink function apps/electron/src/main/deep-link.ts (lines 320-340)

URL Parsing in Server-Core

The parseInternalCraftAgentsDeepLink function in packages/server-core/src/handlers/rpc/system.ts validates and decomposes incoming URLs into structured navigation instructions.

function parseInternalCraftAgentsDeepLink(parsed: URL): ParsedInternalDeepLink | null {
  if (parsed.protocol !== 'craftagents:') return null

  const host = parsed.hostname
  const pathParts = parsed.pathname.split('/').filter(Boolean)
  const windowMode = parsed.searchParams.get('window')

  // "window=focused/full" → let the client open a new window
  if (windowMode === 'focused' || windowMode === 'full') {
    return { requiresExternalOpen: true }
  }

  // OAuth callback – no navigation needed
  if (host === 'auth-callback') {
    return { handledNoop: true }
  }

  // Compound routes (e.g. allSessions, flagged, settings)
  if (COMPOUND_ROUTE_PREFIXES.has(host)) {
    const viewRoute = pathParts.length > 0 ? `${host}/${pathParts.join('/')}` : host
    return { navigation: { view: viewRoute } }
  }

  // Action routes (e.g. craftagents://action/close?reason=idle)
  if (host === 'action') {
    const action = pathParts[0]
    const actionParams = collectDeepLinkParams(parsed, pathParts[1])
    return { navigation: { action, actionParams } }
  }

  // Workspace-scoped routes
  if (host === 'workspace') {
    const workspaceId = pathParts[0]
    const routeType = pathParts[1]

    if (COMPOUND_ROUTE_PREFIXES.has(routeType)) {
      return { workspaceId, navigation: { view: pathParts.slice(1).join('/') } }
    }
    if (routeType === 'action') {
      const action = pathParts[2]
      return {
        workspaceId,
        navigation: { action, actionParams: collectDeepLinkParams(parsed, pathParts[3]) },
      }
    }
  }

  return null
}

The parser returns a ParsedInternalDeepLink object that specifies what to navigate to (view or action), where (workspace ID), or whether the link requires external window handling.

RPC Routing and Navigation Dispatch

When the server receives an OPEN_URL request via RPC_CHANNELS.shell.OPEN_URL, the handler in packages/server-core/src/handlers/rpc/system.ts routes the request based on the parsed result.

server.handle(RPC_CHANNELS.shell.OPEN_URL, async (ctx, url: string) => {
  const parsed = new URL(url)

  // Internal deep-link handling
  if (parsed.protocol === 'craftagents:') {
    const deepLink = parseInternalCraftAgentsDeepLink(parsed)

    if (deepLink?.handledNoop) return               // auth-callback – ignore
    if (deepLink?.navigation?.view || deepLink?.navigation?.action) {
      // Target may be a different workspace → route accordingly
      const target = deepLink.workspaceId && deepLink.workspaceId !== ctx.workspaceId
        ? { to: 'workspace' as const, workspaceId: deepLink.workspaceId }
        : { to: 'client' as const, clientId: ctx.clientId }

      server.push(RPC_CHANNELS.deeplink.NAVIGATE, target, deepLink.navigation)
      return
    }

    // Window-management request or unknown shape → fall back to client
    const deepLinkResult = await requestClientOpenExternal(server, ctx.clientId, url)
    if (!deepLinkResult.opened) throw new Error(`Cannot open URL on client: ${deepLinkResult.error}`)
    return
  }

  // Normal external URLs (http, https, mailto, craftdocs)
  if (!['http:', 'https:', 'mailto:', 'craftdocs:'].includes(parsed.protocol)) {
    throw new Error('Only http, https, mailto, craftdocs, craftagents URLs are allowed')
  }
  const result = await requestClientOpenExternal(server, ctx.clientId, url)
  if (!result.opened) throw new Error(`Cannot open URL on client: ${result.error}`)
})

If the parsed deep-link contains a navigation payload (view or action), the server pushes RPC_CHANNELS.deeplink.NAVIGATE to the target renderer—either the current client or a specific workspace window. If the link requires a new window or uses an unrecognized shape, the server falls back to requestClientOpenExternal.

Electron Main-Process Navigation

The handleDeepLinkUrl function in apps/electron/src/main/deep-link.ts (lines 320-340) receives the deeplink.NAVIGATE RPC and forwards it to the appropriate window via the sink function.

if (target.view || target.action) {
  const navigation: DeepLinkNavigation = {
    view: target.view,
    action: target.action,
    actionParams: target.actionParams,
  }
  const wsId = target.workspaceId ?? windowManager.getWorkspaceForWindow(window.webContents.id)
  const resolvedClientId = resolveClientId?.(window.webContents.id)

  const clientId = resolvedClientId ?? (!resolveClientId ? preferredClientId : undefined)

  if (sink && clientId) {
    sink(RPC_CHANNELS.deeplink.NAVIGATE, { to: 'client', clientId }, navigation)
  } else if (sink && wsId) {
    sink(RPC_CHANNELS.deeplink.NAVIGATE, { to: 'workspace', workspaceId: wsId }, navigation)
  }
}

The sink function wraps webContents.send, delivering the navigation payload to the renderer process. The function determines the target window by checking the workspace ID through windowManager.getWorkspaceForWindow or resolving the client ID.

Protocol Definitions

The deep-link navigation contract is defined in packages/shared/src/protocol/routing.ts:

export interface DeepLinkNavigation {
  view?: string
  action?: string
  actionParams?: Record<string, string>
}

The RPC_CHANNELS constants are defined in packages/shared/src/protocol/events.ts, exposing shell.OPEN_URL for incoming links and deeplink.NAVIGATE for renderer-side routing.

Practical Implementation Examples

To open a specific session view from a React component:

import { RPC_CHANNELS } from '@craft-agent/shared/protocol'

function openSession(sessionId: string) {
  const url = `craftagents://allSessions/session/${sessionId}`
  // Send to server – it will route internally via deeplink.NAVIGATE
  window.api.call(RPC_CHANNELS.shell.OPEN_URL, url)
}

The server parses the URL, identifies allSessions/session/<id>, builds { navigation: { view: 'allSessions/session/<id>' } }, and pushes deeplink.NAVIGATE to the appropriate window, which renders the requested view.

Requesting a New Focused Window

To open a workspace action in a new focused window:

const url = 'craftagents://workspace/abc123/action/close?window=focused'
window.api.call(RPC_CHANNELS.shell.OPEN_URL, url)

The parser returns { requiresExternalOpen: true } due to the window=focused parameter. The server falls back to requestClientOpenExternal, causing the Electron client to spawn a new focused window that loads workspace abc123 and executes the close action.

Handling Authentication Callbacks

To process an OAuth callback without triggering navigation:

// The auth provider redirects to:
const callbackUrl = 'craftagents://auth-callback?code=xyz'

// In the renderer:
window.api.call(RPC_CHANNELS.shell.OPEN_URL, callbackUrl)

The parser returns { handledNoop: true } for the auth-callback host. The server returns immediately without pushing navigation events, allowing the authentication subsystem to extract query parameters like code or state without disrupting the current UI state.

Summary

  • Craft Agents OSS implements deep linking via the craftagents:// URL scheme, enabling external applications to launch specific workspaces, views, or actions within the Electron application.
  • The three-layer architecture separates URL parsing in packages/server-core/src/handlers/rpc/system.ts, RPC routing through shell.OPEN_URL and deeplink.NAVIGATE channels, and Electron main-process navigation in apps/electron/src/main/deep-link.ts.
  • The parser supports compound routes (e.g., allSessions, settings), action routes (e.g., action/close), workspace-scoped routes, and special handling for authentication callbacks and window management.
  • Navigation payloads follow the DeepLinkNavigation interface defined in packages/shared/src/protocol/routing.ts, containing optional view, action, and actionParams fields.

Frequently Asked Questions

What URL schemes does Craft Agents support?

Craft Agents supports craftagents:// for internal deep linking, craftdocs:// for document references, and standard external protocols including http:, https:, and mailto:. The shell.OPEN_URL handler in packages/server-core/src/handlers/rpc/system.ts validates protocols and rejects unrecognized schemes with an explicit error message.

How does Craft Agents handle OAuth callbacks?

OAuth callbacks use the craftagents://auth-callback host pattern. When parseInternalCraftAgentsDeepLink encounters this host, it returns { handledNoop: true }, causing the RPC handler to return immediately without pushing navigation events. This allows the authentication flow to consume query parameters like code or state without triggering any UI navigation or view changes.

Yes. Appending ?window=focused or ?window=full to a craftagents:// URL causes parseInternalCraftAgentsDeepLink to return { requiresExternalOpen: true }. The server then invokes requestClientOpenExternal, prompting the Electron client to spawn a new BrowserWindow with the specified workspace or action loaded, either in focused or full-screen mode.

What is the difference between view routes and action routes?

View routes (e.g., craftagents://allSessions/session/123) map to application screens and populate the view field in DeepLinkNavigation. Action routes (e.g., craftagents://action/close?reason=idle) trigger specific behaviors and populate both the action and actionParams fields. The parser in packages/server-core/src/handlers/rpc/system.ts distinguishes these via the URL host—action for actions and compound route prefixes for views.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →