Docs/Flow Transfer API

Flow Transfer API

Universal Developer

Flow Transfer API

Overview

Flow Transfer is a cross-plugin data handoff system, similar to “share” on mobile but more flexible and structured.

Introduction

This document covers current capabilities:

  • Sender: dispatch() + target selector
  • Target: onFlowTransfer() + acknowledge() / reportError()
  • Native share: nativeShare()

Permission note: Flow Transfer is gated by the Permission Center. Unauthorized requests return PERMISSION_DENIED and trigger consent UI.

Core Concepts

Flow payload

EXAMPLE.TYPESCRIPT
interface FlowPayload {
  type: 'text' | 'image' | 'files' | 'json' | 'html' | 'custom'
  data: string | object
  mimeType?: string
  context?: {
    sourcePluginId: string
    sourceFeatureId?: string
    originalQuery?: TuffQuery
    metadata?: Record<string, any>
  }
}

Flow target

EXAMPLE.TYPESCRIPT
interface FlowTarget {
  id: string
  name: string
  description?: string
  supportedTypes: ('text' | 'image' | 'files' | 'json' | 'html' | 'custom')[]
  icon?: string
  featureId?: string
}

Shortcuts

ShortcutActionNotes
Command/Ctrl+DDetach to DivisionBoxDetach selected item
Command/Ctrl+Shift+DFlow TransferOpen target picker

Plugin Configuration

Declare Flow capabilities in manifest.json:

  • flowSender?: boolean
  • flowTargets?: FlowTarget[]
EXAMPLE.JSON
{
  "name": "my-plugin",
  "version": "1.0.0",
  "flowSender": true,
  "flowTargets": [
    {
      "id": "quick-note",
      "name": "Quick Note",
      "description": "Save content as a note",
      "supportedTypes": ["text", "html", "image"],
      "icon": "ri:sticky-note-line",
      "featureId": "create-note"
    }
  ]
}

SDK Usage

Send flow (sender)

EXAMPLE.TYPESCRIPT
import { createFlowSDK } from '@talex-touch/utils/plugin/sdk'

const flow = createFlowSDK(channel, 'my-plugin-id')

const result = await flow.dispatch(
  {
    type: 'text',
    data: 'Hello from my plugin!',
    context: {
      sourcePluginId: 'my-plugin-id',
      metadata: { timestamp: Date.now() }
    }
  },
  {
    title: 'Share text',
    description: 'Send to another plugin'
  }
)

Get available targets

EXAMPLE.TYPESCRIPT
const allTargets = await flow.getAvailableTargets()
const textTargets = await flow.getAvailableTargets('text')

Receive flow (target)

EXAMPLE.TYPESCRIPT
import { createFlowSDK } from '@talex-touch/utils/plugin/sdk'

const flow = createFlowSDK(channel, 'my-plugin-id')

const unsubscribe = flow.onFlowTransfer(async (payload, sessionId, sender) => {
  console.log(`Received ${payload.type} from ${sender.senderName}`)

  try {
    const result = await handlePayload(payload)
    await flow.acknowledge(sessionId, { success: true, result })
  } catch (error) {
    await flow.reportError(sessionId, 'Failed to handle payload')
  }
})

// Unregister when the component unmounts
onUnmounted(() => {
  unsubscribe()
})

Flow Session State

A session moves through these states; flow:session:update broadcasts every transition.

EXAMPLE.TYPESCRIPT
type FlowSessionState =
  | 'INIT'             // created, no target chosen yet
  | 'TARGET_SELECTING' // the chooser is open
  | 'TARGET_SELECTED'  // a target has been picked
  | 'DELIVERING'       // payload is being handed to the target
  | 'DELIVERED'        // the target received it
  | 'PROCESSING'       // the target is working on it
  | 'ACKED'            // the target acknowledged completion
  | 'FAILED'           // terminal failure
  | 'CANCELLED'        // cancelled by the sender or the user

Error Handling

flow.reportError(sessionId, message) and failed dispatches carry a FlowErrorCode. Treat any unrecognised value as INTERNAL_ERROR rather than assuming the list is closed.

