zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Frame Pop-Out

The frame pop-out subsystem (W9.1, #1442) lets users detach any editor frame into its own native window and dock it back. Three Tauri commands handle the native-window operations; the bridge.framePopout facade adds a typed event layer including the cross-window restore protocol (#1920) used to move live provider state back into the main window.

Source: tauri-app/src/commands/window.rs

Tauri Commands

frame_pop_out

Open a new native window for the given frame.

const windowLabel = await invoke<string>('frame_pop_out', {
  frameId: 'frame-abc123',
  providerId: 'inbox',
  propsJson: JSON.stringify({ draftNum: 1 }),
});

Parameters:

NameTypeDescription
frame_idstringStable identifier of the frame being popped out
provider_idstringProvider type identifier (e.g. "inbox", "search")
props_jsonstringJSON-serialized initial provider props (max 32 KB)

Returns: The Tauri window label (popout-{frameId}) on success. Throws a string error on failure or if props_json exceeds 32 KB.

Behavior:

  • Opens a new WebviewWindow with label popout-{frameId}, loading index.html#/popped-out/{frameId}?providerId={providerId}&props={encoded}.

  • Idempotent: if a window with the same label already exists, focuses it and returns its label without opening a second window.

frame_dock

Close a popped-out window programmatically.

await invoke('frame_dock', { windowLabel: 'popout-frame-abc123' });

Parameters:

NameTypeDescription
window_labelstringThe Tauri window label returned by frame_pop_out

Returns: null on success. Throws a string error if the window is not found.

Behavior: Closes the named window. The Rust window-destroy handler emits frame:popout-closed so the main window can update its frame state (dock the leaf back to normal).

frame_popout_emit_event

Relay a named Tauri event between the main window and a popped-out window.

await invoke('frame_popout_emit_event', {
  eventName: 'frame:popout-restore-request',
  payload: { windowLabel: 'popout-frame-abc123' },
});

Parameters:

NameTypeDescription
event_namestringEvent name — must be one of the allowed events (see below)
payloadanyJSON-serializable event payload

Returns: null on success. Throws a string error if event_name is not in the allowlist.

Allowed event names (allowlist enforced by Rust):

Event nameDirectionPurpose
frame:popout-restore-requestmain → popoutAsk popout to send its current state
frame:popout-restore-payloadpopout → mainPopout's serialized state
frame:popout-restore-ackmain → popoutMain has restored state; popout may close
frame:popout-return-requestpopout → mainUser clicked "return to frame" inside popout
frame:popout-notifypopout → mainForward a serialized toast spec to the main window notification stack
frame:popout-sync-state-requestpopout → mainAsk main for a fresh sync-state snapshot
frame:popout-sync-statemain → popoutBroadcast the serializable sync-state snapshot
frame:popout-auth-login-requestpopout → mainRun the interactive auth login flow in the main window

The allowlist prevents the relay from being used as a general-purpose cross-window event bus.

Bridge Facade (bridge.framePopout)

The bridge facade wraps the Tauri commands and events with typed helper methods. The cross-window restore protocol is coordinated via frame_popout_emit_event relays — window.postMessage does not cross separate Tauri WebviewWindows.

Window Lifecycle

// Open a popped-out window
const windowLabel = await bridge.framePopout.popOut(frameId, providerId, propsJson);

// Programmatically close it
await bridge.framePopout.dock(windowLabel);

// Listen for window-closed events (emitted by Rust destroy handler)
const unlisten = bridge.framePopout.onPopoutClosed((frameId, windowLabel) => {
  // Dock the leaf back to normal state in the main window
});

Cross-Window Restore Protocol

Main-initiated restore (user clicks empty-frame "Restore popped window" CTA):

// 1. Main requests the popout's current state
await bridge.framePopout.requestRestore(windowLabel);

// 2. Popout receives the request (registered in the popout renderer)
const unlisten = bridge.framePopout.onRestoreRequest((windowLabel) => {
  // Serialize current provider state and send it back
  bridge.framePopout.sendRestorePayload(windowLabel, JSON.stringify(currentProps));
});

// 3. Main receives the payload and restores into an empty leaf
const unlisten = bridge.framePopout.onRestorePayload((windowLabel, propsJson) => {
  const props = JSON.parse(propsJson);
  targetLeaf.replaceProvider(providerId, props);
  // Acknowledge so popout knows it can close
  bridge.framePopout.sendRestoreAck(windowLabel);
});

// 4. Popout receives the ack and closes itself
const unlisten = bridge.framePopout.onRestoreAck((windowLabel) => {
  window.close();
});

Popout-initiated return (user clicks "return to frame" inside the popout):

// 1. Popout sends a return request with current state
await bridge.framePopout.sendReturnRequest(windowLabel, JSON.stringify(currentProps));

// 2. Main receives it — decides where to restore (empty leaf, or prompts user)
const unlisten = bridge.framePopout.onReturnRequest((windowLabel, propsJson) => {
  // Find an empty leaf, or show a "open an empty frame first" toast
  // Then call sendRestoreAck to close the popout (same as main-initiated path)
});

Notify, Sync, and Auth Relays

Popped-out windows do not own the main app's notification stack, sync state, or auth login flow. The bridge.framePopout facade therefore exposes typed relay helpers on top of the same frame_popout_emit_event allowlist:

HelperEventDirectionNotes
sendNotify(windowLabel, specJson) / onNotifyForwarded(cb)frame:popout-notifypopout → mainspecJson is the serializable NotificationSpec subset; React icon and actions are stripped before serialization.
requestSyncState(windowLabel) / onSyncStateRequest(cb)frame:popout-sync-state-requestpopout → mainPopouts request a snapshot on mount and retry until one lands.
sendSyncState(snapshotJson) / onSyncState(cb)frame:popout-sync-statemain → popoutSync state is global, so this is a label-less broadcast.
requestAuthLogin(windowLabel) / onAuthLoginRequest(cb)frame:popout-auth-login-requestpopout → mainThe Better Auth browser handoff and deep-link callback stay in the main window process.

Events

frame:popout-closed

Emitted by the Rust window-destroy handler when a popped-out window is closed (by the user or by frame_dock).

import { listen } from '@tauri-apps/api/event';

const unlisten = await listen<{ frameId: string; windowLabel: string }>(
  'frame:popout-closed',
  (event) => {
    const { frameId, windowLabel } = event.payload;
    // Dock the leaf back to normal
  }
);

The restore-protocol events (frame:popout-restore-request, frame:popout-restore-payload, frame:popout-restore-ack, frame:popout-return-request) and the notify/sync/auth relay events listed above are relayed via frame_popout_emit_event — use bridge.framePopout helpers rather than listen() directly.

macOS Handoff Note

The MockAdapter jsdom tests cover the in-process relay path and the Rust core tests cover the event allowlist, but real two-window delivery depends on the Tauri runtime and WebviewWindow event broadcast behavior. Before shipping the popout notify relay, verify on a macOS Tauri build:

  1. Open a kanban or todo board in a popped-out frame.

  2. Trigger an in-app toast in the popped-out frame and confirm the main window shows it.

Help window

Help uses the fixed native label help, outside the popout-* namespace and frame restore protocol. BackendAPI.helpWindow exposes isSupported(), open(entryId?, shortcuts?), close(), isOpen(), and asynchronous onClosed(callback) (resolves to an unsubscribe function). Shortcuts are a map of command keys to formatted display strings, serialized as JSON by the adapter.

On desktop, open_help_window creates an 1100 × 760 window titled <app> — Help at index.html#/help?entryId=…&shortcuts=. Query values are URL-encoded. Reopening focuses and unminimizes the existing window and returns { disposition: "reused" }; creation returns { disposition: "created" }. Reusing a window preserves its current catalog navigation. close_help_window is idempotent, is_help_window_open probes the fixed label, and native destruction emits help:window-closed.

The renderer declares secondary scope help before loading appearance. It mounts only HelpWindowPage: no auth resumption, workspace boot gate, sync arming, or host outbox. The external button carries the currently selected catalog entry and effective shortcut labels. Closing the native window clears external state; the next Help request opens the modal. Subscription precedes liveness queries, and close events invalidate stale query/open results.

“Try it now” focuses the main window with focus_main_window, then emits help:invoke-command with only commandId. The host resolves that identifier against its current command list. Executable callbacks never cross the boundary.

The button is hidden on mobile and unsupported backends. REST advertises isSupported() === false. The mock adapter also advertises no native-window support, keeping Help visible as a modal in dev:mock; its in-process open/reuse/close/subscription lifecycle remains available for deterministic tests.

Native verification must cover creating/reusing Help, retaining the selected entry and shortcut display, invoking a command in the main window, closing the window, and reopening the modal. Unit tests cover the bridge shapes, isolated bootstrap, route parsing, and close-to-modal restoration.

The child subscribes to help:available-commands before emitting help:commands-request. The host responds with its current executable command IDs and republishes when that list changes, so catalog actions remain disabled until the main window confirms availability.

The isolated native-e2e fixture composes production Help transport with mock workspace data and a dedicated incognito WebView store. e2e/native/help-window.e2e.ts checks actual native creation/reuse and probes main focus from the Help realm before switching WebDriver context. This is separate from ordinary browser mock coverage and does not use a personal account.