diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index 5071d925..d5a8b603 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, isNotNull, isNull, lte, ne, or } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, @@ -1030,8 +1030,8 @@ export function routineService(db: Db) { eq(routineTriggers.kind, "schedule"), eq(routineTriggers.enabled, true), eq(routines.status, "active"), - sql`${routineTriggers.nextRunAt} is not null`, - sql`${routineTriggers.nextRunAt} <= ${now}`, + isNotNull(routineTriggers.nextRunAt), + lte(routineTriggers.nextRunAt, now), ), ) .orderBy(asc(routineTriggers.nextRunAt), asc(routineTriggers.createdAt)); diff --git a/ui/src/pages/RoutineDetail.tsx b/ui/src/pages/RoutineDetail.tsx index 46bbb5c3..c8716b54 100644 --- a/ui/src/pages/RoutineDetail.tsx +++ b/ui/src/pages/RoutineDetail.tsx @@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { + ChevronDown, + ChevronRight, Clock3, Copy, Play, @@ -21,12 +23,16 @@ import { useToast } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; +import { AgentIcon } from "../components/AgentIconPicker"; import { IssueRow } from "../components/IssueRow"; +import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector"; +import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor"; +import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, @@ -68,6 +74,12 @@ type SecretMessage = { webhookSecret: string; }; +function autoResizeTextarea(element: HTMLTextAreaElement | null) { + if (!element) return; + element.style.height = "auto"; + element.style.height = `${element.scrollHeight}px`; +} + function isRoutineTab(value: string | null): value is RoutineTab { return value !== null && routineTabs.includes(value as RoutineTab); } @@ -244,7 +256,12 @@ export function RoutineDetail() { const location = useLocation(); const { pushToast } = useToast(); const hydratedRoutineIdRef = useRef(null); + const titleInputRef = useRef(null); + const descriptionEditorRef = useRef(null); + const assigneeSelectorRef = useRef(null); + const projectSelectorRef = useRef(null); const [secretMessage, setSecretMessage] = useState(null); + const [advancedOpen, setAdvancedOpen] = useState(false); const [newTrigger, setNewTrigger] = useState({ kind: "schedule", label: "", @@ -354,6 +371,10 @@ export function RoutineDetail() { } }, [routine, routineDefaults, isEditDirty, setBreadcrumbs]); + useEffect(() => { + autoResizeTextarea(titleInputRef.current); + }, [editDraft.title, routine?.id]); + const copySecretValue = async (label: string, value: string) => { try { await navigator.clipboard.writeText(value); @@ -504,14 +525,38 @@ export function RoutineDetail() { }, }); - const agentName = useMemo( - () => new Map((agents ?? []).map((agent) => [agent.id, agent.name])), + const agentById = useMemo( + () => new Map((agents ?? []).map((agent) => [agent.id, agent])), [agents], ); - const projectName = useMemo( - () => new Map((projects ?? []).map((project) => [project.id, project.name])), + const projectById = useMemo( + () => new Map((projects ?? []).map((project) => [project.id, project])), [projects], ); + const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [routine?.id]); + const assigneeOptions = useMemo( + () => + sortAgentsByRecency( + (agents ?? []).filter((agent) => agent.status !== "terminated"), + recentAssigneeIds, + ).map((agent) => ({ + id: agent.id, + label: agent.name, + searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, + })), + [agents, recentAssigneeIds], + ); + const projectOptions = useMemo( + () => + (projects ?? []).map((project) => ({ + id: project.id, + label: project.name, + searchText: project.description ?? "", + })), + [projects], + ); + const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null; + const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null; if (!selectedCompanyId) { return ; @@ -566,149 +611,266 @@ export function RoutineDetail() { )} - - -
-
- {routine.title} - - Project {projectName.get(routine.projectId) ?? routine.projectId.slice(0, 8)} ยท Assignee {agentName.get(routine.assigneeAgentId) ?? routine.assigneeAgentId.slice(0, 8)} - -
-
- - {routine.status.replaceAll("_", " ")} - - -
-
-
- -
- - setEditDraft((current) => ({ ...current, title: event.target.value }))} /> -
-
- -