EXAMPLE.TYPESCRIPT
enum FlowErrorCode {
  SENDER_NOT_ALLOWED = 'SENDER_NOT_ALLOWED',
  TARGET_NOT_FOUND = 'TARGET_NOT_FOUND',
  TARGET_OFFLINE = 'TARGET_OFFLINE',
  PAYLOAD_INVALID = 'PAYLOAD_INVALID',
  PAYLOAD_TOO_LARGE = 'PAYLOAD_TOO_LARGE',
  TYPE_NOT_SUPPORTED = 'TYPE_NOT_SUPPORTED',
  PERMISSION_DENIED = 'PERMISSION_DENIED',
  TIMEOUT = 'TIMEOUT',
  CANCELLED = 'CANCELLED',
  INTERNAL_ERROR = 'INTERNAL_ERROR'
}

Native System Share

Flow Transfer integrates the system's native share capabilities, so data can be shared to system apps such as AirDrop, Mail, and Messages. If your plugin is sharing the current CoreBox item from a MetaK / Quick Actions action, prefer QuickActions SDK shareItem() so target resolution and platform fallback stay centralized.

Using native share

EXAMPLE.TYPESCRIPT
const flow = createFlowSDK(channel, 'my-plugin-id')

// Share via the system
const result = await flow.nativeShare({
  type: 'text',
  data: 'Hello World!'
})

// Specify a share target
const result = await flow.nativeShare(
  { type: 'text', data: 'Hello!' },
  'airdrop'  // Optional: 'system' | 'airdrop' | 'mail' | 'messages'
)

if (result.success) {
  console.log('Share succeeded:', result.target)
} else {
  console.error('Share failed:', result.error)
}

Supported native targets

PlatformTargetNotes
macOSsystem / system-shareNative share chooser
macOSairdropAirDrop
macOSmailMail
macOSmessagesiMessage
WindowsmailDefault mail client
LinuxmailDefault mail client

Flow target lists expose the system chooser as system-share; flow.nativeShare() also accepts system as a compatibility alias. Windows and Linux currently expose only the explicit mail fallback, not a fake system share panel.

Target Ordering Rules

The target list is ordered by:

  1. Native share targets — the system chooser and its siblings come first.
  2. Adapted plugins — those that registered an onFlowTransfer handler.
  3. Unadapted plugins — shown with an adaptation hint rather than hidden, so a user can tell the difference between "cannot receive this" and "not installed".
EXAMPLE.TYPESCRIPT
// The adaptation fields on FlowTargetInfo
interface FlowTargetInfo {
  // ...other fields
  hasFlowHandler: boolean    // an onFlowTransfer handler is registered
  isNativeShare?: boolean    // this is a native share target
  adaptationHint?: string    // shown when the plugin has not adapted yet
}

IPC Channels

Event names are composed as namespace:module:action, so every Flow channel carries a module segment. These are the names the main process actually registers — see apps/core-app/src/main/modules/flow-bus/.

ChannelDirectionPurpose
flow:bus:dispatchplugin → mainStart a flow
flow:bus:get-targetsplugin → mainList available targets
flow:bus:cancelplugin → mainCancel a session
flow:bus:acknowledgetarget → mainAcknowledge completion
flow:bus:report-errortarget → mainReport a FlowErrorCode
flow:bus:select-targetUI → mainUser picked a target in the chooser
flow:session:updatemain → allBroadcast a session state transition
flow:session:delivermain → targetHand the payload to the target
flow:native:shareplugin → mainInvoke a native share target
flow:consent:checkplugin → mainCheck transfer consent
flow:consent:grantUI → mainGrant transfer consent
flow:ui:trigger-transfermain → UIOpen the transfer surface
flow:ui:trigger-detachmain → UIDetach the transfer surface

Plugin registration, from the plugin process:

ChannelPurpose
flow:plugin:register-targetsRegister this plugin's targets
flow:plugin:unregister-targetsRemove them
flow:plugin:set-plugin-enabledUpdate the plugin's enabled state
flow:plugin:set-plugin-handlerDeclare whether onFlowTransfer is registered

Prefer the FlowEvents constants over these literals — the SDK builds the name from the same builder, so a rename stays in one place.

Best Practices

  • Register onFlowTransfer to avoid being marked “not supported”.
  • Provide clear supportedTypes and description for better target ranking.
  • Use requireAck for critical workflows and handle fallback actions.
  • Use QuickActions shareItem() for MetaK item sharing.

Technical Notes

  • Target list is maintained by the main process and merged with native share targets.
  • Plugins are registered via flow:plugin:register-targets; missing registration means targets won’t appear.