Files
orca/src/preload/api/ui-command-event-api.ts
T
Neil 4efc86a33c feat(app): open Markdown files from the OS in the floating workspace (#17906)
* feat(app): open Markdown files from the OS in the floating workspace

Registers Orca as a Markdown handler on macOS, Windows and Linux, and opens
an OS-handed .md/.markdown/.mdx file as a floating-workspace editor tab —
the one editor surface that needs no project. Works cold-start and when
Orca is already running.

Main buffers the paths and both pushes to a live renderer and answers a
pull on renderer mount, mirroring SkillShareDeepLinkState. The buffer is
only released once delivery is possible: the renderer's pull is what proves
its ui:openMarkdownFiles listener is attached, because a push into a window
whose renderer has not subscribed is dropped by Electron with no error. Both
the push and the pull restore an undelivered batch, and a renderer reload
clears the latch so the fresh renderer re-proves itself. Paths are stat'd and
proven to be files before authorizeExternalPath sees them.

Windows association is registered by hand in the NSIS include rather than
through electron-builder's `fileAssociations`: app-builder-lib emits
APP_ASSOCIATE, whose first line overwrites Software\Classes\.md's default
value with no backup — silently taking .md from whichever editor owns it,
for every existing user on their next update — and APP_UNASSOCIATE never
restores it. The hand-rolled registration is additive (ProgID +
OpenWithProgids + SupportedTypes) and leaves the user's default alone;
verified end to end on a real Windows 11 host.

Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com>
Co-authored-by: Jaydev <java-jaydev@users.noreply.github.com>

Closes #10138

* fix(os-open): register the new listener in the IPC inventory, and guard a non-array payload

CI caught two things the local run did not.

useIpcEvents-lifecycle.test.ts is an inventory of every App-lifetime IPC
listener and the exact order they register in; ui.onOpenMarkdownFiles now
appears there, positioned after the workspace-shortcut bridge's last
listener, which is where it actually registers.

Chasing that failure surfaced a real gap: the pending-open payload crosses
the preload boundary, so a stale or mismatched preload can resolve with
something that is not an array, and reading .length off it threw inside the
promise chain instead of failing at the boundary. Array.isArray now gates it,
with a regression test.
2026-09-01 17:35:38 -07:00

235 lines
10 KiB
TypeScript

import type { MarkdownDocument } from '../../shared/filesystem-entry-types'
import type { PersistedUIState } from '../../shared/persisted-ui-state-types'
import type { TuiAgent } from '../../shared/tui-agent'
import type {
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch,
WorktreeStartupLaunch
} from '../../shared/worktree/launch-types'
import type { FeatureInteractionId } from '../../shared/feature-interactions'
import type { KeybindingActionId } from '../../shared/keybindings'
import type { BrowserFindSource } from '../../shared/browser-find-source'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../shared/agent-session-resume'
import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry'
import type {
RuntimeMobileSessionTabMove,
RuntimeTerminalCreateRequestPayload,
RuntimeTerminalPresentation
} from '../../shared/runtime-types'
import type {
RuntimeMobileMarkdownRequest,
RuntimeMobileMarkdownResponse
} from '../../shared/mobile-markdown-document'
import type { TerminalTabCreateReply } from '../../shared/terminal-reveal-identity'
import type {
TerminalTabCloseRequest,
TerminalTabCloseResponse
} from '../../shared/terminal-tab-close'
import type {
SessionTabCloseRequest,
SessionTabCloseResponse
} from '../../shared/session-tab-close'
export type CloseActiveTabPayload = { sourceId: string }
export type UiCommandEventApi = {
get: () => Promise<PersistedUIState>
set: (args: Partial<PersistedUIState>) => Promise<void>
/** Like set, but REJECTS when the update did not reach the host (the web preload's set
* swallows transport failures for offline use). The diff writer needs the distinction:
* folding an unacked patch into its baseline would silently stop retrying it (STA-5781). */
setWithAck?: (args: Partial<PersistedUIState>) => Promise<void>
recordFeatureInteraction: (id: FeatureInteractionId) => Promise<PersistedUIState>
onStateChanged: (callback: (ui: PersistedUIState) => void) => () => void
onOpenSettings: (callback: () => void) => () => void
/** Consumes a one-shot tray/menu-bar "open settings" intent queued before mount. */
consumePendingOpenSettings: () => Promise<boolean>
onOpenSkillShare: (callback: (shareId: string) => void) => () => void
consumePendingSkillShare: () => Promise<string | null>
/** OS "Open With" markdown paths pushed while a renderer is already listening. */
onOpenMarkdownFiles: (callback: (documents: MarkdownDocument[]) => void) => () => void
/** Drains the "Open With" paths queued before this renderer's listener attached. */
consumePendingMarkdownFileOpens: () => Promise<MarkdownDocument[]>
onOpenSetupGuide: (callback: () => void) => () => void
onOpenFeatureTour: (callback: () => void) => () => void
onOpenCrashReport: (callback: () => void) => () => void
onToggleLeftSidebar: (callback: () => void) => () => void
onToggleRightSidebar: (callback: () => void) => () => void
onToggleWorktreePalette: (callback: () => void) => () => void
onToggleFloatingTerminal: (callback: () => void) => () => void
onTerminalShortcutCaptured: (
callback: (data: { actionId: KeybindingActionId }) => void
) => () => void
onOpenQuickOpen: (callback: () => void) => () => void
onToggleQuickCommandsMenu: (callback: () => void) => () => void
onOpenNewWorkspace: (callback: () => void) => () => void
onDeleteCurrentWorkspace: (callback: () => void) => () => void
onOpenWorkspaceBoard: (callback: () => void) => () => void
onOpenTasks: (callback: () => void) => () => void
onToggleAgentDashboard: (callback: () => void) => () => void
onJumpToWorktreeIndex: (callback: (index: number) => void) => () => void
onJumpToTabIndex: (callback: (index: number) => void) => () => void
onWorktreeHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void
onNewBrowserTab: (callback: () => void) => () => void
onNewMarkdownTab: (callback: () => void) => () => void
onNewSimulatorTab: (callback: () => void) => () => void
onRequestTabCreate: (
callback: (data: {
requestId: string
url: string
worktreeId?: string
browserPageId?: string
sessionProfileId?: string | null
sessionPartition?: string
activate?: boolean
}) => void
) => () => void
replyTabCreate: (reply: { requestId: string; browserPageId?: string; error?: string }) => void
onRequestTabSetProfile: (
callback: (data: {
requestId: string
browserPageId: string
profileId: string
sessionPartition?: string
}) => void
) => () => void
replyTabSetProfile: (reply: { requestId: string; error?: string }) => void
onRequestTabClose: (
callback: (data: { requestId: string; tabId: string | null; worktreeId?: string }) => void
) => () => void
replyTabClose: (reply: {
requestId: string
error?: string
code?: 'browser_tab_not_found'
}) => void
onNewTerminalTab: (callback: () => void) => () => void
onFocusBrowserAddressBar: (callback: () => void) => () => void
onFindInBrowserPage: (source: BrowserFindSource, callback: () => void) => () => void
onReloadBrowserPage: (callback: () => void) => () => void
onBrowserHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void
onZoomBrowserPage: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void
onScrollBrowserPage?: (
callback: (event: { browserPageId: string; deltaX: number; deltaY: number }) => void
) => () => void
onHardReloadBrowserPage: (callback: () => void) => () => void
onCloseActiveTab: (callback: (payload?: CloseActiveTabPayload) => void) => () => void
onCloseFloatingItem: (callback: (payload: { sourceId: string }) => void) => () => void
onSelectFloatingIndex: (callback: (payload: { index: number }) => void) => () => void
onSwitchTab: (callback: (direction: 1 | -1) => void) => () => void
onSwitchTabAcrossAllTypes: (callback: (direction: 1 | -1) => void) => () => void
onSwitchRecentTab: (callback: () => void) => () => void
onSwitchTerminalTab: (callback: (direction: 1 | -1) => void) => () => void
onCtrlTabKeyDown: (callback: (data: { shiftKey: boolean }) => void) => () => void
onCtrlTabKeyUp: (callback: () => void) => () => void
onToggleStatusBar: (callback: () => void) => () => void
onDictationKeyDown: (callback: () => void) => () => void
onExportPdfRequested: (callback: () => void) => () => void
onAppMenuPaste: (callback: () => void) => () => void
onAppMenuSelectionAction: (callback: (action: 'copy' | 'select-all') => void) => () => void
onEditableContextPaste: (callback: (data: { plainTextOnly: boolean }) => void) => () => void
onActivateWorktree: (
callback: (data: {
repoId: string
worktreeId: string
setup?: WorktreeSetupLaunch
startup?: WorktreeStartupLaunch
defaultTabs?: WorktreeDefaultTabsLaunch
}) => void
) => () => void
onCreateTerminal: (
callback: (data: {
requestId?: string
worktreeId: string
command?: string
cwd?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: false
tabId?: string
leafId?: string
splitFromLeafId?: string
splitDirection?: 'horizontal' | 'vertical'
splitTelemetrySource?: TerminalPaneSplitSource
}) => void
) => () => void
onRequestTerminalCreate: (
callback: (data: RuntimeTerminalCreateRequestPayload) => void
) => () => void
onRequestTerminalTabMount: (
callback: (data: { worktreeId: string; tabId?: string; ptyId?: string }) => void
) => () => void
replyTerminalCreate: (reply: TerminalTabCreateReply) => void
onSplitTerminal: (
callback: (data: {
tabId: string
paneRuntimeId: number
direction: 'horizontal' | 'vertical'
command?: string
worktreeId?: string
sourceLeafId?: string
telemetrySource?: TerminalPaneSplitSource
newLeafId?: string
}) => void
) => () => void
onRenameTerminal: (
callback: (data: { tabId: string; title: string | null }) => void
) => () => void
onFocusTerminal: (
callback: (data: {
tabId: string
worktreeId: string
leafId?: string | null
ackPaneKeyOnSuccess?: string
flashFocusedPane?: boolean
scrollToBottomIfOutputSinceLastView?: boolean
}) => void
) => () => void
onFocusEditorTab: (callback: (data: { tabId: string; worktreeId: string }) => void) => () => void
onCloseSessionTab: (callback: (data: { tabId: string; worktreeId: string }) => void) => () => void
onSessionTabCloseRequest: (callback: (request: SessionTabCloseRequest) => void) => () => void
respondSessionTabClose: (response: SessionTabCloseResponse) => void
onMoveSessionTab: (
callback: (data: { worktreeId: string } & RuntimeMobileSessionTabMove) => void
) => () => void
onOpenFileFromMobile: (
callback: (data: {
worktreeId: string
filePath: string
relativePath: string
runtimeEnvironmentId?: string
}) => void
) => () => void
onOpenDiffFromMobile: (
callback: (data: {
worktreeId: string
filePath: string
relativePath: string
staged: boolean
runtimeEnvironmentId?: string
}) => void
) => () => void
onMobileMarkdownRequest: (callback: (request: RuntimeMobileMarkdownRequest) => void) => () => void
respondMobileMarkdownRequest: (response: RuntimeMobileMarkdownResponse) => void
onCloseTerminal: (
callback: (data: { tabId: string; paneRuntimeId?: number }) => void
) => () => void
onTerminalTabCloseRequest: (callback: (request: TerminalTabCloseRequest) => void) => () => void
respondTerminalTabClose: (response: TerminalTabCloseResponse) => void
onSleepWorktree: (callback: (data: { worktreeId: string }) => void) => () => void
onResumeSleepingAgents: (callback: (data: { worktreeId: string }) => void) => () => void
onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void
onSystemResumed: (callback: () => void) => () => void
}