mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(runtime): a phone reads and saves Markdown tabs on a headless server With no window to relay through, markdown.readTab and markdown.saveTab answered renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and saves with a version compare plus the disk write precondition, in the shapes the window's editor relay returns. With a window attached the relay is unchanged. * Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear * Use the disk baseline only for a draft * ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job * Headless Markdown: answer from disk only; a window's saved draft stays its view state Clearing a saved draft wrote workspace layout from main, which no layout command carries. Main now never shows or edits a draft; the desktop's restore check reports a disk change. * Phone Markdown: one path through main and the disk, window or not A paired server's workspace is read and saved on that server with files.read / files.writeIfUnchanged, so it answers from its own disk and keeps the precondition. The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed. * e2e: a phone saves a Markdown tab the desktop holds unsaved edits in The phone reads the disk with the tab marked dirty; its save lands on disk, and the desktop keeps its draft and shows the changed-on-disk banner. * Group the Markdown disk host's imports * Drop an unneeded store cast from the dirty-readers test * Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab A server before #30605 has no files.writeIfUnchanged, so the save is refused with the existing update-required message rather than written unchecked. * Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer * Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads - The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers it read-only with unsupported_untitled and refuses its save before touching the disk. - A server without the files.write.disk-precondition capability gets the desktop editor's guarded save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned to the pairing it probed. - A preview whose source tab was closed has no saved record; it reads its own file, read-only. * e2e: read the untitled tab's answer without a cast * Phone Markdown: pin a save's pairing before its first read A save captures the server's pairing before it reads, and every call of the save, the read included, is pinned to it, so a re-pair between the read and the write is refused. * Test: with no window, a phone saves an existing untitled.md and never creates a deleted one --------- Co-authored-by: E2E Test <e2e@test.local>
188 lines
7.6 KiB
TypeScript
188 lines
7.6 KiB
TypeScript
import type {
|
|
RuntimeEditorTabReveal,
|
|
RuntimeFileOpenPosition
|
|
} from '../../shared/runtime-file-contracts'
|
|
import type { AgentLaunchPaneVerdictEvent } from '../../shared/agent-launch-pane-verdict'
|
|
import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
|
|
import type {
|
|
AgentLaunchTabPublished,
|
|
AgentLaunchTabViewerRule,
|
|
AgentLaunchTabPublishRequest
|
|
} from '../../shared/agent-launch-tab-publication'
|
|
import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry'
|
|
import type { TerminalRevealIdentity } from '../../shared/terminal-reveal-identity'
|
|
import type { TerminalPaneRevealTarget } from '../../shared/terminal-pane-reveal'
|
|
import type { TuiAgent } from '../../shared/tui-agent'
|
|
import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation'
|
|
import type { ClientHostedBrowserRowsEvent } from '../../shared/client-hosted-browser-rows'
|
|
import type {
|
|
WorktreeBaseStatusEvent,
|
|
WorktreeRemoteBranchConflictEvent
|
|
} from '../../shared/worktree/base-ref-drift-types'
|
|
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
|
|
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
|
|
import type {
|
|
RuntimeBrowserDriverState,
|
|
RuntimeMobileSessionTabMove,
|
|
RuntimeTerminalDriverState,
|
|
RuntimeTerminalPresentation
|
|
} from '../../shared/runtime-types'
|
|
|
|
type DriverState = RuntimeTerminalDriverState
|
|
|
|
export type RuntimeNotifier = {
|
|
automationsChanged?(payload: {
|
|
selector?: { kind: 'self' } | { kind: 'ssh'; targetId: string } | { kind: 'orphan' }
|
|
reason?: 'definition' | 'run' | 'usage'
|
|
}): void
|
|
worktreesChanged(
|
|
repoId: string,
|
|
renamed?: { oldWorktreeId: string; newWorktreeId: string },
|
|
// Why: false for metadata-only changes (sort order) that leave Git scan results valid.
|
|
options?: { invalidateGitScans?: boolean }
|
|
): void
|
|
worktreeBaseStatus?(event: WorktreeBaseStatusEvent): void
|
|
worktreeRemoteBranchConflict?(event: WorktreeRemoteBranchConflictEvent): void
|
|
reposChanged(): void
|
|
activateWorktree(
|
|
repoId: string,
|
|
worktreeId: string,
|
|
setup?: CreateWorktreeResult['setup'],
|
|
startup?: WorktreeStartupLaunch,
|
|
defaultTabs?: CreateWorktreeResult['defaultTabs'],
|
|
providesInitialSurface?: boolean
|
|
): void
|
|
createTerminal(
|
|
worktreeId: string,
|
|
opts: {
|
|
command?: string
|
|
cwd?: string
|
|
env?: Record<string, string>
|
|
title?: string
|
|
presentation?: RuntimeTerminalPresentation
|
|
}
|
|
): void
|
|
revealTerminalSession?(
|
|
worktreeId: string,
|
|
opts: {
|
|
ptyId: string
|
|
title?: string | null
|
|
cwd?: string
|
|
launchConfig?: SleepingAgentLaunchConfig
|
|
launchToken?: string
|
|
launchAgent?: TuiAgent
|
|
viewMode?: 'terminal' | 'chat'
|
|
targetGroupId?: string
|
|
afterTabId?: string
|
|
shellOverride?: string
|
|
activate?: boolean
|
|
presentation?: RuntimeTerminalPresentation
|
|
surfaceOwner?: false
|
|
agentLaunchViewer?: AgentLaunchTabViewerRule
|
|
tabId?: string
|
|
leafId?: string
|
|
splitFromLeafId?: string
|
|
splitDirection?: 'horizontal' | 'vertical'
|
|
splitTelemetrySource?: TerminalPaneSplitSource
|
|
focus?: boolean
|
|
expectedProcessIdentity?: {
|
|
terminalHandle: string
|
|
incarnationId: string
|
|
}
|
|
}
|
|
):
|
|
| Promise<{ tabId: string; title?: string | null; identity?: TerminalRevealIdentity }>
|
|
| { tabId: string; title?: string | null; identity?: TerminalRevealIdentity }
|
|
| void
|
|
/** Shows an agent launch's tab before its process exists; the pane attaches when it does. */
|
|
publishAgentLaunchTab?(
|
|
request: Omit<AgentLaunchTabPublishRequest, 'requestId'>
|
|
): Promise<AgentLaunchTabPublished>
|
|
/** A launch pane's fate, for the window to keep on its tab or act on. */
|
|
agentLaunchPaneVerdict?(event: AgentLaunchPaneVerdictEvent): void
|
|
resolveLegacyWorkerTerminalRecovery?(
|
|
paneKey: string,
|
|
resolution: 'adopted' | 'exited' | 'rolled_back',
|
|
ptyId?: string
|
|
): void
|
|
/** The fence lives in the workspace session, which a live renderer only re-reads at startup. */
|
|
splitTerminal(
|
|
tabId: string,
|
|
paneRuntimeId: number,
|
|
opts: {
|
|
direction: 'horizontal' | 'vertical'
|
|
command?: string
|
|
worktreeId?: string
|
|
sourceLeafId?: string
|
|
telemetrySource?: TerminalPaneSplitSource
|
|
newLeafId?: string
|
|
}
|
|
): void
|
|
/** `recordInteraction: false` marks a title the host applied, not one the user typed. */
|
|
renameTerminal(tabId: string, title: string | null, options?: { recordInteraction: false }): void
|
|
/** False when the window could not be reached; absent on notifiers that predate pane titles. */
|
|
setPaneTitle?(tabId: string, leafId: string, title: string | null): boolean
|
|
focusTerminal(tabId: string, worktreeId: string, leafId?: string | null): void
|
|
/** Raises the window onto a pane it shows; resolves false when the window does not know the worktree. */
|
|
revealTerminalPane?(target: TerminalPaneRevealTarget): Promise<boolean>
|
|
focusEditorTab?(tabId: string, worktreeId: string): void
|
|
closeSessionTab?(tabId: string, worktreeId: string): void | Promise<void>
|
|
moveSessionTab?(worktreeId: string, move: RuntimeMobileSessionTabMove): void
|
|
/**
|
|
* Acts only on the host's own window: 'host'/'all' move it, 'caller'/'clients' open without moving
|
|
* it, absent keeps the original switch. Paired clients are never navigated (intended; 'all' == 'host').
|
|
*/
|
|
openFile?(
|
|
worktreeId: string,
|
|
filePath: string,
|
|
relativePath: string,
|
|
runtimeEnvironmentId?: string | null,
|
|
navigation?: RuntimeNavigationTarget,
|
|
position?: RuntimeFileOpenPosition
|
|
): void
|
|
/** Layout-owner builds: shows a tab main already wrote for a file it opened. */
|
|
revealEditorTab?(reveal: RuntimeEditorTabReveal): void
|
|
openDiff?(
|
|
worktreeId: string,
|
|
filePath: string,
|
|
relativePath: string,
|
|
staged: boolean,
|
|
runtimeEnvironmentId?: string | null,
|
|
navigation?: RuntimeNavigationTarget
|
|
): void
|
|
/** Closes the whole tab. */
|
|
closeTerminal(tabId: string): void
|
|
/** Drops one split pane main already closed; never closes its tab. */
|
|
closeTerminalPane?(tabId: string, leafId: string): void
|
|
closeTerminalTab?(
|
|
tabId: string,
|
|
options?: { localPtyTeardownOwnedExternally?: boolean; force?: boolean }
|
|
): Promise<void>
|
|
sleepWorktree(worktreeId: string): void
|
|
// Why: a phone opening a worktree wakes its slept agents by asking the host
|
|
// renderer to run its own navigation-free wake (experimental agent sleep);
|
|
// the runtime has no in-memory sleeping records or wake authority. Optional to
|
|
// match the many renderer-backed notifier methods only the real bridge wires.
|
|
resumeSleepingAgents?(worktreeId: string): void
|
|
terminalFitOverrideChanged(
|
|
ptyId: string,
|
|
mode: 'mobile-fit' | 'remote-desktop-fit' | 'desktop-fit',
|
|
cols: number,
|
|
rows: number
|
|
): void
|
|
// Why: presence-based lock signal — desktop renderer mounts the lock
|
|
// banner when `driver.kind === 'mobile'` and unmounts otherwise. The
|
|
// structured payload (vs a `locked: boolean`) carries the active mobile
|
|
// actor's clientId so the renderer can disambiguate multi-phone scenarios
|
|
// and so a future write coordinator can use the same signal as scheduling
|
|
// input. See docs/mobile-presence-lock.md.
|
|
terminalDriverChanged(ptyId: string, driver: DriverState): void
|
|
nativeChatLaunchDraftResolved?(
|
|
tabId: string,
|
|
resolution: { text: string; createdAt: number }
|
|
): void
|
|
browserDriverChanged?(browserPageId: string, driver: RuntimeBrowserDriverState): void
|
|
browserRemoteViewersChanged?(browserPageId: string, hasRemoteViewers: boolean): void
|
|
clientHostedBrowserRowsChanged?(event: ClientHostedBrowserRowsEvent): void
|
|
}
|