From f6cc881ee0b067507dae353752a8e32fc69caf8d Mon Sep 17 00:00:00 2001 From: Annika <14750837+annikaschmid@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:08:15 +0000 Subject: [PATCH 1/4] codex version that works locally --- .claude/worktrees/gifted-haibt | 1 + .../src/renderer/components/MainLayout.tsx | 5 + .../components/AutomationsView.tsx | 616 ++++++++++++++++++ .../hooks/useAutomationScheduler.ts | 168 +++++ .../automations/stores/automationStore.ts | 252 +++++++ .../features/automations/templates.ts | 34 + .../features/automations/utils/schedule.ts | 156 +++++ .../sidebar/components/SidebarMenu.tsx | 19 + .../components/items/AutomationsItem.tsx | 40 ++ .../features/sidebar/hooks/useSidebarData.ts | 6 +- .../src/renderer/stores/navigationStore.ts | 11 +- apps/code/src/shared/types/automations.ts | 43 ++ 12 files changed, 1349 insertions(+), 2 deletions(-) create mode 160000 .claude/worktrees/gifted-haibt create mode 100644 apps/code/src/renderer/features/automations/components/AutomationsView.tsx create mode 100644 apps/code/src/renderer/features/automations/hooks/useAutomationScheduler.ts create mode 100644 apps/code/src/renderer/features/automations/stores/automationStore.ts create mode 100644 apps/code/src/renderer/features/automations/templates.ts create mode 100644 apps/code/src/renderer/features/automations/utils/schedule.ts create mode 100644 apps/code/src/renderer/features/sidebar/components/items/AutomationsItem.tsx create mode 100644 apps/code/src/shared/types/automations.ts diff --git a/.claude/worktrees/gifted-haibt b/.claude/worktrees/gifted-haibt new file mode 160000 index 000000000..53ed7f4e2 --- /dev/null +++ b/.claude/worktrees/gifted-haibt @@ -0,0 +1 @@ +Subproject commit 53ed7f4e23ebe2afb197fb2e9adfdb26afb25afb diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index 698574438..78b85cab3 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -4,6 +4,8 @@ import { HedgehogMode } from "@components/HedgehogMode"; import { KeyboardShortcutsSheet } from "@components/KeyboardShortcutsSheet"; import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksView"; +import { AutomationsView } from "@features/automations/components/AutomationsView"; +import { useAutomationScheduler } from "@features/automations/hooks/useAutomationScheduler"; import { CommandMenu } from "@features/command/components/CommandMenu"; import { CommandCenterView } from "@features/command-center/components/CommandCenterView"; import { InboxView } from "@features/inbox/components/InboxView"; @@ -42,6 +44,7 @@ export function MainLayout() { useIntegrations(); useTaskDeepLink(); + useAutomationScheduler(); useEffect(() => { if (tasks) { @@ -80,6 +83,8 @@ export function MainLayout() { {view.type === "command-center" && } + {view.type === "automations" && } + {view.type === "skills" && } diff --git a/apps/code/src/renderer/features/automations/components/AutomationsView.tsx b/apps/code/src/renderer/features/automations/components/AutomationsView.tsx new file mode 100644 index 000000000..b42796cfc --- /dev/null +++ b/apps/code/src/renderer/features/automations/components/AutomationsView.tsx @@ -0,0 +1,616 @@ +import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; +import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker"; +import { useRepositoryIntegration } from "@hooks/useIntegrations"; +import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; +import { + ClockCounterClockwise, + FloppyDisk, + Play, + Plus, + Robot, + Trash, +} from "@phosphor-icons/react"; +import { + Badge, + Box, + Button, + Flex, + ScrollArea, + Separator, + Switch, + Text, + TextArea, + TextField, +} from "@radix-ui/themes"; +import type { Automation } from "@shared/types/automations"; +import { useEffect, useMemo, useState } from "react"; +import { runAutomationNow } from "../hooks/useAutomationScheduler"; +import { useAutomationStore } from "../stores/automationStore"; +import { AUTOMATION_TEMPLATES } from "../templates"; +import { formatAutomationDateTime, getLocalTimezone } from "../utils/schedule"; + +interface AutomationDraft { + name: string; + prompt: string; + repoPath: string; + repository: string | null; + githubIntegrationId: number | null; + scheduleTime: string; + templateId: string | null; +} + +function toDraft( + automation?: Automation | null, + githubIntegrationId?: number | null, +): AutomationDraft { + if (!automation) { + return { + name: "", + prompt: "", + repoPath: "", + repository: null, + githubIntegrationId: githubIntegrationId ?? null, + scheduleTime: "09:00", + templateId: null, + }; + } + + return { + name: automation.name, + prompt: automation.prompt, + repoPath: automation.repoPath, + repository: automation.repository ?? null, + githubIntegrationId: + automation.githubIntegrationId ?? githubIntegrationId ?? null, + scheduleTime: automation.scheduleTime, + templateId: automation.templateId ?? null, + }; +} + +function AutomationStatusBadge({ automation }: { automation: Automation }) { + if (!automation.enabled) { + return ( + + Paused + + ); + } + + if (automation.lastRunStatus === "failed") { + return ( + + Failed + + ); + } + + if (automation.lastRunStatus === "success") { + return ( + + Healthy + + ); + } + + return ( + + Active + + ); +} + +export function AutomationsView() { + const automations = useAutomationStore((state) => state.automations); + const selectedAutomationId = useAutomationStore( + (state) => state.selectedAutomationId, + ); + const runningAutomationIds = useAutomationStore( + (state) => state.runningAutomationIds, + ); + const setSelectedAutomationId = useAutomationStore( + (state) => state.setSelectedAutomationId, + ); + const createAutomation = useAutomationStore( + (state) => state.createAutomation, + ); + const updateAutomation = useAutomationStore( + (state) => state.updateAutomation, + ); + const deleteAutomation = useAutomationStore( + (state) => state.deleteAutomation, + ); + const toggleAutomation = useAutomationStore( + (state) => state.toggleAutomation, + ); + + const { githubIntegration, repositories, isLoadingRepos } = + useRepositoryIntegration(); + + const selectedAutomation = useMemo( + () => + automations.find( + (automation) => automation.id === selectedAutomationId, + ) ?? null, + [automations, selectedAutomationId], + ); + + const [isCreating, setIsCreating] = useState(automations.length === 0); + const [draft, setDraft] = useState(() => + toDraft(null, githubIntegration?.id), + ); + + useEffect(() => { + if (!isCreating && selectedAutomation) { + setDraft(toDraft(selectedAutomation, githubIntegration?.id)); + } + }, [isCreating, selectedAutomation, githubIntegration?.id]); + + const headerContent = useMemo( + () => ( + + + + Automations + + + ), + [], + ); + + useSetHeaderContent(headerContent); + + const openCreate = () => { + setIsCreating(true); + setSelectedAutomationId(null); + setDraft(toDraft(null, githubIntegration?.id)); + }; + + const openExisting = (automation: Automation) => { + setIsCreating(false); + setSelectedAutomationId(automation.id); + setDraft(toDraft(automation, githubIntegration?.id)); + }; + + const applyTemplate = (templateId: string) => { + const template = AUTOMATION_TEMPLATES.find( + (item) => item.id === templateId, + ); + if (!template) { + return; + } + + setDraft((current) => ({ + ...current, + name: current.name || template.name, + prompt: template.prompt, + templateId: template.id, + })); + }; + + const handleSave = () => { + if (!draft.name.trim() || !draft.prompt.trim() || !draft.repoPath.trim()) { + return; + } + + if (isCreating || !selectedAutomation) { + const automationId = createAutomation({ + name: draft.name, + prompt: draft.prompt, + repoPath: draft.repoPath, + repository: draft.repository, + githubIntegrationId: draft.githubIntegrationId, + scheduleTime: draft.scheduleTime, + templateId: draft.templateId, + }); + const created = useAutomationStore + .getState() + .automations.find((item) => item.id === automationId); + if (created) { + openExisting(created); + } + return; + } + + updateAutomation(selectedAutomation.id, { + name: draft.name, + prompt: draft.prompt, + repoPath: draft.repoPath, + repository: draft.repository, + githubIntegrationId: draft.githubIntegrationId, + scheduleTime: draft.scheduleTime, + templateId: draft.templateId, + }); + }; + + const handleDelete = () => { + if (!selectedAutomation) { + return; + } + deleteAutomation(selectedAutomation.id); + openCreate(); + }; + + const enabledCount = automations.filter( + (automation) => automation.enabled, + ).length; + const timezone = getLocalTimezone(); + + return ( + + + + {automations.length} automation{automations.length === 1 ? "" : "s"} + + + {enabledCount} enabled + +
+ + + + + + + + {automations.length === 0 ? ( + + + + No automations yet + + + ) : ( + automations.map((automation) => { + const isSelected = + !isCreating && selectedAutomation?.id === automation.id; + + return ( + + ); + }) + )} + + + + + + + + + + {isCreating + ? "New automation" + : (selectedAutomation?.name ?? "Automation")} + + + Runs locally on this app while it is open. Missed runs are + skipped. + + + + + + Template library + + + {AUTOMATION_TEMPLATES.map((template) => ( + + + + + {template.name} + + + {template.category} + + + + {template.description} + + + + + ))} + + + + + + + + + Name + + + setDraft((current) => ({ + ...current, + name: event.target.value, + })) + } + placeholder="Morning repo check" + /> + + + + + Prompt + +