From 977ee5aa3b1279d4fe324c05664146a6dda9e104 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:40:01 +0200 Subject: [PATCH 1/4] feat(automations): add unread run badge --- .../core/automations/automations-service.ts | 16 +++- .../src/main/core/automations/controller.ts | 1 + .../src/main/core/settings/schema.ts | 2 + .../main/core/settings/settings-registry.ts | 1 + .../use-automation-unread-count.ts | 43 ++++++++++ .../sidebar/automations-sidebar-item.tsx | 85 +++++++++++++++++++ .../features/sidebar/left-sidebar.tsx | 15 +--- 7 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts create mode 100644 apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx diff --git a/apps/emdash-desktop/src/main/core/automations/automations-service.ts b/apps/emdash-desktop/src/main/core/automations/automations-service.ts index 292c079de5..f62b83c044 100644 --- a/apps/emdash-desktop/src/main/core/automations/automations-service.ts +++ b/apps/emdash-desktop/src/main/core/automations/automations-service.ts @@ -1,4 +1,4 @@ -import { and, asc, count, desc, eq, ne, sql } from 'drizzle-orm'; +import { and, asc, count, desc, eq, gt, inArray, ne, sql } from 'drizzle-orm'; import { db } from '@main/db/client'; import { automationRuns, tasks } from '@main/db/schema'; import { events } from '@main/lib/events'; @@ -151,6 +151,20 @@ export class AutomationsService implements Hookable { return rows.map(({ run, taskId }) => mapAutomationRunRowToAutomationRun(run, taskId)); } + async countUnreadFinishedRuns(sinceMs: number): Promise { + const finishedAt = sql`COALESCE(${automationRuns.finishedAt}, ${automationRuns.startedAt}, ${automationRuns.scheduledAt})`; + const [result] = await db + .select({ count: count() }) + .from(automationRuns) + .where( + and( + inArray(automationRuns.status, ['done', 'failed', 'skipped']), + gt(finishedAt, sinceMs) + ) + ); + return result?.count ?? 0; + } + async countAutomationRunsByStatus( automationId: string ): Promise<{ all: number; done: number; failed: number; skipped: number }> { diff --git a/apps/emdash-desktop/src/main/core/automations/controller.ts b/apps/emdash-desktop/src/main/core/automations/controller.ts index ea76812721..8fd3cba871 100644 --- a/apps/emdash-desktop/src/main/core/automations/controller.ts +++ b/apps/emdash-desktop/src/main/core/automations/controller.ts @@ -11,6 +11,7 @@ export const automationsController = createRPCController({ listAutomationRuns: automationsService.listAutomationRuns.bind(automationsService), countAutomationRunsByStatus: automationsService.countAutomationRunsByStatus.bind(automationsService), + countUnreadFinishedRuns: automationsService.countUnreadFinishedRuns.bind(automationsService), getLatestRun: automationsService.getLatestRun.bind(automationsService), getNextScheduledRun: automationsService.getNextScheduledRun.bind(automationsService), runAutomation: automationsService.runAutomation.bind(automationsService), diff --git a/apps/emdash-desktop/src/main/core/settings/schema.ts b/apps/emdash-desktop/src/main/core/settings/schema.ts index 96701939f4..82bedc1ab8 100644 --- a/apps/emdash-desktop/src/main/core/settings/schema.ts +++ b/apps/emdash-desktop/src/main/core/settings/schema.ts @@ -91,6 +91,8 @@ export const interfaceSettingsSchema = z.object({ showLeftSidebarTimestamps: z.boolean(), confirmTabClose: z.boolean(), hideContextBar: z.boolean(), + /** Timestamp (ms) of the last time the user viewed automations; used for unread run badges. */ + automationsLastReadAt: z.number(), }); export const changesViewModeSchema = z.object({ diff --git a/apps/emdash-desktop/src/main/core/settings/settings-registry.ts b/apps/emdash-desktop/src/main/core/settings/settings-registry.ts index 2c6a83ccae..6212d8d9b9 100644 --- a/apps/emdash-desktop/src/main/core/settings/settings-registry.ts +++ b/apps/emdash-desktop/src/main/core/settings/settings-registry.ts @@ -60,6 +60,7 @@ export const SETTINGS_DEFAULTS = { showLeftSidebarTimestamps: true, confirmTabClose: false, hideContextBar: false, + automationsLastReadAt: 0, }, browserPreview: { enabled: true, diff --git a/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts new file mode 100644 index 0000000000..77c4643141 --- /dev/null +++ b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts @@ -0,0 +1,43 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect } from 'react'; +import { useAppSettingsKey } from '@renderer/features/settings/use-app-settings-key'; +import { events, rpc } from '@renderer/lib/ipc'; +import { automationRunChangedChannel } from '@shared/core/automations/automationEvents'; + +export const automationUnreadCountQueryKey = (lastReadAt: number) => + ['automations', 'unread-count', lastReadAt] as const; + +export function useAutomationUnreadCount() { + const { value: interfaceSettings, updateAsync } = useAppSettingsKey('interface'); + const lastReadAt = interfaceSettings?.automationsLastReadAt ?? 0; + const queryClient = useQueryClient(); + + useEffect(() => { + if (lastReadAt !== 0 || interfaceSettings === undefined) return; + void updateAsync({ automationsLastReadAt: Date.now() }); + }, [interfaceSettings, lastReadAt, updateAsync]); + + const query = useQuery({ + queryKey: automationUnreadCountQueryKey(lastReadAt), + queryFn: () => rpc.automations.countUnreadFinishedRuns(lastReadAt), + enabled: lastReadAt > 0, + }); + + useEffect(() => { + return events.on(automationRunChangedChannel, () => { + void queryClient.invalidateQueries({ queryKey: ['automations', 'unread-count'] }); + }); + }, [queryClient]); + + return query.data ?? 0; +} + +export function useMarkAutomationsRead() { + const { updateAsync } = useAppSettingsKey('interface'); + const queryClient = useQueryClient(); + + return useCallback(async () => { + await updateAsync({ automationsLastReadAt: Date.now() }); + void queryClient.invalidateQueries({ queryKey: ['automations', 'unread-count'] }); + }, [updateAsync, queryClient]); +} diff --git a/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx b/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx new file mode 100644 index 0000000000..95768476cd --- /dev/null +++ b/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx @@ -0,0 +1,85 @@ +import { CheckCheck, Clock } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useState } from 'react'; +import { + useAutomationUnreadCount, + useMarkAutomationsRead, +} from '@renderer/features/automations/use-automation-unread-count'; +import { + isCurrentView, + useNavigate, + useWorkspaceSlots, +} from '@renderer/lib/layout/navigation-provider'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from '@renderer/lib/ui/context-menu'; +import { cn } from '@renderer/utils/utils'; +import { SidebarMenuAction, SidebarMenuRow } from './sidebar-primitives'; + +function formatUnreadCount(count: number): string { + if (count > 99) return '99+'; + return String(count); +} + +export const AutomationsSidebarItem = observer(function AutomationsSidebarItem() { + const { navigate } = useNavigate(); + const { currentView } = useWorkspaceSlots(); + const unreadCount = useAutomationUnreadCount(); + const markAsRead = useMarkAutomationsRead(); + const isActive = isCurrentView(currentView, 'automations'); + const [menuOpen, setMenuOpen] = useState(false); + + useEffect(() => { + if (unreadCount === 0) setMenuOpen(false); + }, [unreadCount]); + + async function handleMarkAsRead() { + setMenuOpen(false); + await markAsRead(); + } + + return ( + { + if (open && unreadCount === 0) return; + setMenuOpen(open); + }} + > + + e.preventDefault()} + onClick={() => navigate('automations')} + > + + + Automations + + {unreadCount > 0 ? ( + + {formatUnreadCount(unreadCount)} + + ) : null} + + + + void handleMarkAsRead()}> + + Mark all as read + + + + ); +}); diff --git a/apps/emdash-desktop/src/renderer/features/sidebar/left-sidebar.tsx b/apps/emdash-desktop/src/renderer/features/sidebar/left-sidebar.tsx index 676bd7c987..73744ea446 100644 --- a/apps/emdash-desktop/src/renderer/features/sidebar/left-sidebar.tsx +++ b/apps/emdash-desktop/src/renderer/features/sidebar/left-sidebar.tsx @@ -1,4 +1,4 @@ -import { Clock, FolderInput, Library, MessageSquareShare, Settings } from 'lucide-react'; +import { FolderInput, Library, MessageSquareShare, Settings } from 'lucide-react'; import { observer } from 'mobx-react-lite'; import React from 'react'; import { @@ -9,6 +9,7 @@ import { import { useShowModal } from '@renderer/lib/modal/modal-provider'; import { BoundShortcut } from '@renderer/lib/ui/shortcut'; import { cn } from '@renderer/utils/utils'; +import { AutomationsSidebarItem } from './automations-sidebar-item'; import { SidebarPinnedTaskList } from './pinned-task-list'; import { ProjectsGroupLabel } from './projects-group-label'; import { @@ -66,17 +67,7 @@ export const LeftSidebar: React.FC = observer(function LeftSidebar() { - navigate('automations')} - aria-label="Automations" - className="w-full justify-between" - > - - - Automations - - + Date: Tue, 23 Jun 2026 21:22:27 +0200 Subject: [PATCH 2/4] fix(automations): baseline unread notifications --- .../core/automations/automations-service.ts | 27 +++++++++++++------ .../src/main/core/automations/controller.ts | 2 ++ .../use-automation-unread-count.ts | 4 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/automations/automations-service.ts b/apps/emdash-desktop/src/main/core/automations/automations-service.ts index f62b83c044..de49456175 100644 --- a/apps/emdash-desktop/src/main/core/automations/automations-service.ts +++ b/apps/emdash-desktop/src/main/core/automations/automations-service.ts @@ -1,4 +1,4 @@ -import { and, asc, count, desc, eq, gt, inArray, ne, sql } from 'drizzle-orm'; +import { and, asc, count, desc, eq, gt, inArray, isNotNull, ne, sql } from 'drizzle-orm'; import { db } from '@main/db/client'; import { automationRuns, tasks } from '@main/db/schema'; import { events } from '@main/lib/events'; @@ -31,6 +31,15 @@ import { import { markRunSkipped } from './run-transitions'; import { mapAutomationRunRowToAutomationRun } from './utils'; +const notificationRunStatuses = ['done', 'failed', 'skipped']; + +function notificationRunPredicate() { + return and( + inArray(automationRuns.status, notificationRunStatuses), + isNotNull(automationRuns.finishedAt) + ); +} + export type AutomationsServiceHooks = { 'automation:created': (automation: Automation) => void | Promise; 'automation:updated': (automation: Automation) => void | Promise; @@ -151,17 +160,19 @@ export class AutomationsService implements Hookable { return rows.map(({ run, taskId }) => mapAutomationRunRowToAutomationRun(run, taskId)); } + async getNotificationsBaselineTimestamp(): Promise { + const [row] = await db + .select({ ts: sql`MAX(${automationRuns.finishedAt})` }) + .from(automationRuns) + .where(notificationRunPredicate()); + return row?.ts ?? Date.now(); + } + async countUnreadFinishedRuns(sinceMs: number): Promise { - const finishedAt = sql`COALESCE(${automationRuns.finishedAt}, ${automationRuns.startedAt}, ${automationRuns.scheduledAt})`; const [result] = await db .select({ count: count() }) .from(automationRuns) - .where( - and( - inArray(automationRuns.status, ['done', 'failed', 'skipped']), - gt(finishedAt, sinceMs) - ) - ); + .where(and(notificationRunPredicate(), gt(automationRuns.finishedAt, sinceMs))); return result?.count ?? 0; } diff --git a/apps/emdash-desktop/src/main/core/automations/controller.ts b/apps/emdash-desktop/src/main/core/automations/controller.ts index 8fd3cba871..c8ba61b8b5 100644 --- a/apps/emdash-desktop/src/main/core/automations/controller.ts +++ b/apps/emdash-desktop/src/main/core/automations/controller.ts @@ -12,6 +12,8 @@ export const automationsController = createRPCController({ countAutomationRunsByStatus: automationsService.countAutomationRunsByStatus.bind(automationsService), countUnreadFinishedRuns: automationsService.countUnreadFinishedRuns.bind(automationsService), + getNotificationsBaselineTimestamp: + automationsService.getNotificationsBaselineTimestamp.bind(automationsService), getLatestRun: automationsService.getLatestRun.bind(automationsService), getNextScheduledRun: automationsService.getNextScheduledRun.bind(automationsService), runAutomation: automationsService.runAutomation.bind(automationsService), diff --git a/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts index 77c4643141..59933b9f8f 100644 --- a/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts +++ b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts @@ -14,7 +14,9 @@ export function useAutomationUnreadCount() { useEffect(() => { if (lastReadAt !== 0 || interfaceSettings === undefined) return; - void updateAsync({ automationsLastReadAt: Date.now() }); + void rpc.automations.getNotificationsBaselineTimestamp().then((baseline) => { + void updateAsync({ automationsLastReadAt: baseline }); + }); }, [interfaceSettings, lastReadAt, updateAsync]); const query = useQuery({ From b849c3df5ba1f3fa6f3b285abe2cd1c676af4be1 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:27:08 +0200 Subject: [PATCH 3/4] fix(automations): refresh unread badge --- .../core/automations/automations-service.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/automations/automations-service.ts b/apps/emdash-desktop/src/main/core/automations/automations-service.ts index de49456175..0603956e3c 100644 --- a/apps/emdash-desktop/src/main/core/automations/automations-service.ts +++ b/apps/emdash-desktop/src/main/core/automations/automations-service.ts @@ -58,13 +58,21 @@ export class AutomationsService implements Hookable { private readonly scheduler = new AutomationScheduler({ onRunStep: (run) => { this._hooks.callHookBackground('run:step-completed', run); - events.emit(automationRunChangedChannel, { automationId: run.automationId, run }); + this.emitRunChanged(run); }, onScheduledRunChanged: (automationId) => { events.emit(automationChangedChannel, { automationId }); }, }); + private emitRunChanged(run: AutomationRun): void { + events.emit(automationRunChangedChannel, { automationId: run.automationId, run }); + } + + private emitRunChanges(runs: AutomationRun[]): void { + for (const run of runs) this.emitRunChanged(run); + } + on(name: K, handler: AutomationsServiceHooks[K]) { return this._hooks.on(name, handler); } @@ -100,7 +108,8 @@ export class AutomationsService implements Hookable { const automation = await updateSettingsInRepo(id, patch); if (!automation) throw new Error('automation_not_found'); if (patch.triggerConfig !== undefined) { - await skipQueuedCronRuns(id, 'trigger_changed'); + const skippedRuns = await skipQueuedCronRuns(id, 'trigger_changed'); + this.emitRunChanges(skippedRuns); if (automation.enabled) { await ensureNextCronRun(automation); events.emit(automationChangedChannel, { automationId: id }); @@ -130,7 +139,8 @@ export class AutomationsService implements Hookable { if (enabled) { await ensureNextCronRun(automation); } else { - await skipQueuedCronRuns(id, 'disabled'); + const skippedRuns = await skipQueuedCronRuns(id, 'disabled'); + this.emitRunChanges(skippedRuns); } this._hooks.callHookBackground('automation:enabled', automation); events.emit(automationChangedChannel, { automationId: id }); @@ -263,6 +273,7 @@ export class AutomationsService implements Hookable { } this._hooks.callHookBackground('run:stopped', stopped); this._hooks.callHookBackground('run:step-completed', stopped); + this.emitRunChanged(stopped); return stopped; } @@ -271,7 +282,8 @@ export class AutomationsService implements Hookable { } async deleteAutomation(id: string): Promise { - await skipQueuedCronRuns(id, 'automation_deleted'); + const skippedRuns = await skipQueuedCronRuns(id, 'automation_deleted'); + this.emitRunChanges(skippedRuns); const deleted = await softDeleteAutomation(id); if (!deleted) throw new Error('automation_not_found'); this._hooks.callHookBackground('automation:deleted', id); From fd57d657d9d61a950c3bd8926e145cac70b6c117 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:06:20 +0200 Subject: [PATCH 4/4] fix(automations): refine unread run badges --- .../core/automations/automations-service.ts | 44 ++++++++++++++++--- .../use-automation-unread-count.ts | 4 +- .../sidebar/automations-sidebar-item.tsx | 13 +++++- .../shared/core/automations/automation-run.ts | 16 +++++++ 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/apps/emdash-desktop/src/main/core/automations/automations-service.ts b/apps/emdash-desktop/src/main/core/automations/automations-service.ts index 0603956e3c..4e7583d4ba 100644 --- a/apps/emdash-desktop/src/main/core/automations/automations-service.ts +++ b/apps/emdash-desktop/src/main/core/automations/automations-service.ts @@ -1,4 +1,17 @@ -import { and, asc, count, desc, eq, gt, inArray, isNotNull, ne, sql } from 'drizzle-orm'; +import { + and, + asc, + count, + desc, + eq, + gt, + inArray, + isNotNull, + isNull, + ne, + or, + sql, +} from 'drizzle-orm'; import { db } from '@main/db/client'; import { automationRuns, tasks } from '@main/db/schema'; import { events } from '@main/lib/events'; @@ -9,7 +22,11 @@ import type { CreateAutomationParams, UpdateAutomationSettingsPatch, } from '@shared/core/automations/automation'; -import type { AutomationRun } from '@shared/core/automations/automation-run'; +import { + AUTOMATION_SKIP_CODES_EXCLUDED_FROM_NOTIFICATIONS, + TERMINAL_AUTOMATION_RUN_STATUSES, + type AutomationRun, +} from '@shared/core/automations/automation-run'; import { automationChangedChannel, automationRunChangedChannel, @@ -31,15 +48,24 @@ import { import { markRunSkipped } from './run-transitions'; import { mapAutomationRunRowToAutomationRun } from './utils'; -const notificationRunStatuses = ['done', 'failed', 'skipped']; - function notificationRunPredicate() { return and( - inArray(automationRuns.status, notificationRunStatuses), + inArray(automationRuns.status, [...TERMINAL_AUTOMATION_RUN_STATUSES]), isNotNull(automationRuns.finishedAt) ); } +function isExcludedFromUnreadNotifications() { + const excludedInList = AUTOMATION_SKIP_CODES_EXCLUDED_FROM_NOTIFICATIONS.map( + (code) => `'${code}'` + ).join(', '); + return or( + ne(automationRuns.status, 'skipped'), + isNull(sql`json_extract(${automationRuns.error}, '$.code')`), + sql`json_extract(${automationRuns.error}, '$.code') NOT IN (${sql.raw(excludedInList)})` + ); +} + export type AutomationsServiceHooks = { 'automation:created': (automation: Automation) => void | Promise; 'automation:updated': (automation: Automation) => void | Promise; @@ -182,7 +208,13 @@ export class AutomationsService implements Hookable { const [result] = await db .select({ count: count() }) .from(automationRuns) - .where(and(notificationRunPredicate(), gt(automationRuns.finishedAt, sinceMs))); + .where( + and( + notificationRunPredicate(), + isExcludedFromUnreadNotifications(), + gt(automationRuns.finishedAt, sinceMs) + ) + ); return result?.count ?? 0; } diff --git a/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts index 59933b9f8f..1cb339cedb 100644 --- a/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts +++ b/apps/emdash-desktop/src/renderer/features/automations/use-automation-unread-count.ts @@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect } from 'react'; import { useAppSettingsKey } from '@renderer/features/settings/use-app-settings-key'; import { events, rpc } from '@renderer/lib/ipc'; +import { isTerminalAutomationRunStatus } from '@shared/core/automations/automation-run'; import { automationRunChangedChannel } from '@shared/core/automations/automationEvents'; export const automationUnreadCountQueryKey = (lastReadAt: number) => @@ -26,7 +27,8 @@ export function useAutomationUnreadCount() { }); useEffect(() => { - return events.on(automationRunChangedChannel, () => { + return events.on(automationRunChangedChannel, ({ run }) => { + if (!isTerminalAutomationRunStatus(run.status)) return; void queryClient.invalidateQueries({ queryKey: ['automations', 'unread-count'] }); }); }, [queryClient]); diff --git a/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx b/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx index 95768476cd..508c165771 100644 --- a/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx +++ b/apps/emdash-desktop/src/renderer/features/sidebar/automations-sidebar-item.tsx @@ -5,6 +5,7 @@ import { useAutomationUnreadCount, useMarkAutomationsRead, } from '@renderer/features/automations/use-automation-unread-count'; +import { toast } from '@renderer/lib/hooks/use-toast'; import { isCurrentView, useNavigate, @@ -37,8 +38,16 @@ export const AutomationsSidebarItem = observer(function AutomationsSidebarItem() }, [unreadCount]); async function handleMarkAsRead() { - setMenuOpen(false); - await markAsRead(); + try { + await markAsRead(); + setMenuOpen(false); + } catch { + toast({ + title: 'Could not mark as read', + description: 'Your read state could not be saved. Please try again.', + variant: 'destructive', + }); + } } return ( diff --git a/apps/emdash-desktop/src/shared/core/automations/automation-run.ts b/apps/emdash-desktop/src/shared/core/automations/automation-run.ts index 3e9b58794d..f64b9726de 100644 --- a/apps/emdash-desktop/src/shared/core/automations/automation-run.ts +++ b/apps/emdash-desktop/src/shared/core/automations/automation-run.ts @@ -18,6 +18,22 @@ export type RunError = { message?: string; // supplementary context (branch name, timeout ms, etc.) }; +export const TERMINAL_AUTOMATION_RUN_STATUSES = ['done', 'failed', 'skipped'] as const; + +export type TerminalAutomationRunStatus = (typeof TERMINAL_AUTOMATION_RUN_STATUSES)[number]; + +export function isTerminalAutomationRunStatus( + status: AutomationRunStatus +): status is TerminalAutomationRunStatus { + return (TERMINAL_AUTOMATION_RUN_STATUSES as readonly AutomationRunStatus[]).includes(status); +} + +/** Skip reasons that reflect user-initiated lifecycle changes, not actionable run outcomes. */ +export const AUTOMATION_SKIP_CODES_EXCLUDED_FROM_NOTIFICATIONS = [ + 'disabled', + 'automation_deleted', +] as const; + export type AutomationRun = { id: string; automationId: string;