From b826b3fa5b7b3a3d28babd86adc5dba2568bf2f8 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Sat, 29 Aug 2026 15:03:36 -0700
Subject: [PATCH 1/2] feat(editor): raise rich Markdown size limit to 600 KB
(#17288)
---
.../editor/markdown-rich-size-limit.test.ts | 4 ++++
src/shared/constants.ts | 17 +++++++----------
2 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts b/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts
index 616e298b175..3539ffc565f 100644
--- a/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts
+++ b/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts
@@ -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)
})
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 15c47bdd968..96c7901cc08 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -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
From 8b01cf1a48d91cf28a87451cb5412d5c7626e46c Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Sat, 29 Aug 2026 15:07:38 -0700
Subject: [PATCH 2/2] Clarify automation history unavailability and improve
recovery UX (#17284)
* Clarify automation history unavailability and improve recovery UX
- Improve error message to explain run history is unavailable due to host
version requirements, not automation failure
- Hide misleading "0 runs" count badge when history is unavailable
- Deep-link "update server" recovery actions to specific runtime environment
in settings instead of pane root
- Add test coverage for run count hiding and recovery targeting
* Watch for deep-linked settings targets that render asynchronously
Some settings panes (such as Remote Orca Servers) fetch and render their
rows asynchronously. Deep links can name targets that don't exist yet,
but the scroll effect has no way to know when they finally mount. Add a
MutationObserver-based watcher to detect when async rows appear and
trigger scrolling.
---
.../automations/AutomationRunHistory.test.tsx | 3 +-
.../automations/AutomationRunHistory.tsx | 2 +-
.../AutomationsDetailPane.run-count.test.tsx | 88 ++++++++++++++
.../automations/AutomationsDetailPane.tsx | 4 +-
.../AutomationsPage.notice-recovery.test.tsx | 13 ++-
.../automation-captured-owner.test.ts | 6 +
.../automations/automation-captured-owner.ts | 2 +-
.../automation-host-recovery.test.ts | 14 ++-
.../automations/automation-host-recovery.ts | 8 +-
.../use-automation-host-catalog.ts | 2 +-
.../src/components/settings/Settings.tsx | 22 ++++
.../settings-deep-link-target-watcher.test.ts | 108 ++++++++++++++++++
.../settings-deep-link-target-watcher.ts | 54 +++++++++
src/renderer/src/i18n/locales/en.json | 4 +-
14 files changed, 315 insertions(+), 15 deletions(-)
create mode 100644 src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx
create mode 100644 src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts
create mode 100644 src/renderer/src/components/settings/settings-deep-link-target-watcher.ts
diff --git a/src/renderer/src/components/automations/AutomationRunHistory.test.tsx b/src/renderer/src/components/automations/AutomationRunHistory.test.tsx
index 7742f74b26a..41d839e615a 100644
--- a/src/renderer/src/components/automations/AutomationRunHistory.test.tsx
+++ b/src/renderer/src/components/automations/AutomationRunHistory.test.tsx
@@ -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')
})
diff --git a/src/renderer/src/components/automations/AutomationRunHistory.tsx b/src/renderer/src/components/automations/AutomationRunHistory.tsx
index cd2ece0d7dc..13395dae6ad 100644
--- a/src/renderer/src/components/automations/AutomationRunHistory.tsx
+++ b/src/renderer/src/components/automations/AutomationRunHistory.tsx
@@ -167,7 +167,7 @@ export function AutomationRunHistory({
{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.'
)}
diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx
new file mode 100644
index 00000000000..740f1130405
--- /dev/null
+++ b/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx
@@ -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 {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ await act(async () => {
+ root?.render(
+
+ )
+ })
+ const runsTab = [...container.querySelectorAll('[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')
+ })
+})
diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.tsx
index 7eb2ff13c1d..0affe311f2c 100644
--- a/src/renderer/src/components/automations/AutomationsDetailPane.tsx
+++ b/src/renderer/src/components/automations/AutomationsDetailPane.tsx
@@ -213,7 +213,9 @@ export function AutomationsDetailPane({
{translate('auto.components.automations.AutomationsPage.0e110a3469', 'Runs')}{' '}
- {selectedRuns.length}
+ {selectedRunsNotice ? null : (
+ {selectedRuns.length}
+ )}
diff --git a/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx b/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx
index 503d0877ed5..d99cfb91dac 100644
--- a/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx
+++ b/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx
@@ -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
+ })
})
})
diff --git a/src/renderer/src/components/automations/automation-captured-owner.test.ts b/src/renderer/src/components/automations/automation-captured-owner.test.ts
index ffc998a5b36..ca22b0286e0 100644
--- a/src/renderer/src/components/automations/automation-captured-owner.test.ts
+++ b/src/renderer/src/components/automations/automation-captured-owner.test.ts
@@ -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', () => {
diff --git a/src/renderer/src/components/automations/automation-captured-owner.ts b/src/renderer/src/components/automations/automation-captured-owner.ts
index 761dd787e78..7a95b517fe5 100644
--- a/src/renderer/src/components/automations/automation-captured-owner.ts
+++ b/src/renderer/src/components/automations/automation-captured-owner.ts
@@ -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'
}
diff --git a/src/renderer/src/components/automations/automation-host-recovery.test.ts b/src/renderer/src/components/automations/automation-host-recovery.test.ts
index 8d6419fb128..871f9343114 100644
--- a/src/renderer/src/components/automations/automation-host-recovery.test.ts
+++ b/src/renderer/src/components/automations/automation-host-recovery.test.ts
@@ -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', () => {
diff --git a/src/renderer/src/components/automations/automation-host-recovery.ts b/src/renderer/src/components/automations/automation-host-recovery.ts
index bc71f08ff18..d443d485b1d 100644
--- a/src/renderer/src/components/automations/automation-host-recovery.ts
+++ b/src/renderer/src/components/automations/automation-host-recovery.ts
@@ -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.
diff --git a/src/renderer/src/components/automations/use-automation-host-catalog.ts b/src/renderer/src/components/automations/use-automation-host-catalog.ts
index bd7cd8dbb67..e40a4393e20 100644
--- a/src/renderer/src/components/automations/use-automation-host-catalog.ts
+++ b/src/renderer/src/components/automations/use-automation-host-catalog.ts
@@ -248,8 +248,8 @@ export function useAutomationHostCatalog(
void window.api.runtimeEnvironments.connect({ selector: environmentId })
},
openSettings: (target) => {
- openSettingsPage()
openSettingsTarget(target)
+ openSettingsPage()
}
}),
[controller, openSettingsPage, openSettingsTarget, pairingRevision]
diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx
index fb928aa2ceb..f51a7fe22f5 100644
--- a/src/renderer/src/components/settings/Settings.tsx
+++ b/src/renderer/src/components/settings/Settings.tsx
@@ -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
+): 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(null)
const pendingScrollTargetRef = useRef(null)
const pendingSubsectionScrollFrameRef = useRef(null)
+ const pendingScrollTargetWatchRef = useRef(null)
const repoHooksRequestSeqRef = useRef(0)
const shortcutsEscapeConfirmUntilRef = useRef(0)
const sourceControlAiWriteQueueRef = useRef>(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 => {
diff --git a/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts b/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts
new file mode 100644
index 00000000000..f3d87ee07c6
--- /dev/null
+++ b/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts
@@ -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 {
+ 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)
+ })
+})
diff --git a/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts b/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts
new file mode 100644
index 00000000000..30d7ee1b103
--- /dev/null
+++ b/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts
@@ -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 }
+}
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 23f03820b50..6660bb6c200 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -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": {