From e2aa6bf636c88a00a25789f6be494319837b1a3a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sat, 13 Jun 2026 00:39:32 -0700 Subject: [PATCH] Clarify runtime automation disabled states Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + .../automations/AutomationsPage.tsx | 10 ++- .../automation-target-availability.test.ts | 75 +++++++++++++++++++ .../automation-target-availability.ts | 69 ++++++++++++++++- 4 files changed, 148 insertions(+), 7 deletions(-) diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index b5765f62a63..40a5a0f228e 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -272,3 +272,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-13: Tightened runtime project-host setup mutations so older remote servers that advertise `project-host-setup.v1` but not `workspace-run-context.v1` are blocked before setup/clone/create/update/delete RPCs mutate server state; read-only hydration still degrades to repo-derived compatibility rows. Verified with focused repo capability tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-13: Routed the full-page Pull Request metadata popovers and mention suggestions through the PR repo owner host instead of ambient focused runtime state, so reviewer, label, assignee, and comment-composer reads align with the existing repo-owned mutation routing. Verified with a focused PR host-boundary test, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-13: Routed GitHub Project view field edits, slug-only row edits, issue-type lookups, label/assignee pickers, and slug dialog detail reads through the Project view source host instead of the ambient focused runtime when no registered repo owner is available. Verified with GitHub slice source-routing regressions, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. +- [x] 2026-06-13: Replaced the generic remote-server Automation manual-run block with explicit checking, disconnected/unreachable, graph-not-ready, and update-required disabled states derived from saved runtime status before the current unsupported manual-run fallback. Verified with focused automation availability tests, targeted oxlint, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index 87b2b557d66..e121966e91b 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -276,6 +276,7 @@ export default function AutomationsPage(): React.JSX.Element { const agentStatusByPaneKey = useAppStore((s) => s.agentStatusByPaneKey) const retainedAgentsByPaneKey = useAppStore((s) => s.retainedAgentsByPaneKey) const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) + const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId) const settings = useAppStore((s) => s.settings) const selectedId = useAppStore((s) => s.selectedAutomationId) const setSelectedId = useAppStore((s) => s.setSelectedAutomationId) @@ -498,7 +499,8 @@ export default function AutomationsPage(): React.JSX.Element { repo: selectedRepo, workspace: selectedWorktree, projectHostSetups, - sshConnectionStates + sshConnectionStates, + runtimeStatusByEnvironmentId }) : null const canSaveDraft = @@ -1250,7 +1252,8 @@ export default function AutomationsPage(): React.JSX.Element { repo, workspace, projectHostSetups, - sshConnectionStates + sshConnectionStates, + runtimeStatusByEnvironmentId }) if (!availability.canRunNow) { toast.error(availability.message) @@ -1833,7 +1836,8 @@ export default function AutomationsPage(): React.JSX.Element { repo: automationRepo, workspace: automationWorktree, projectHostSetups, - sshConnectionStates + sshConnectionStates, + runtimeStatusByEnvironmentId }) const workspaceLabel = automation.workspaceMode === 'new_per_run' diff --git a/src/renderer/src/components/automations/automation-target-availability.test.ts b/src/renderer/src/components/automations/automation-target-availability.test.ts index 97b020ac531..759192d3e63 100644 --- a/src/renderer/src/components/automations/automation-target-availability.test.ts +++ b/src/renderer/src/components/automations/automation-target-availability.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Automation } from '../../../../shared/automations-types' +import type { RuntimeStatus } from '../../../../shared/runtime-types' import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' import { getAutomationTargetAvailability } from './automation-target-availability' @@ -69,6 +70,20 @@ function makeProjectHostSetup(overrides: Partial = {}): Projec } } +function makeRuntimeStatus(overrides: Partial = {}): RuntimeStatus { + return { + runtimeId: 'runtime-1', + rendererGraphEpoch: 1, + graphStatus: 'ready', + authoritativeWindowId: null, + liveTabCount: 0, + liveLeafCount: 0, + runtimeProtocolVersion: 3, + minCompatibleRuntimeClientVersion: 2, + ...overrides + } +} + describe('automation target availability', () => { it('allows local automations with an available existing workspace', () => { expect( @@ -240,4 +255,64 @@ describe('automation target availability', () => { }).reason ).toBe('ssh-connecting') }) + + it('explains runtime-host automation availability before the unsupported manual-run fallback', () => { + const automation = makeAutomation({ + runContext: { + kind: 'workspace-run', + projectId: 'project-1', + hostId: 'runtime:env-1', + projectHostSetupId: 'setup-1', + repoId: 'repo-1', + path: '/repo' + } + }) + const repo = makeRepo({ executionHostId: 'runtime:env-1' }) + const setup = makeProjectHostSetup({ + hostId: 'runtime:env-1', + executionHostId: 'runtime:env-1' + }) + const base = { + automation, + repo, + workspace: makeWorkspace(), + projectHostSetups: [setup], + sshConnectionStates: new Map() + } + + expect(getAutomationTargetAvailability(base).reason).toBe('runtime-checking') + expect( + getAutomationTargetAvailability({ + ...base, + runtimeStatusByEnvironmentId: new Map([['env-1', { status: null, checkedAt: 1 }]]) + }).reason + ).toBe('runtime-unavailable') + expect( + getAutomationTargetAvailability({ + ...base, + runtimeStatusByEnvironmentId: new Map([ + ['env-1', { status: makeRuntimeStatus({ graphStatus: 'unavailable' }), checkedAt: 1 }] + ]) + }).message + ).toBe('The selected remote server is not ready to run automations yet.') + expect( + getAutomationTargetAvailability({ + ...base, + runtimeStatusByEnvironmentId: new Map([ + ['env-1', { status: makeRuntimeStatus({ runtimeProtocolVersion: 0 }), checkedAt: 1 }] + ]) + }).reason + ).toBe('runtime-update-required') + expect( + getAutomationTargetAvailability({ + ...base, + runtimeStatusByEnvironmentId: new Map([ + ['env-1', { status: makeRuntimeStatus(), checkedAt: 1 }] + ]) + }) + ).toMatchObject({ + reason: 'unsupported-host', + message: 'Manual runs for remote-server automations are not available from this client yet.' + }) + }) }) diff --git a/src/renderer/src/components/automations/automation-target-availability.ts b/src/renderer/src/components/automations/automation-target-availability.ts index 0a459e4c78a..0319078de10 100644 --- a/src/renderer/src/components/automations/automation-target-availability.ts +++ b/src/renderer/src/components/automations/automation-target-availability.ts @@ -1,6 +1,15 @@ import type { Automation } from '../../../../shared/automations-types' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' +import { + describeRuntimeCompatBlock, + evaluateRuntimeCompat +} from '../../../../shared/protocol-compat' +import { + MIN_COMPATIBLE_RUNTIME_SERVER_VERSION, + RUNTIME_PROTOCOL_VERSION +} from '../../../../shared/protocol-version' import type { SshConnectionState } from '../../../../shared/ssh-types' +import type { RuntimeStatus } from '../../../../shared/runtime-types' import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' export type AutomationTargetAvailability = @@ -18,6 +27,9 @@ export type AutomationTargetAvailability = | 'missing-workspace' | 'host-mismatch' | 'unsupported-host' + | 'runtime-checking' + | 'runtime-unavailable' + | 'runtime-update-required' | 'ssh-auth-needed' | 'ssh-unavailable' | 'ssh-connecting' @@ -30,6 +42,10 @@ type AutomationTargetAvailabilityArgs = { workspace: Worktree | null | undefined projectHostSetups: readonly ProjectHostSetup[] sshConnectionStates: ReadonlyMap> + runtimeStatusByEnvironmentId?: ReadonlyMap< + string, + { status: RuntimeStatus | null; checkedAt: number } + > } export function getAutomationTargetAvailability({ @@ -37,7 +53,8 @@ export function getAutomationTargetAvailability({ repo, workspace, projectHostSetups, - sshConnectionStates + sshConnectionStates, + runtimeStatusByEnvironmentId }: AutomationTargetAvailabilityArgs): AutomationTargetAvailability { if (!repo) { return unavailable('missing-project', 'The target project is no longer available.') @@ -45,10 +62,13 @@ export function getAutomationTargetAvailability({ if (automation.runContext) { const parsedHost = parseExecutionHostId(automation.runContext.hostId) if (parsedHost?.kind === 'runtime') { - return unavailable( - 'unsupported-host', - 'This automation targets a remote server that this client cannot run manually yet.' + const runtimeAvailability = getRuntimeAutomationAvailability( + parsedHost.environmentId, + runtimeStatusByEnvironmentId ) + if (!runtimeAvailability.canRunNow) { + return runtimeAvailability + } } const setup = projectHostSetups.find( (candidate) => candidate.id === automation.runContext?.projectHostSetupId @@ -106,6 +126,47 @@ export function getAutomationTargetAvailability({ } } +function getRuntimeAutomationAvailability( + environmentId: string, + runtimeStatusByEnvironmentId: + | ReadonlyMap + | undefined +): AutomationTargetAvailability { + const entry = runtimeStatusByEnvironmentId?.get(environmentId) + if (!entry) { + return unavailable( + 'runtime-checking', + 'Checking the selected remote server before running manually.' + ) + } + if (!entry.status) { + return unavailable( + 'runtime-unavailable', + 'Reconnect this remote server before running manually.' + ) + } + if (entry.status.graphStatus !== 'ready') { + return unavailable( + 'runtime-unavailable', + 'The selected remote server is not ready to run automations yet.' + ) + } + const compat = evaluateRuntimeCompat({ + clientProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleServerProtocolVersion: MIN_COMPATIBLE_RUNTIME_SERVER_VERSION, + serverProtocolVersion: entry.status.runtimeProtocolVersion ?? entry.status.protocolVersion, + serverMinCompatibleClientProtocolVersion: + entry.status.minCompatibleRuntimeClientVersion ?? entry.status.minCompatibleMobileVersion + }) + if (compat.kind === 'blocked') { + return unavailable('runtime-update-required', describeRuntimeCompatBlock(compat)) + } + return unavailable( + 'unsupported-host', + 'Manual runs for remote-server automations are not available from this client yet.' + ) +} + function getAutomationSshTargetId(automation: Automation, repo: Repo): string | null { const parsedHost = parseExecutionHostId(automation.runContext?.hostId) if (parsedHost?.kind === 'ssh') {