mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
* 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.
235 lines
10 KiB
TypeScript
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
|
|
}
|