diff --git a/src/renderer/src/components/automations/AutomationDeleteDialogs.tsx b/src/renderer/src/components/automations/AutomationDeleteDialogs.tsx
index 0dcba7d0635..9921ad0b614 100644
--- a/src/renderer/src/components/automations/AutomationDeleteDialogs.tsx
+++ b/src/renderer/src/components/automations/AutomationDeleteDialogs.tsx
@@ -152,7 +152,7 @@ export function ExternalAutomationDeleteDialog({
'auto.components.automations.AutomationsPage.8500baacb4',
'external source'
)}{' '}
- {translate('auto.components.automations.AutomationsPage.1b586f0e2b', 'on')}
+ {translate('auto.components.automations.AutomationsPage.1b586f0e2b', 'on')}{' '}
{externalDeleteTarget?.manager.targetLabel}.
diff --git a/src/renderer/src/components/automations/AutomationListExternalRows.tsx b/src/renderer/src/components/automations/AutomationListExternalRows.tsx
index eeb246110e2..b5269b65593 100644
--- a/src/renderer/src/components/automations/AutomationListExternalRows.tsx
+++ b/src/renderer/src/components/automations/AutomationListExternalRows.tsx
@@ -104,7 +104,7 @@ export function AutomationListExternalRows({
}
const nextRunLabel = entry.job.enabled
? formatExternalDate(entry.job.nextRunAt, relativeNow)
- : 'Paused'
+ : translate('auto.components.automations.AutomationsPage.paused', 'Paused')
const entrySshStatus =
entry.manager.target.type === 'ssh'
? sshConnectionStates.get(entry.manager.target.connectionId)?.status
@@ -151,7 +151,11 @@ export function AutomationListExternalRows({
·
{entry.manager.provider === 'hermes'
- ? `${entry.job.runCount} ${entry.job.runCount === 1 ? 'run' : 'runs'}`
+ ? translate(
+ 'auto.components.automations.AutomationsPage.runCount',
+ '{{count}} runs',
+ { count: entry.job.runCount }
+ )
: entry.manager.canManage
? translate(
'auto.components.automations.AutomationsPage.aecdc3681f',
diff --git a/src/renderer/src/components/automations/AutomationListLocalRows.tsx b/src/renderer/src/components/automations/AutomationListLocalRows.tsx
index 0dd2d6c1c2f..1a5a4994aac 100644
--- a/src/renderer/src/components/automations/AutomationListLocalRows.tsx
+++ b/src/renderer/src/components/automations/AutomationListLocalRows.tsx
@@ -67,6 +67,20 @@ export function AutomationListLocalRows({
onToggle: (automation: Automation) => void
onDelete: (automation: Automation) => void
}): React.JSX.Element {
+ // Why: one pass over runs instead of a full scan per rendered automation —
+ // this list re-renders on the relativeNow timer.
+ const runsByAutomationId = React.useMemo(() => {
+ const grouped = new Map()
+ for (const run of runs) {
+ const existing = grouped.get(run.automationId)
+ if (existing) {
+ existing.push(run)
+ } else {
+ grouped.set(run.automationId, [run])
+ }
+ }
+ return grouped
+ }, [runs])
return (
<>
{automations.map((automation) => {
@@ -84,24 +98,50 @@ export function AutomationListLocalRows({
automationHostTarget,
sourceHostAvailability: automationSourceHostAvailabilityById.get(automation.id)
})
+ const baseRefLabel =
+ automation.baseBranch ??
+ automationRepo?.worktreeBaseRef ??
+ translate(
+ 'auto.components.automations.AutomationsPage.projectDefaultBaseRef',
+ 'project default'
+ )
const workspaceLabel =
automation.workspaceMode === 'new_per_run'
- ? `Create from ${automation.baseBranch ?? automationRepo?.worktreeBaseRef ?? 'project default'}`
- : (automationWorktree?.displayName ?? 'Missing workspace')
+ ? translate(
+ 'auto.components.automations.AutomationsPage.createFromBaseRef',
+ 'Create from {{baseRef}}',
+ { baseRef: baseRefLabel }
+ )
+ : (automationWorktree?.displayName ??
+ translate(
+ 'auto.components.automations.AutomationsPage.missingWorkspace',
+ 'Missing workspace'
+ ))
const usageSummary = summarizeAutomationRunUsage(
- runs.filter((run) => run.automationId === automation.id)
+ runsByAutomationId.get(automation.id) ?? []
)
const usageText =
usageSummary.knownRuns > 0
- ? `${formatAutomationCost(
- usageSummary.estimatedCostUsd
- )} est. · ${formatAutomationTokens(usageSummary.totalTokens)} tokens`
+ ? translate(
+ 'auto.components.automations.AutomationsPage.runUsageSummary',
+ '{{cost}} est. · {{tokens}} tokens',
+ {
+ cost: formatAutomationCost(usageSummary.estimatedCostUsd),
+ tokens: formatAutomationTokens(usageSummary.totalTokens)
+ }
+ )
: usageSummary.unavailableRuns > 0
- ? 'Usage unavailable'
- : 'No run usage yet'
+ ? translate(
+ 'auto.components.automations.AutomationsPage.usageUnavailable',
+ 'Usage unavailable'
+ )
+ : translate(
+ 'auto.components.automations.AutomationsPage.noRunUsageYet',
+ 'No run usage yet'
+ )
const nextRunLabel = automation.enabled
? formatAutomationDateTimeWithRelative(automation.nextRunAt, relativeNow)
- : 'Paused'
+ : translate('auto.components.automations.AutomationsPage.paused', 'Paused')
const scheduleLabel = formatAutomationSchedule(automation.rrule)
return (
diff --git a/src/renderer/src/components/automations/AutomationListSearchField.tsx b/src/renderer/src/components/automations/AutomationListSearchField.tsx
index 4700891eeaa..3a2bceb7c48 100644
--- a/src/renderer/src/components/automations/AutomationListSearchField.tsx
+++ b/src/renderer/src/components/automations/AutomationListSearchField.tsx
@@ -45,8 +45,13 @@ export function AutomationListSearchField({
)}
aria-invalid={isTooLarge || undefined}
aria-describedby={isTooLarge ? 'automations-list-search-too-large' : undefined}
- className="h-8 border-border/60 bg-background pl-8 text-xs"
- style={hasText ? { paddingRight: isTooLarge ? 72 : 28 } : undefined}
+ // Why: the page-level capture Escape handler blurs inputs; this opts out
+ // so the first Escape clears the query without also losing focus.
+ data-escape-clears-value={hasText ? 'true' : undefined}
+ className={cn(
+ 'h-8 border-border/60 bg-background pl-8 text-xs',
+ hasText && (isTooLarge ? 'pr-20' : 'pr-7')
+ )}
onChange={(event) => onQueryChange(event.target.value)}
onKeyDown={(event) => {
if (event.key !== 'Escape' || event.nativeEvent.isComposing) {
diff --git a/src/renderer/src/components/automations/AutomationRunHistory.tsx b/src/renderer/src/components/automations/AutomationRunHistory.tsx
index be8a76dcd5e..777eaff3fa2 100644
--- a/src/renderer/src/components/automations/AutomationRunHistory.tsx
+++ b/src/renderer/src/components/automations/AutomationRunHistory.tsx
@@ -19,7 +19,7 @@ import { translate } from '@/i18n/i18n'
type AutomationRunHistoryProps = {
runs: AutomationRun[]
automationId: string
- worktreeMap: Map
+ worktreeMap: ReadonlyMap
onOpenRun: (run: AutomationRun) => void
}
diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.tsx
index d7c3fd3347d..2d290f26ac6 100644
--- a/src/renderer/src/components/automations/AutomationsDetailPane.tsx
+++ b/src/renderer/src/components/automations/AutomationsDetailPane.tsx
@@ -37,16 +37,9 @@ import type { AutomationTargetAvailability } from './automation-target-availabil
import type { AutomationRunViewState } from './automation-run-view-state'
import type { AutomationRunWorkspaceDisplay } from './automation-run-workspace-display'
import type { ExternalAutomationSourceAvailability } from './external-automation-source-availability'
+import type { AutomationPaneTab, SelectedExternalRunPage } from './automation-page-state'
import { translate } from '@/i18n/i18n'
-type AutomationPaneTab = 'overview' | 'runs'
-
-type SelectedExternalRunPage = {
- manager: ExternalAutomationManager
- job: ExternalAutomationJob
- run: ExternalAutomationRun
-}
-
type AutomationsDetailPaneProps = {
selected: Automation | null
selectedExternal: ExternalAutomationListEntry | null
@@ -70,7 +63,7 @@ type AutomationsDetailPaneProps = {
selectedAutomationRunPageViewState: AutomationRunViewState | null
canRerunSelectedAutomationRunPage: boolean
isSelectedAutomationRunPageRerunPending: boolean
- worktreeMap: Map
+ worktreeMap: ReadonlyMap
fetchExternalAutomationRuns: FetchExternalAutomationRuns
onActivePaneTabChange: (tab: AutomationPaneTab) => void
onClearExternalRunPage: () => void
@@ -263,10 +256,19 @@ export function AutomationsDetailPane({
relativeNow
),
'Orca',
- selectedAutomationRunPageWorkspaceDisplay?.detailLabel ?? 'No workspace'
+ selectedAutomationRunPageWorkspaceDisplay?.detailLabel ??
+ translate(
+ 'auto.components.automations.AutomationsPage.noWorkspace',
+ 'No workspace'
+ )
]}
detail={
- selectedAutomationRunPage.outputSnapshot?.truncated ? 'Latest saved output' : null
+ selectedAutomationRunPage.outputSnapshot?.truncated
+ ? translate(
+ 'auto.components.automations.AutomationsPage.latestSavedOutput',
+ 'Latest saved output'
+ )
+ : null
}
statusLabel={getAutomationRunStatusLabel(selectedAutomationRunPage.status)}
statusVariant={getAutomationRunStatusVariant(selectedAutomationRunPage.status)}
diff --git a/src/renderer/src/components/automations/AutomationsListPanel.tsx b/src/renderer/src/components/automations/AutomationsListPanel.tsx
index a40fd9426f6..31d9cc9d6d9 100644
--- a/src/renderer/src/components/automations/AutomationsListPanel.tsx
+++ b/src/renderer/src/components/automations/AutomationsListPanel.tsx
@@ -14,6 +14,7 @@ import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
import type { AutomationHostTarget } from './automation-host-client'
import { clampAutomationListSearchQueryInput } from './automation-list-search'
+import type { AutomationPaneTab } from './automation-page-state'
import { AutomationListSearchField } from './AutomationListSearchField'
import { getAutomationTemplates, type AutomationTemplate } from './automation-templates'
import type { ExternalAutomationListEntry } from './external-automation-list-entries'
@@ -47,7 +48,7 @@ type AutomationsListPanelProps = {
externalActionKey: string | null
selectAutomationId: (automationId: string | null) => void
selectExternalKey: (externalKey: string | null) => void
- setActivePaneTab: (tab: 'overview' | 'runs') => void
+ setActivePaneTab: (tab: AutomationPaneTab) => void
runNow: (automation: Automation) => void
openEditDialog: (automation: Automation) => void
toggleAutomation: (automation: Automation) => void
diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx
index 0487379d2f6..97f15d844a6 100644
--- a/src/renderer/src/components/automations/AutomationsPage.tsx
+++ b/src/renderer/src/components/automations/AutomationsPage.tsx
@@ -105,6 +105,7 @@ import {
getRuntimeSourceHostAvailability,
type RepoBackedAutomationSourceContext
} from './automation-source-context'
+import type { AutomationPaneTab, SelectedExternalRunPage } from './automation-page-state'
import {
getExternalAutomationKey,
getExternalAutomationSourceKey,
@@ -122,14 +123,6 @@ import { translate } from '@/i18n/i18n'
const AGENTS = getAgentCatalog().map((agent) => agent.id)
const AUTOMATIONS_CHANGED_EVENT = 'orca:automations-changed'
-type AutomationPaneTab = 'overview' | 'runs'
-
-type SelectedExternalRunPage = {
- manager: ExternalAutomationManager
- job: ExternalAutomationJob
- run: ExternalAutomationRun
-}
-
export default function AutomationsPage(): React.JSX.Element {
const repos = useAppStore((s) => s.repos)
const projectHostSetups = useAppStore((s) => s.projectHostSetups)
@@ -236,6 +229,9 @@ export default function AutomationsPage(): React.JSX.Element {
const [dontAskDeleteAgain, setDontAskDeleteAgain] = useState(false)
const editRequestRef = useRef(0)
const deleteConfirmButtonRef = useRef(null)
+ // Why: both dialogs stay mounted, so a shared ref would let one dialog's
+ // unmount clear the focus target the other still needs.
+ const externalDeleteConfirmButtonRef = useRef(null)
const completionInFlightRef = useRef>(new Set())
const rerunRunIdsInFlightRef = useRef>(new Set())
const workspaceNameCacheRef = useRef