Flow Transfer API
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_DENIEDand trigger consent UI.
Core Concepts
Flow payload
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
interface FlowTarget {
id: string
name: string
description?: string
supportedTypes: ('text' | 'image' | 'files' | 'json' | 'html' | 'custom')[]
icon?: string
featureId?: string
}
Shortcuts
| Shortcut | Action | Notes |
|---|---|---|
Command/Ctrl+D | Detach to DivisionBox | Detach selected item |
Command/Ctrl+Shift+D | Flow Transfer | Open target picker |
Plugin Configuration
Declare Flow capabilities in manifest.json:
flowSender?: booleanflowTargets?: FlowTarget[]
{
"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)
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
const allTargets = await flow.getAvailableTargets()
const textTargets = await flow.getAvailableTargets('text')
Receive flow (target)
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.
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.
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
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
| Platform | Target | Notes |
|---|---|---|
| macOS | system / system-share | Native share chooser |
| macOS | airdrop | AirDrop |
| macOS | mail | |
| macOS | messages | iMessage |
| Windows | mail | Default mail client |
| Linux | mail | Default 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:
- Native share targets — the system chooser and its siblings come first.
- Adapted plugins — those that registered an
onFlowTransferhandler. - Unadapted plugins — shown with an adaptation hint rather than hidden, so a user can tell the difference between "cannot receive this" and "not installed".
// 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/.
| Channel | Direction | Purpose |
|---|---|---|
flow:bus:dispatch | plugin → main | Start a flow |
flow:bus:get-targets | plugin → main | List available targets |
flow:bus:cancel | plugin → main | Cancel a session |
flow:bus:acknowledge | target → main | Acknowledge completion |
flow:bus:report-error | target → main | Report a FlowErrorCode |
flow:bus:select-target | UI → main | User picked a target in the chooser |
flow:session:update | main → all | Broadcast a session state transition |
flow:session:deliver | main → target | Hand the payload to the target |
flow:native:share | plugin → main | Invoke a native share target |
flow:consent:check | plugin → main | Check transfer consent |
flow:consent:grant | UI → main | Grant transfer consent |
flow:ui:trigger-transfer | main → UI | Open the transfer surface |
flow:ui:trigger-detach | main → UI | Detach the transfer surface |
Plugin registration, from the plugin process:
| Channel | Purpose |
|---|---|
flow:plugin:register-targets | Register this plugin's targets |
flow:plugin:unregister-targets | Remove them |
flow:plugin:set-plugin-enabled | Update the plugin's enabled state |
flow:plugin:set-plugin-handler | Declare 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
onFlowTransferto avoid being marked “not supported”. - Provide clear
supportedTypesanddescriptionfor better target ranking. - Use
requireAckfor 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.