Skip to content

Commit 68d32fe

Browse files
committed
feat(chat): add scheduled tasks as a viewable resource
Adds a new `scheduledtask` resource type so a scheduled task can be opened and viewed as a tab in Chat: - registry config (Calendar icon, label, tab/dropdown renderers) - add-resource dropdown listing (workspace jobs) - embedded detail viewer (status, schedule, next/last run, prompt, recent runs) - "Open in scheduled tasks" action + query invalidation - contract enum + route validation so the resource persists Agent-side mention/OpenResource wiring is left as a follow-up.
1 parent 3fe061e commit 68d32fe

6 files changed

Lines changed: 190 additions & 1 deletion

File tree

apps/sim/app/api/copilot/chat/resources/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ const VALID_RESOURCE_TYPES = new Set<ResourceType>([
2727
'workflow',
2828
'knowledgebase',
2929
'folder',
30+
'scheduledtask',
3031
'log',
3132
'integration',
3233
])
33-
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log'])
34+
const GENERIC_TITLES = new Set([
35+
'Table',
36+
'File',
37+
'Workflow',
38+
'Knowledge Base',
39+
'Folder',
40+
'Scheduled Task',
41+
'Log',
42+
])
3443

3544
export const POST = withRouteHandler(async (req: NextRequest) => {
3645
try {

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useMemo, useState } from 'react'
4+
import { truncate } from '@sim/utils/string'
45
import {
56
Button,
67
DropdownMenu,
@@ -30,6 +31,7 @@ import { useFolders } from '@/hooks/queries/folders'
3031
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
3132
import { useLogsList } from '@/hooks/queries/logs'
3233
import { useMothershipChats } from '@/hooks/queries/mothership-chats'
34+
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
3335
import { useTablesList } from '@/hooks/queries/tables'
3436
import { useWorkflows } from '@/hooks/queries/workflows'
3537
import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders'
@@ -77,6 +79,7 @@ export function useAvailableResources(
7779
const { data: folders = [] } = useFolders(workspaceId)
7880
const { data: fileFolders = [] } = useWorkspaceFileFolders(workspaceId)
7981
const { data: tasks = [] } = useMothershipChats(workspaceId)
82+
const { data: schedules = [] } = useWorkspaceSchedules(workspaceId)
8083
const { data: logsData } = useLogsList(workspaceId, LOG_DROPDOWN_FILTERS)
8184
const logs = useMemo(() => (logsData?.pages ?? []).flatMap((page) => page.logs), [logsData])
8285

@@ -155,6 +158,16 @@ export function useAvailableResources(
155158
isOpen: existingKeys.has(`task:${t.id}`),
156159
})),
157160
},
161+
{
162+
type: 'scheduledtask' as const,
163+
items: schedules
164+
.filter((s) => s.sourceType === 'job')
165+
.map((s) => ({
166+
id: s.id,
167+
name: s.jobTitle || truncate(s.prompt ?? '', 40) || 'Scheduled Task',
168+
isOpen: existingKeys.has(`scheduledtask:${s.id}`),
169+
})),
170+
},
158171
{
159172
type: 'log' as const,
160173
items: logs.map((log) => {
@@ -179,6 +192,7 @@ export function useAvailableResources(
179192
files,
180193
knowledgeBases,
181194
tasks,
195+
schedules,
182196
logs,
183197
existingKeys,
184198
excludeTypes,

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { format } from 'date-fns'
56
import { useRouter } from 'next/navigation'
67
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
78
import {
9+
Calendar,
810
Download,
911
FileX,
1012
Folder as FolderIcon,
@@ -24,6 +26,7 @@ import {
2426
import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
2527
import { triggerFileDownload } from '@/lib/uploads/client/download'
2628
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
29+
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
2730
import {
2831
FileViewer,
2932
type PreviewMode,
@@ -50,6 +53,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
5053
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
5154
import { useFolders } from '@/hooks/queries/folders'
5255
import { useLogDetail } from '@/hooks/queries/logs'
56+
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
5357
import { downloadTableExport } from '@/hooks/queries/tables'
5458
import { useWorkflows } from '@/hooks/queries/workflows'
5559
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
@@ -182,6 +186,15 @@ export const ResourceContent = memo(function ResourceContent({
182186
case 'folder':
183187
return <EmbeddedFolder key={resource.id} workspaceId={workspaceId} folderId={resource.id} />
184188

189+
case 'scheduledtask':
190+
return (
191+
<EmbeddedScheduledTask
192+
key={resource.id}
193+
workspaceId={workspaceId}
194+
scheduleId={resource.id}
195+
/>
196+
)
197+
185198
case 'log':
186199
return (
187200
<EmbeddedLog
@@ -233,6 +246,8 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
233246
)
234247
case 'log':
235248
return <EmbeddedLogActions workspaceId={workspaceId} logId={resource.id} />
249+
case 'scheduledtask':
250+
return <EmbeddedScheduledTaskActions workspaceId={workspaceId} />
236251
case 'folder':
237252
case 'generic':
238253
return null
@@ -647,6 +662,141 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
647662
)
648663
}
649664

665+
const SCHEDULE_STATUS_LABEL: Record<string, string> = {
666+
active: 'Active',
667+
disabled: 'Paused',
668+
completed: 'Completed',
669+
}
670+
671+
function formatScheduleInstant(iso: string | null): string {
672+
if (!iso) return '—'
673+
const date = new Date(iso)
674+
return Number.isNaN(date.getTime()) ? '—' : format(date, "EEE, MMM d 'at' h:mm a")
675+
}
676+
677+
interface ScheduledTaskFieldProps {
678+
title: string
679+
value: string
680+
}
681+
682+
function ScheduledTaskField({ title, value }: ScheduledTaskFieldProps) {
683+
return (
684+
<div className='flex flex-col gap-1'>
685+
<span className='text-[var(--text-muted)] text-caption'>{title}</span>
686+
<span className='text-[var(--text-body)] text-small'>{value}</span>
687+
</div>
688+
)
689+
}
690+
691+
interface EmbeddedScheduledTaskProps {
692+
workspaceId: string
693+
scheduleId: string
694+
}
695+
696+
function EmbeddedScheduledTask({ workspaceId, scheduleId }: EmbeddedScheduledTaskProps) {
697+
const { data: schedules = [], isLoading } = useWorkspaceSchedules(workspaceId)
698+
const schedule = useMemo(
699+
() => schedules.find((s) => s.id === scheduleId),
700+
[schedules, scheduleId]
701+
)
702+
703+
if (isLoading && !schedule) return LOADING_SKELETON
704+
705+
if (!schedule) {
706+
return (
707+
<div className='flex h-full flex-col items-center justify-center gap-3'>
708+
<Calendar className='size-[32px] text-[var(--text-icon)]' />
709+
<div className='flex flex-col items-center gap-1'>
710+
<h2 className='font-medium text-[20px] text-[var(--text-primary)]'>
711+
Scheduled task not found
712+
</h2>
713+
<p className='text-[var(--text-body)] text-small'>
714+
This scheduled task may have been deleted
715+
</p>
716+
</div>
717+
</div>
718+
)
719+
}
720+
721+
const title = schedule.jobTitle || schedule.prompt || 'Scheduled task'
722+
const timing = schedule.cronExpression
723+
? parseCronToHumanReadable(schedule.cronExpression, schedule.timezone)
724+
: 'Runs once'
725+
const status = SCHEDULE_STATUS_LABEL[schedule.status] ?? schedule.status
726+
727+
return (
728+
<div className='flex h-full flex-col gap-6 overflow-y-auto p-6'>
729+
<div className='flex items-center gap-2'>
730+
<Calendar className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
731+
<h2 className='truncate font-medium text-[16px] text-[var(--text-primary)]'>{title}</h2>
732+
</div>
733+
734+
<div className='grid grid-cols-2 gap-4'>
735+
<ScheduledTaskField title='Status' value={status} />
736+
<ScheduledTaskField title='Schedule' value={timing} />
737+
<ScheduledTaskField title='Next run' value={formatScheduleInstant(schedule.nextRunAt)} />
738+
<ScheduledTaskField title='Last run' value={formatScheduleInstant(schedule.lastRanAt)} />
739+
</div>
740+
741+
<div className='flex flex-col gap-1'>
742+
<span className='text-[var(--text-muted)] text-caption'>Prompt</span>
743+
<p className='whitespace-pre-wrap text-[var(--text-body)] text-small'>
744+
{schedule.prompt || '—'}
745+
</p>
746+
</div>
747+
748+
{schedule.jobHistory && schedule.jobHistory.length > 0 && (
749+
<div className='flex flex-col gap-2'>
750+
<span className='text-[var(--text-muted)] text-caption'>Recent runs</span>
751+
<div className='flex flex-col gap-2'>
752+
{schedule.jobHistory.slice(0, 5).map((run) => (
753+
<div
754+
key={run.timestamp}
755+
className='flex flex-col gap-1 rounded-[6px] bg-[var(--surface-4)] px-3 py-2'
756+
>
757+
<span className='text-[var(--text-tertiary)] text-caption'>
758+
{formatScheduleInstant(run.timestamp)}
759+
</span>
760+
<span className='text-[var(--text-body)] text-small'>{run.summary}</span>
761+
</div>
762+
))}
763+
</div>
764+
</div>
765+
)}
766+
</div>
767+
)
768+
}
769+
770+
interface EmbeddedScheduledTaskActionsProps {
771+
workspaceId: string
772+
}
773+
774+
function EmbeddedScheduledTaskActions({ workspaceId }: EmbeddedScheduledTaskActionsProps) {
775+
const router = useRouter()
776+
777+
const handleOpenScheduledTasks = () => {
778+
router.push(`/workspace/${workspaceId}/scheduled-tasks`)
779+
}
780+
781+
return (
782+
<Tooltip.Root>
783+
<Tooltip.Trigger asChild>
784+
<Button
785+
variant='subtle'
786+
onClick={handleOpenScheduledTasks}
787+
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
788+
aria-label='Open in scheduled tasks'
789+
>
790+
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
791+
</Button>
792+
</Tooltip.Trigger>
793+
<Tooltip.Content side='bottom'>
794+
<p>Open in scheduled tasks</p>
795+
</Tooltip.Content>
796+
</Tooltip.Root>
797+
)
798+
}
799+
650800
interface EmbeddedLogProps {
651801
workspaceId: string
652802
logId: string

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { ElementType, ReactNode } from 'react'
44
import type { QueryClient } from '@tanstack/react-query'
55
import {
6+
Calendar,
67
Connections,
78
Database,
89
File as FileIcon,
@@ -23,6 +24,7 @@ import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
2324
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
2425
import { logKeys } from '@/hooks/queries/logs'
2526
import { mothershipChatKeys } from '@/hooks/queries/mothership-chats'
27+
import { scheduleKeys } from '@/hooks/queries/schedules'
2628
import { tableKeys } from '@/hooks/queries/tables'
2729
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
2830
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
@@ -183,6 +185,15 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
183185
),
184186
renderDropdownItem: (props) => <DefaultDropdownItem {...props} />,
185187
},
188+
scheduledtask: {
189+
type: 'scheduledtask',
190+
label: 'Scheduled Tasks',
191+
icon: Calendar,
192+
renderTabIcon: (_resource, className) => (
193+
<Calendar className={cn(className, 'text-[var(--text-icon)]')} />
194+
),
195+
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Calendar} />,
196+
},
186197
log: {
187198
type: 'log',
188199
label: 'Logs',
@@ -241,6 +252,9 @@ const RESOURCE_INVALIDATORS: Record<
241252
task: (qc, wId) => {
242253
qc.invalidateQueries({ queryKey: mothershipChatKeys.list(wId) })
243254
},
255+
scheduledtask: (qc, wId) => {
256+
qc.invalidateQueries({ queryKey: scheduleKeys.list(wId) })
257+
},
244258
log: (qc, _wId, id) => {
245259
qc.invalidateQueries({ queryKey: logKeys.details() })
246260
qc.invalidateQueries({ queryKey: logKeys.detail(id) })

apps/sim/lib/api/contracts/copilot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const copilotResourceTypeSchema = z.enum([
9292
'workflow',
9393
'knowledgebase',
9494
'folder',
95+
'scheduledtask',
9596
'log',
9697
])
9798

apps/sim/lib/copilot/resources/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const MothershipResourceType = {
66
folder: 'folder',
77
filefolder: 'filefolder',
88
task: 'task',
9+
scheduledtask: 'scheduledtask',
910
log: 'log',
1011
integration: 'integration',
1112
generic: 'generic',

0 commit comments

Comments
 (0)