mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
Merge remote-tracking branch 'origin/main' into nwparker/fix-terminal-ime-overlay
This commit is contained in:
@@ -113,7 +113,8 @@ describe('AutomationRunHistory unanswered history', () => {
|
||||
expect(container.textContent).not.toContain('No runs yet.')
|
||||
// "0 runs" is a count of something nobody managed to read.
|
||||
expect(container.textContent).not.toContain('0 runs')
|
||||
expect(container.textContent).toContain('Run history could not be loaded')
|
||||
expect(container.textContent).toContain('Run history is unavailable from this host')
|
||||
expect(container.textContent).toContain('does not mean the automation failed or has no runs')
|
||||
expect(container.textContent).toContain('web-01 is not connected')
|
||||
})
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ export function AutomationRunHistory({
|
||||
<p className="text-center text-sm text-foreground">
|
||||
{translate(
|
||||
'auto.components.automations.AutomationRunHistory.historyUnavailable',
|
||||
'Run history could not be loaded for this automation.'
|
||||
'Run history is unavailable from this host. This does not mean the automation failed or has no runs.'
|
||||
)}
|
||||
</p>
|
||||
<AutomationOwnerConflictNotice notice={notice} onRecover={onRecoverHistory} />
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AutomationsDetailPane } from './AutomationsDetailPane'
|
||||
import { makeAutomation } from './automations-page-fixtures'
|
||||
|
||||
let root: Root | null = null
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root?.unmount())
|
||||
root = null
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
async function renderRunsTab(historyUnavailable: boolean): Promise<HTMLButtonElement> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<AutomationsDetailPane
|
||||
selected={makeAutomation()}
|
||||
selectedExternal={null}
|
||||
selectedExternalRunPage={null}
|
||||
selectedAutomationRunPage={null}
|
||||
selectedRuns={[]}
|
||||
selectedRunsNotice={
|
||||
historyUnavailable
|
||||
? { message: 'Server update required', recovery: 'update-server', severity: 'failure' }
|
||||
: null
|
||||
}
|
||||
activePaneTab="runs"
|
||||
relativeNow={0}
|
||||
externalActionKey={null}
|
||||
selectedRepoDisplayName="Orca"
|
||||
selectedRepoDefaultBaseRef={null}
|
||||
selectedWorkspaceName="Workspace"
|
||||
selectedHostEntry={null}
|
||||
hostLabelById={new Map()}
|
||||
selectedRunNowAvailability={null}
|
||||
selectedAutomationRunPageWorkspaceDisplay={null}
|
||||
selectedAutomationRunPageViewState={null}
|
||||
canRerunSelectedAutomationRunPage={false}
|
||||
isSelectedAutomationRunPageRerunPending={false}
|
||||
worktreeMap={new Map()}
|
||||
fetchExternalAutomationRuns={vi.fn()}
|
||||
onActivePaneTabChange={vi.fn()}
|
||||
onClearExternalRunPage={vi.fn()}
|
||||
onClearAutomationRunPage={vi.fn()}
|
||||
requestExternalAction={vi.fn()}
|
||||
openExternalRunPage={vi.fn()}
|
||||
openEditExternalDialog={vi.fn()}
|
||||
runNow={vi.fn()}
|
||||
openEditDialog={vi.fn()}
|
||||
toggleAutomation={vi.fn()}
|
||||
requestDeleteAutomation={vi.fn()}
|
||||
rerunAutomationRun={vi.fn()}
|
||||
openRunWorkspace={vi.fn()}
|
||||
openAutomationRunPage={vi.fn()}
|
||||
onBackToList={vi.fn()}
|
||||
recoverSelectedRuns={vi.fn()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
const runsTab = [...container.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find((tab) =>
|
||||
tab.textContent?.includes('Runs')
|
||||
)
|
||||
if (!runsTab) {
|
||||
throw new Error('Runs tab was not rendered')
|
||||
}
|
||||
return runsTab
|
||||
}
|
||||
|
||||
describe('AutomationsDetailPane run count', () => {
|
||||
it('does not report zero when run history is unavailable', async () => {
|
||||
expect((await renderRunsTab(true)).textContent?.trim()).toBe('Runs')
|
||||
})
|
||||
|
||||
it('reports zero when the host answered with an empty history', async () => {
|
||||
expect((await renderRunsTab(false)).textContent).toContain('0')
|
||||
})
|
||||
})
|
||||
@@ -213,7 +213,9 @@ export function AutomationsDetailPane({
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="runs" disabled={!selected}>
|
||||
{translate('auto.components.automations.AutomationsPage.0e110a3469', 'Runs')}{' '}
|
||||
<span className="text-xs text-muted-foreground">{selectedRuns.length}</span>
|
||||
{selectedRunsNotice ? null : (
|
||||
<span className="text-xs text-muted-foreground">{selectedRuns.length}</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
mocks,
|
||||
renderPage,
|
||||
runtimeHost,
|
||||
RUNTIME_ID,
|
||||
RUNTIME_REPO_ID,
|
||||
RUNTIME_SELF_FILTER,
|
||||
RUNTIME_WORKSPACE_ID,
|
||||
@@ -88,7 +89,11 @@ describe('AutomationsPage notice recovery', () => {
|
||||
})
|
||||
|
||||
expect(settings.open).toHaveBeenCalled()
|
||||
expect(settings.target).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
expect(settings.target).toHaveBeenCalledWith({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
sectionId: RUNTIME_ID
|
||||
})
|
||||
})
|
||||
|
||||
it('sends a row Update server to the row own host, not to the unfiltered list', async () => {
|
||||
@@ -116,6 +121,10 @@ describe('AutomationsPage notice recovery', () => {
|
||||
|
||||
// All hosts selects no host, so only the row's captured owner names one.
|
||||
expect(settings.open).toHaveBeenCalled()
|
||||
expect(settings.target).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
expect(settings.target).toHaveBeenCalledWith({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
sectionId: RUNTIME_ID
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -92,6 +92,12 @@ describe('captured owner availability', () => {
|
||||
const availability = automationActionAvailability(legacy, 'delete')
|
||||
expect(availability.kind).toBe('blocked')
|
||||
expect(availability.kind === 'blocked' && availability.block.recovery).toBe('update-server')
|
||||
expect(availability.kind === 'blocked' && availability.block.message).toContain(
|
||||
'needs an update'
|
||||
)
|
||||
expect(availability.kind === 'blocked' && availability.block.message).toContain(
|
||||
'Scheduled runs may still continue'
|
||||
)
|
||||
})
|
||||
|
||||
it('reports a row with no metadata as uncaptured rather than blocked', () => {
|
||||
|
||||
@@ -71,7 +71,7 @@ function unfencedBlock(): AutomationActionBlock {
|
||||
reason: 'unfenced',
|
||||
message: translate(
|
||||
'auto.components.automations.capturedOwner.unfenced',
|
||||
'This host cannot confirm which automation it is changing, so this automation is read-only here.'
|
||||
'Orca on this host needs an update before you can view run history or manage this automation. Scheduled runs may still continue on the host.'
|
||||
),
|
||||
recovery: 'update-server'
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ const DESKTOP_SSH = entry({
|
||||
stableRef: { authority: { kind: 'desktop' }, selector: { kind: 'ssh', targetId: 't1' } },
|
||||
kind: 'ssh'
|
||||
})
|
||||
const RUNTIME_ENVIRONMENT_ID = 'gpu'
|
||||
const RUNTIME_SSH = entry({
|
||||
stableRef: {
|
||||
authority: { kind: 'runtime', environmentId: 'gpu' },
|
||||
authority: { kind: 'runtime', environmentId: RUNTIME_ENVIRONMENT_ID },
|
||||
selector: { kind: 'ssh', targetId: 't1' }
|
||||
},
|
||||
kind: 'ssh'
|
||||
@@ -65,7 +66,7 @@ describe('automation host recovery', () => {
|
||||
{ ...RUNTIME_SSH, authorityHealth: 'unavailable' },
|
||||
target
|
||||
)
|
||||
expect(target.connectRuntimeEnvironment).toHaveBeenCalledWith('gpu')
|
||||
expect(target.connectRuntimeEnvironment).toHaveBeenCalledWith(RUNTIME_ENVIRONMENT_ID)
|
||||
expect(target.connectSshTarget).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -78,10 +79,15 @@ describe('automation host recovery', () => {
|
||||
expect(target.retry).toHaveBeenCalledWith(desktopSelf)
|
||||
})
|
||||
|
||||
it('sends a runtime to the servers pane, since nothing here updates a host', () => {
|
||||
it('deep-links a runtime to its update row in Remote Orca Servers settings', () => {
|
||||
const target = deps()
|
||||
runAutomationHostRecovery('update-server', RUNTIME_SSH, target)
|
||||
expect(target.openSettings).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
expect(target.openSettings).toHaveBeenCalledWith({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
// Runtime environment IDs are nested scroll anchors within the pane.
|
||||
sectionId: RUNTIME_ENVIRONMENT_ID
|
||||
})
|
||||
})
|
||||
|
||||
it('sends a stale desktop SSH registration to the SSH pane instead', () => {
|
||||
|
||||
@@ -20,10 +20,14 @@ export type AutomationHostRecoveryDeps = {
|
||||
openSettings: (target: SettingsNavigationTarget) => void
|
||||
}
|
||||
|
||||
/** Where a host's version is managed; the app cannot update a host on its own. */
|
||||
/** The Remote Orca Servers pane owns each runtime's version status and update action. */
|
||||
function versionSettingsTarget(entry: AutomationHostCatalogEntry): SettingsNavigationTarget {
|
||||
if (entry.stableRef.authority.kind === 'runtime') {
|
||||
return { pane: 'servers', repoId: null }
|
||||
return {
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
sectionId: entry.stableRef.authority.environmentId
|
||||
}
|
||||
}
|
||||
// A desktop SSH host with no registration generation is a stale registration,
|
||||
// repaired by re-adding the target rather than by updating anything.
|
||||
|
||||
@@ -248,8 +248,8 @@ export function useAutomationHostCatalog(
|
||||
void window.api.runtimeEnvironments.connect({ selector: environmentId })
|
||||
},
|
||||
openSettings: (target) => {
|
||||
openSettingsPage()
|
||||
openSettingsTarget(target)
|
||||
openSettingsPage()
|
||||
}
|
||||
}),
|
||||
[controller, openSettingsPage, openSettingsTarget, pairingRevision]
|
||||
|
||||
@@ -3,6 +3,10 @@ import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants'
|
||||
import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit'
|
||||
|
||||
describe('exceedsMarkdownRichModeSizeLimit', () => {
|
||||
it('uses a 600 KB default rich-mode ceiling', () => {
|
||||
expect(RICH_MARKDOWN_MAX_SIZE_BYTES).toBe(600 * 1024)
|
||||
})
|
||||
|
||||
it('allows markdown at the rich-mode byte limit', () => {
|
||||
expect(exceedsMarkdownRichModeSizeLimit('a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES))).toBe(false)
|
||||
})
|
||||
|
||||
@@ -120,6 +120,10 @@ import {
|
||||
getLinearAgentSkillNavInstallStatus
|
||||
} from '@/lib/agent-skill-nav-install-status'
|
||||
import { deriveNeededSectionIds, getInitialMountedSectionIds } from './settings-load-performance'
|
||||
import {
|
||||
watchForSettingsDeepLinkTarget,
|
||||
type SettingsDeepLinkTargetWatch
|
||||
} from './settings-deep-link-target-watcher'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getProjectHostSetupProjectionFromState } from '../../store/selectors'
|
||||
import { getRepoHostIdentity } from '../../store/slices/repo-host-identity'
|
||||
@@ -277,6 +281,13 @@ function cancelPendingSettingsSubsectionScrollFrame(
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPendingSettingsDeepLinkTargetWatch(
|
||||
watchRef: MutableRefObject<SettingsDeepLinkTargetWatch | null>
|
||||
): void {
|
||||
watchRef.current?.cancel()
|
||||
watchRef.current = null
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false
|
||||
@@ -399,6 +410,7 @@ function Settings(): React.JSX.Element {
|
||||
const pendingNavSectionRef = useRef<string | null>(null)
|
||||
const pendingScrollTargetRef = useRef<string | null>(null)
|
||||
const pendingSubsectionScrollFrameRef = useRef<number | null>(null)
|
||||
const pendingScrollTargetWatchRef = useRef<SettingsDeepLinkTargetWatch | null>(null)
|
||||
const repoHooksRequestSeqRef = useRef(0)
|
||||
const shortcutsEscapeConfirmUntilRef = useRef(0)
|
||||
const sourceControlAiWriteQueueRef = useRef<Promise<void>>(Promise.resolve())
|
||||
@@ -446,6 +458,7 @@ function Settings(): React.JSX.Element {
|
||||
}
|
||||
// Why: cancel pending subsection jumps with the scroll container so a stale deep-link frame can't run after close.
|
||||
cancelPendingSettingsSubsectionScrollFrame(pendingSubsectionScrollFrameRef)
|
||||
cancelPendingSettingsDeepLinkTargetWatch(pendingScrollTargetWatchRef)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1043,6 +1056,8 @@ function Settings(): React.JSX.Element {
|
||||
useEffect(() => {
|
||||
const scrollTargetId = pendingScrollTargetRef.current
|
||||
const pendingNavSectionId = pendingNavSectionRef.current
|
||||
// Why: this pass re-decides whether to wait for the target, so drop any watch armed by the previous one.
|
||||
cancelPendingSettingsDeepLinkTargetWatch(pendingScrollTargetWatchRef)
|
||||
|
||||
// Why: subsection deep links clear a stale filter that could hide the target row; pane-level links keep it to force-open the matching section.
|
||||
if (
|
||||
@@ -1069,6 +1084,13 @@ function Settings(): React.JSX.Element {
|
||||
if (scrollTargetId !== pendingNavSectionId) {
|
||||
// Why: target can arrive before the lazy section mounts; keep pending refs until it does.
|
||||
if (!getSettingsScrollTarget(scrollTargetId, container)) {
|
||||
// Why: async panes (the server list, say) mount rows after this pass, and nothing else re-runs the effect.
|
||||
pendingScrollTargetWatchRef.current = watchForSettingsDeepLinkTarget({
|
||||
root: container,
|
||||
isTargetPresent: () =>
|
||||
getSettingsScrollTarget(scrollTargetId, contentScrollRef.current) !== null,
|
||||
onTargetPresent: () => setPendingNavRequestTick((tick) => tick + 1)
|
||||
})
|
||||
return
|
||||
}
|
||||
const scrollToSubsection = (): void => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { watchForSettingsDeepLinkTarget } from './settings-deep-link-target-watcher'
|
||||
|
||||
function flushMutations(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('watchForSettingsDeepLinkTarget', () => {
|
||||
it('reports the target once the async rows render', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const onTargetPresent = vi.fn()
|
||||
watchForSettingsDeepLinkTarget({
|
||||
root: container,
|
||||
isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null,
|
||||
onTargetPresent
|
||||
})
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.dataset.settingsSection = 'env-1'
|
||||
container.append(row)
|
||||
await flushMutations()
|
||||
|
||||
expect(onTargetPresent).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps waiting while unrelated rows render', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const onTargetPresent = vi.fn()
|
||||
watchForSettingsDeepLinkTarget({
|
||||
root: container,
|
||||
isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null,
|
||||
onTargetPresent
|
||||
})
|
||||
|
||||
const other = document.createElement('div')
|
||||
other.dataset.settingsSection = 'env-2'
|
||||
container.append(other)
|
||||
await flushMutations()
|
||||
expect(onTargetPresent).not.toHaveBeenCalled()
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.dataset.settingsSection = 'env-1'
|
||||
container.append(row)
|
||||
await flushMutations()
|
||||
expect(onTargetPresent).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reports at most once even as more rows arrive', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const onTargetPresent = vi.fn()
|
||||
watchForSettingsDeepLinkTarget({
|
||||
root: container,
|
||||
isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null,
|
||||
onTargetPresent
|
||||
})
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.dataset.settingsSection = 'env-1'
|
||||
container.append(row)
|
||||
await flushMutations()
|
||||
container.append(document.createElement('div'))
|
||||
await flushMutations()
|
||||
|
||||
expect(onTargetPresent).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('stops reporting after cancel', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const onTargetPresent = vi.fn()
|
||||
const watch = watchForSettingsDeepLinkTarget({
|
||||
root: container,
|
||||
isTargetPresent: () => true,
|
||||
onTargetPresent
|
||||
})
|
||||
watch.cancel()
|
||||
|
||||
container.append(document.createElement('div'))
|
||||
await flushMutations()
|
||||
|
||||
expect(onTargetPresent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the document body when the pane has no scroll container yet', async () => {
|
||||
const onTargetPresent = vi.fn()
|
||||
watchForSettingsDeepLinkTarget({
|
||||
root: null,
|
||||
isTargetPresent: () => document.querySelector('[data-settings-section="env-1"]') !== null,
|
||||
onTargetPresent
|
||||
})
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.dataset.settingsSection = 'env-1'
|
||||
document.body.append(row)
|
||||
await flushMutations()
|
||||
|
||||
expect(onTargetPresent).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* A deep link can name a row that does not exist yet: panes such as Remote Orca
|
||||
* Servers render their rows only after an async fetch resolves. The scroll
|
||||
* effect parks its target in a ref, but nothing re-runs it when those rows
|
||||
* finally mount, so this watcher reports the moment they do.
|
||||
*/
|
||||
|
||||
/** Give up eventually; a target that never mounts must not leave an observer alive. */
|
||||
const TARGET_WAIT_TIMEOUT_MS = 5000
|
||||
|
||||
export type SettingsDeepLinkTargetWatch = {
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
export function watchForSettingsDeepLinkTarget(args: {
|
||||
/** Subtree to observe; falls back to the document body when the pane has no scroll container yet. */
|
||||
root: HTMLElement | null
|
||||
isTargetPresent: () => boolean
|
||||
onTargetPresent: () => void
|
||||
}): SettingsDeepLinkTargetWatch {
|
||||
const root = args.root ?? (typeof document === 'undefined' ? null : document.body)
|
||||
if (!root || typeof MutationObserver === 'undefined') {
|
||||
return { cancel: () => {} }
|
||||
}
|
||||
|
||||
let settled = false
|
||||
let observer: MutationObserver | null = null
|
||||
let timeout: number | null = null
|
||||
|
||||
const finish = (): void => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
observer?.disconnect()
|
||||
observer = null
|
||||
if (timeout !== null) {
|
||||
window.clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
}
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (settled || !args.isTargetPresent()) {
|
||||
return
|
||||
}
|
||||
finish()
|
||||
args.onTargetPresent()
|
||||
})
|
||||
observer.observe(root, { subtree: true, childList: true })
|
||||
timeout = window.setTimeout(finish, TARGET_WAIT_TIMEOUT_MS)
|
||||
|
||||
return { cancel: finish }
|
||||
}
|
||||
@@ -15405,7 +15405,7 @@
|
||||
"53fc5f07ab": "Run history",
|
||||
"a00e38d1a3": "n/a",
|
||||
"fdb3caa8fb": "known",
|
||||
"historyUnavailable": "Run history could not be loaded for this automation."
|
||||
"historyUnavailable": "Run history is unavailable from this host. This does not mean the automation failed or has no runs."
|
||||
},
|
||||
"AutomationRunPageFrame": {
|
||||
"40a511bed4": "Run context",
|
||||
@@ -15727,7 +15727,7 @@
|
||||
},
|
||||
"capturedOwner": {
|
||||
"orphan": "This automation has no host to run on. Delete it, or re-add the host it belongs to.",
|
||||
"unfenced": "This host cannot confirm which automation it is changing, so this automation is read-only here."
|
||||
"unfenced": "Orca on this host needs an update before you can view run history or manage this automation. Scheduled runs may still continue on the host."
|
||||
},
|
||||
"hostStatus": {
|
||||
"authority": {
|
||||
|
||||
+7
-10
@@ -83,16 +83,13 @@ export const getDefaultTerminalRightClickToPaste = (
|
||||
platform = typeof process !== 'undefined' ? process.platform : ''
|
||||
): boolean => platform === 'win32'
|
||||
|
||||
/** Why: ProseMirror renders the whole document — no virtualization — so opening
|
||||
* one blocks the main thread for as long as it takes to build the DOM. Measured
|
||||
* in a packaged build (M-series) on a doc with a code block every ~570 bytes,
|
||||
* blocking mount / median keystroke: 300 KB 1.7 s / 59 ms · 600 KB 4.2 s / 201 ms
|
||||
* · 900 KB 8.8 s / 431 ms. The same 300 KB as pure prose is 0.8 s / 16 ms, so
|
||||
* node-view count drives the cost far more than byte size — but bytes are the
|
||||
* only thing cheap enough to test before parsing. The blocking mount is what
|
||||
* pins this ceiling; past it, fall back to source mode (Monaco) with a per-file
|
||||
* "Open anyway" escape hatch. Real headroom needs #7056. */
|
||||
export const RICH_MARKDOWN_MAX_SIZE_BYTES = 300 * 1024
|
||||
/** Why: ProseMirror renders the whole document without virtualization. After the
|
||||
* parser/highlighter work in #17134/#17147/#17158, M-series Electron measurements
|
||||
* on distinct code blocks every ~600 bytes put visible mount / longest task at
|
||||
* 300 KB 0.70 / 0.66 s · 450 KB 1.09 / 1.02 s · 600 KB 1.49 / 1.41 s, with
|
||||
* 600 KB typing at 38 ms median / 39 ms p95. Bytes remain the only cheap
|
||||
* pre-parse guard; larger files use source mode with an "Open anyway" escape hatch. */
|
||||
export const RICH_MARKDOWN_MAX_SIZE_BYTES = 600 * 1024
|
||||
|
||||
export const DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS = 1000
|
||||
export const MIN_EDITOR_AUTO_SAVE_DELAY_MS = 250
|
||||
|
||||
Reference in New Issue
Block a user