Skip to content

Commit 39cfae9

Browse files
authored
improvement(scheduled-tasks): render prompt chips in task details and align weekday picker (#5159)
1 parent 2bbf70e commit 39cfae9

7 files changed

Lines changed: 196 additions & 67 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ export interface PromptEditorProps extends PromptEditorKeyPolicy {
2727
placeholder?: string
2828
/** Focuses the editor (caret at end) on mount. */
2929
autoFocus?: boolean
30+
/**
31+
* Renders the editor as a non-editable display surface: the textarea becomes
32+
* `readOnly` (so the chip overlay still paints `@`-mention / `/`-skill chips
33+
* and the text stays selectable/copyable) and the caret-anchored resource and
34+
* skill menus are not mounted. Use for read-only records — e.g. a finished
35+
* scheduled task — where the prompt should render with chips but not be edited.
36+
*/
37+
readOnly?: boolean
3038
/**
3139
* Layout/sizing only — a height cap (`max-h-[200px]`) or fill (`flex-1`)
3240
* for the scroll container. The text chrome is owned by the editor.
@@ -56,6 +64,7 @@ export function PromptEditor({
5664
editor,
5765
placeholder,
5866
autoFocus = false,
67+
readOnly = false,
5968
className,
6069
'aria-label': ariaLabel,
6170
onSubmit,
@@ -73,22 +82,24 @@ export function PromptEditor({
7382
}, [value, textareaRef])
7483

7584
useEffect(() => {
76-
if (autoFocus) editor.focusAtEnd()
85+
if (autoFocus && !readOnly) editor.focusAtEnd()
7786
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only focus
7887
}, [])
7988

8089
/**
8190
* Clicking the editor's empty regions (padding, space below the last line)
8291
* focuses the textarea; clicks on the textarea itself keep native caret
83-
* placement.
92+
* placement. No-op in read-only mode: the surface is display-only, so a
93+
* padding click should not pull focus onto the non-editable textarea.
8494
*/
8595
const handleSurfaceClick = useCallback(
8696
(e: React.MouseEvent<HTMLDivElement>) => {
97+
if (readOnly) return
8798
if (e.target === textareaRef.current) return
8899
if ((e.target as HTMLElement).closest('button')) return
89100
textareaRef.current?.focus()
90101
},
91-
[textareaRef]
102+
[readOnly, textareaRef]
92103
)
93104

94105
const overlayContent = useMemo(() => {
@@ -167,38 +178,45 @@ export function PromptEditor({
167178
<textarea
168179
ref={textareaRef}
169180
value={value}
170-
onChange={editor.handleInputChange}
171-
onKeyDown={(e) => editor.handleKeyDown(e, { onSubmit, onArrowUpOnEmpty })}
172-
onPaste={editor.handlePaste}
181+
readOnly={readOnly}
182+
onChange={readOnly ? undefined : editor.handleInputChange}
183+
onKeyDown={
184+
readOnly ? undefined : (e) => editor.handleKeyDown(e, { onSubmit, onArrowUpOnEmpty })
185+
}
186+
onPaste={readOnly ? undefined : editor.handlePaste}
173187
onCopy={editor.handleCopy}
174-
onCut={editor.handleCut}
175-
onSelect={editor.handleSelectAdjust}
176-
onMouseUp={editor.handleSelectAdjust}
188+
onCut={readOnly ? undefined : editor.handleCut}
189+
onSelect={readOnly ? undefined : editor.handleSelectAdjust}
190+
onMouseUp={readOnly ? undefined : editor.handleSelectAdjust}
177191
placeholder={placeholder}
178192
aria-label={ariaLabel}
179193
rows={1}
180-
className={TEXTAREA_BASE_CLASSES}
194+
className={cn(TEXTAREA_BASE_CLASSES, readOnly && 'cursor-default caret-transparent')}
181195
/>
182196
</div>
183197

184-
<PlusMenuDropdown
185-
ref={editor.plusMenuRef}
186-
availableResources={editor.availableResources}
187-
onResourceSelect={editor.insertResource}
188-
onClose={editor.handlePlusMenuClose}
189-
textareaRef={editor.textareaRef}
190-
pendingCursorRef={editor.pendingCursorRef}
191-
mentionQuery={editor.mentionQuery ?? undefined}
192-
/>
193-
<SkillsMenuDropdown
194-
ref={editor.skillsMenuRef}
195-
skills={editor.skills}
196-
onSkillSelect={editor.handleSkillSelect}
197-
onClose={editor.handleSkillsMenuClose}
198-
textareaRef={editor.textareaRef}
199-
pendingCursorRef={editor.pendingCursorRef}
200-
slashQuery={editor.slashQuery ?? undefined}
201-
/>
198+
{!readOnly && (
199+
<>
200+
<PlusMenuDropdown
201+
ref={editor.plusMenuRef}
202+
availableResources={editor.availableResources}
203+
onResourceSelect={editor.insertResource}
204+
onClose={editor.handlePlusMenuClose}
205+
textareaRef={editor.textareaRef}
206+
pendingCursorRef={editor.pendingCursorRef}
207+
mentionQuery={editor.mentionQuery ?? undefined}
208+
/>
209+
<SkillsMenuDropdown
210+
ref={editor.skillsMenuRef}
211+
skills={editor.skills}
212+
onSkillSelect={editor.handleSkillSelect}
213+
onClose={editor.handleSkillsMenuClose}
214+
textareaRef={editor.textareaRef}
215+
pendingCursorRef={editor.pendingCursorRef}
216+
slashQuery={editor.slashQuery ?? undefined}
217+
/>
218+
</>
219+
)}
202220
</div>
203221
)
204222
}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ export interface UsePromptEditorProps {
104104
workspaceId: string
105105
/** Initial text. Chipified (`@`-mentions / `/`-skills converted) on mount. */
106106
initialValue?: string
107+
/**
108+
* Contexts to seed the editor with — restored resource mentions (files,
109+
* tables, knowledge) that cannot be recovered from the prompt text alone.
110+
* Seed these rather than calling `setContexts` after mount: the mount
111+
* chipify pass MERGES integration `@`-mentions and `/`-skills on top, so a
112+
* post-mount `setContexts` would clobber those auto-registered contexts.
113+
*/
114+
initialContexts?: ChatContext[]
107115
/**
108116
* Notified when a context is added through an interactive path — a mention
109117
* pick, a resource drop, or a skill pick. Paste re-registration is
@@ -142,6 +150,7 @@ export type PromptEditorInstance = ReturnType<typeof usePromptEditor>
142150
export function usePromptEditor({
143151
workspaceId,
144152
initialValue = '',
153+
initialContexts,
145154
onContextAdd,
146155
onPasteFiles,
147156
}: UsePromptEditorProps) {
@@ -170,7 +179,7 @@ export function usePromptEditor({
170179
const slashRangeRef = useRef<{ start: number; end: number } | null>(null)
171180
const [slashQuery, setSlashQuery] = useState<string | null>(null)
172181

173-
const contextManagement = useContextManagement({ message: value })
182+
const contextManagement = useContextManagement({ message: value, initialContexts })
174183
const contextManagementRef = useRef(contextManagement)
175184
contextManagementRef.current = contextManagement
176185

apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-details-modal/task-details-modal.tsx

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
'use client'
22

33
import { format } from 'date-fns'
4+
import { useParams } from 'next/navigation'
45
import {
56
Calendar,
67
ChipModal,
78
ChipModalBody,
89
ChipModalField,
910
ChipModalFooter,
1011
ChipModalHeader,
12+
chipFieldSurfaceClass,
1113
} from '@/components/emcn'
14+
import { cn } from '@/lib/core/utils/cn'
15+
import {
16+
PromptEditor,
17+
usePromptEditor,
18+
} from '@/app/workspace/[workspaceId]/home/components/user-input/components'
1219
import type {
1320
ScheduledTask,
1421
ScheduledTaskStatus,
@@ -35,7 +42,7 @@ interface TaskDetailsModalProps {
3542
/**
3643
* Read-only record modal for tasks that are running or already finished —
3744
* pending tasks open the edit `TaskModal` instead. Three plaintext fields:
38-
* Status and the run time as copy fields, the prompt as a view-only textarea.
45+
* Status and the run time as copy fields, the prompt as a view-only chip editor.
3946
*/
4047
export function TaskDetailsModal({ task, onClose }: TaskDetailsModalProps) {
4148
return (
@@ -47,23 +54,54 @@ export function TaskDetailsModal({ task, onClose }: TaskDetailsModalProps) {
4754
size='md'
4855
srTitle='Scheduled task'
4956
>
50-
{task && (
51-
<>
52-
<ChipModalHeader icon={Calendar} onClose={onClose}>
53-
Scheduled task
54-
</ChipModalHeader>
55-
<ChipModalBody>
56-
<ChipModalField type='copy' title='Status' value={STATUS_COPY[task.status].label} />
57-
<ChipModalField
58-
type='copy'
59-
title={STATUS_COPY[task.status].timeTitle}
60-
value={format(task.runAt, "EEEE, MMMM d, yyyy 'at' h:mm a")}
61-
/>
62-
<ChipModalField type='textarea' title='Prompt' value={task.prompt} viewOnly />
63-
</ChipModalBody>
64-
<ChipModalFooter onCancel={onClose} primaryAction={{ label: 'Done', onClick: onClose }} />
65-
</>
66-
)}
57+
{/* Key by the occurrence id so switching tasks while the modal stays open
58+
remounts the content — the editor seeds prompt + contexts on mount, so
59+
without a fresh mount it would keep showing the first task's prompt. */}
60+
{task && <TaskDetailsContent key={task.id} task={task} onClose={onClose} />}
6761
</ChipModal>
6862
)
6963
}
64+
65+
/**
66+
* Inner content, mounted only while a task is shown (the Radix portal unmounts
67+
* closed content). Holding the read-only editor here keeps its mention-data
68+
* queries from firing on page load and re-seeds from the task on each open.
69+
*/
70+
function TaskDetailsContent({ task, onClose }: { task: ScheduledTask; onClose: () => void }) {
71+
const { workspaceId } = useParams<{ workspaceId: string }>()
72+
/**
73+
* Seed the stored resource mentions (files, tables, knowledge) as the editor's
74+
* initial contexts — these can't be recovered from the prompt text alone. The
75+
* mount chipify pass then merges integration `@`-mentions and `/`-skills on top
76+
* (they DO chipify from text), so the overlay renders the full set. Seeding is
77+
* deliberate over a post-mount `setContexts`, which would clobber the
78+
* auto-registered integration/skill contexts.
79+
*/
80+
const editor = usePromptEditor({
81+
workspaceId,
82+
initialValue: task.prompt,
83+
initialContexts: task.contexts,
84+
})
85+
86+
return (
87+
<>
88+
<ChipModalHeader icon={Calendar} onClose={onClose}>
89+
Scheduled task
90+
</ChipModalHeader>
91+
<ChipModalBody>
92+
<ChipModalField type='copy' title='Status' value={STATUS_COPY[task.status].label} />
93+
<ChipModalField
94+
type='copy'
95+
title={STATUS_COPY[task.status].timeTitle}
96+
value={format(task.runAt, "EEEE, MMMM d, yyyy 'at' h:mm a")}
97+
/>
98+
<ChipModalField type='custom' title='Prompt'>
99+
<div className={cn(chipFieldSurfaceClass, 'max-h-[200px] overflow-y-auto px-1 py-0.5')}>
100+
<PromptEditor editor={editor} readOnly aria-label='Prompt' />
101+
</div>
102+
</ChipModalField>
103+
</ChipModalBody>
104+
<ChipModalFooter onCancel={onClose} primaryAction={{ label: 'Done', onClick: onClose }} />
105+
</>
106+
)
107+
}

apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import { useRef } from 'react'
44
import { format } from 'date-fns'
5-
import { Chip, ChipDatePicker, ChipModalField, ChipModalSeparator, Switch } from '@/components/emcn'
5+
import {
6+
CalendarDayCell,
7+
ChipDatePicker,
8+
ChipModalField,
9+
ChipModalSeparator,
10+
Switch,
11+
} from '@/components/emcn'
612
import type {
713
MonthlyMode,
814
Recurrence,
@@ -219,21 +225,24 @@ export function RecurrenceSection({ recurrence, onChange, launchDate }: Recurren
219225

220226
{recurrence.frequency === 'weekly' && (
221227
<ChipModalField type='custom' title='Repeat on'>
222-
<div className='flex gap-1'>
228+
{/* A one-row extract of the calendar: seven equal day cells built
229+
from the same {@link CalendarDayCell} the date picker uses, so
230+
the weekday toggles read as a sibling of the calendar rather than
231+
a separate segmented bar. */}
232+
<div className='grid grid-cols-7 gap-1'>
223233
{WEEKDAYS.map((weekday) => {
224234
const selected = selectedWeekdays.includes(weekday.value)
225235
return (
226-
<Chip
236+
<CalendarDayCell
227237
key={weekday.value}
228-
active={selected}
229-
flush
230-
className='min-w-0 flex-1 justify-center'
238+
selected={selected}
239+
fullWidth
231240
aria-pressed={selected}
232241
aria-label={weekday.name}
233242
onClick={() => handleWeekdayToggle(weekday.value)}
234243
>
235244
{weekday.short}
236-
</Chip>
245+
</CalendarDayCell>
237246
)
238247
})}
239248
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client'
2+
3+
import { type ButtonHTMLAttributes, forwardRef, type ReactNode } from 'react'
4+
import { chipVariants } from '@/components/emcn/components/chip/chip'
5+
import { cn } from '@/lib/core/utils/cn'
6+
7+
export interface CalendarDayCellProps
8+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
9+
/** Strong `primary` fill — the selected calendar day or an active weekday toggle. */
10+
selected?: boolean
11+
/** The `border` shadow-ring marking today. Ignored while `selected`. */
12+
today?: boolean
13+
/**
14+
* Fills the container width (the weekday-toggle row) instead of the fixed
15+
* 30px square used by the calendar's month grid.
16+
*/
17+
fullWidth?: boolean
18+
children: ReactNode
19+
}
20+
21+
/**
22+
* The single day pill shared by the {@link Calendar} month grid and any
23+
* chip-aligned day toggle (e.g. the scheduled-task weekly "Repeat on" row).
24+
* Built from `chipVariants` so the chrome — height, radius, centered glyph,
25+
* `primary` selected fill, `border` today ring — lives in one place and the
26+
* row of weekday toggles reads as a sibling of the date picker rather than a
27+
* separate control.
28+
*
29+
* @example
30+
* <CalendarDayCell selected={isSelected} today={isToday} onClick={pick}>{day}</CalendarDayCell>
31+
*
32+
* @example
33+
* // Weekday toggle: fill the column, drive selection with `aria-pressed`.
34+
* <CalendarDayCell selected={on} fullWidth aria-pressed={on} aria-label='Monday' onClick={toggle}>M</CalendarDayCell>
35+
*/
36+
export const CalendarDayCell = forwardRef<HTMLButtonElement, CalendarDayCellProps>(
37+
function CalendarDayCell(
38+
{ selected = false, today = false, fullWidth = false, className, children, type, ...props },
39+
ref
40+
) {
41+
return (
42+
<button
43+
ref={ref}
44+
type={type ?? 'button'}
45+
className={cn(
46+
chipVariants({
47+
variant: selected ? 'primary' : today ? 'border' : undefined,
48+
flush: true,
49+
}),
50+
'justify-center p-0',
51+
fullWidth ? 'h-[30px] w-full' : 'size-[30px]',
52+
!selected && 'text-[var(--text-body)]',
53+
className
54+
)}
55+
{...props}
56+
>
57+
{children}
58+
</button>
59+
)
60+
}
61+
)

apps/sim/components/emcn/components/calendar/calendar.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useMemo, useState } from 'react'
44
import { ChevronLeft, ChevronRight } from 'lucide-react'
5+
import { CalendarDayCell } from '@/components/emcn/components/calendar/calendar-day-cell'
56
import { Chip, chipVariants } from '@/components/emcn/components/chip/chip'
67
import { chipContentLabelClass } from '@/components/emcn/components/chip/chip-chrome'
78
import { cn } from '@/lib/core/utils/cn'
@@ -172,20 +173,9 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
172173

173174
return (
174175
<div key={day} className='flex h-[34px] items-center justify-center'>
175-
<button
176-
type='button'
177-
onClick={() => selectDay(day)}
178-
className={cn(
179-
chipVariants({
180-
variant: isSelected ? 'primary' : isToday ? 'border' : undefined,
181-
flush: true,
182-
}),
183-
'size-[30px] justify-center p-0',
184-
!isSelected && 'text-[var(--text-body)]'
185-
)}
186-
>
176+
<CalendarDayCell selected={isSelected} today={isToday} onClick={() => selectDay(day)}>
187177
{day}
188-
</button>
178+
</CalendarDayCell>
189179
</div>
190180
)
191181
})}

0 commit comments

Comments
 (0)