Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export interface PromptEditorProps extends PromptEditorKeyPolicy {
placeholder?: string
/** Focuses the editor (caret at end) on mount. */
autoFocus?: boolean
/**
* Renders the editor as a non-editable display surface: the textarea becomes
* `readOnly` (so the chip overlay still paints `@`-mention / `/`-skill chips
* and the text stays selectable/copyable) and the caret-anchored resource and
* skill menus are not mounted. Use for read-only records — e.g. a finished
* scheduled task — where the prompt should render with chips but not be edited.
*/
readOnly?: boolean
/**
* Layout/sizing only — a height cap (`max-h-[200px]`) or fill (`flex-1`)
* for the scroll container. The text chrome is owned by the editor.
Expand Down Expand Up @@ -56,6 +64,7 @@ export function PromptEditor({
editor,
placeholder,
autoFocus = false,
readOnly = false,
className,
'aria-label': ariaLabel,
onSubmit,
Expand All @@ -73,7 +82,7 @@ export function PromptEditor({
}, [value, textareaRef])

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

Expand Down Expand Up @@ -167,38 +176,45 @@ export function PromptEditor({
<textarea
ref={textareaRef}
value={value}
onChange={editor.handleInputChange}
onKeyDown={(e) => editor.handleKeyDown(e, { onSubmit, onArrowUpOnEmpty })}
onPaste={editor.handlePaste}
readOnly={readOnly}
onChange={readOnly ? undefined : editor.handleInputChange}
onKeyDown={
readOnly ? undefined : (e) => editor.handleKeyDown(e, { onSubmit, onArrowUpOnEmpty })
}
onPaste={readOnly ? undefined : editor.handlePaste}
onCopy={editor.handleCopy}
onCut={editor.handleCut}
onSelect={editor.handleSelectAdjust}
onMouseUp={editor.handleSelectAdjust}
onCut={readOnly ? undefined : editor.handleCut}
onSelect={readOnly ? undefined : editor.handleSelectAdjust}
onMouseUp={readOnly ? undefined : editor.handleSelectAdjust}
placeholder={placeholder}
aria-label={ariaLabel}
rows={1}
className={TEXTAREA_BASE_CLASSES}
/>
</div>

<PlusMenuDropdown
ref={editor.plusMenuRef}
availableResources={editor.availableResources}
onResourceSelect={editor.insertResource}
onClose={editor.handlePlusMenuClose}
textareaRef={editor.textareaRef}
pendingCursorRef={editor.pendingCursorRef}
mentionQuery={editor.mentionQuery ?? undefined}
/>
<SkillsMenuDropdown
ref={editor.skillsMenuRef}
skills={editor.skills}
onSkillSelect={editor.handleSkillSelect}
onClose={editor.handleSkillsMenuClose}
textareaRef={editor.textareaRef}
pendingCursorRef={editor.pendingCursorRef}
slashQuery={editor.slashQuery ?? undefined}
/>
{!readOnly && (
<>
<PlusMenuDropdown
ref={editor.plusMenuRef}
availableResources={editor.availableResources}
onResourceSelect={editor.insertResource}
onClose={editor.handlePlusMenuClose}
textareaRef={editor.textareaRef}
pendingCursorRef={editor.pendingCursorRef}
mentionQuery={editor.mentionQuery ?? undefined}
/>
<SkillsMenuDropdown
ref={editor.skillsMenuRef}
skills={editor.skills}
onSkillSelect={editor.handleSkillSelect}
onClose={editor.handleSkillsMenuClose}
textareaRef={editor.textareaRef}
pendingCursorRef={editor.pendingCursorRef}
slashQuery={editor.slashQuery ?? undefined}
/>
</>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
'use client'

import { useEffect } from 'react'
import { format } from 'date-fns'
import { useParams } from 'next/navigation'
import {
Calendar,
ChipModal,
ChipModalBody,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
chipFieldSurfaceClass,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import {
PromptEditor,
usePromptEditor,
} from '@/app/workspace/[workspaceId]/home/components/user-input/components'
import type {
ScheduledTask,
ScheduledTaskStatus,
Expand All @@ -35,7 +43,7 @@ interface TaskDetailsModalProps {
/**
* Read-only record modal for tasks that are running or already finished —
* pending tasks open the edit `TaskModal` instead. Three plaintext fields:
* Status and the run time as copy fields, the prompt as a view-only textarea.
* Status and the run time as copy fields, the prompt as a view-only chip editor.
*/
export function TaskDetailsModal({ task, onClose }: TaskDetailsModalProps) {
return (
Expand All @@ -47,23 +55,51 @@ export function TaskDetailsModal({ task, onClose }: TaskDetailsModalProps) {
size='md'
srTitle='Scheduled task'
>
{task && (
<>
<ChipModalHeader icon={Calendar} onClose={onClose}>
Scheduled task
</ChipModalHeader>
<ChipModalBody>
<ChipModalField type='copy' title='Status' value={STATUS_COPY[task.status].label} />
<ChipModalField
type='copy'
title={STATUS_COPY[task.status].timeTitle}
value={format(task.runAt, "EEEE, MMMM d, yyyy 'at' h:mm a")}
/>
<ChipModalField type='textarea' title='Prompt' value={task.prompt} viewOnly />
</ChipModalBody>
<ChipModalFooter onCancel={onClose} primaryAction={{ label: 'Done', onClick: onClose }} />
</>
)}
{task && <TaskDetailsContent task={task} onClose={onClose} />}
</ChipModal>
)
}

/**
* Inner content, mounted only while a task is shown (the Radix portal unmounts
* closed content). Holding the read-only editor here keeps its mention-data
* queries from firing on page load and re-seeds from the task on each open.
*/
function TaskDetailsContent({ task, onClose }: { task: ScheduledTask; onClose: () => void }) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const editor = usePromptEditor({ workspaceId, initialValue: task.prompt })
const setContexts = editor.setContexts

/**
* Re-registers the task's stored `@`-mentions once on open so resource chips
* (files, tables, knowledge) render. Integration `@`-mentions and `/`-skills
* chipify from the seeded text alone, so they render even without stored
* contexts. Runs once per open since the content remounts each time it opens.
*/
useEffect(() => {
if (task.contexts && task.contexts.length > 0) setContexts(task.contexts)
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only re-register
}, [])
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

return (
<>
<ChipModalHeader icon={Calendar} onClose={onClose}>
Scheduled task
</ChipModalHeader>
<ChipModalBody>
<ChipModalField type='copy' title='Status' value={STATUS_COPY[task.status].label} />
<ChipModalField
type='copy'
title={STATUS_COPY[task.status].timeTitle}
value={format(task.runAt, "EEEE, MMMM d, yyyy 'at' h:mm a")}
/>
<ChipModalField type='custom' title='Prompt'>
<div className={cn(chipFieldSurfaceClass, 'max-h-[200px] overflow-y-auto px-1 py-0.5')}>
<PromptEditor editor={editor} readOnly aria-label='Prompt' />
</div>
</ChipModalField>
</ChipModalBody>
<ChipModalFooter onCancel={onClose} primaryAction={{ label: 'Done', onClick: onClose }} />
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import { useRef } from 'react'
import { format } from 'date-fns'
import { Chip, ChipDatePicker, ChipModalField, ChipModalSeparator, Switch } from '@/components/emcn'
import {
ChipDatePicker,
ChipModalField,
ChipModalSeparator,
chipVariants,
Switch,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import type {
MonthlyMode,
Recurrence,
Expand Down Expand Up @@ -219,21 +226,28 @@ export function RecurrenceSection({ recurrence, onChange, launchDate }: Recurren

{recurrence.frequency === 'weekly' && (
<ChipModalField type='custom' title='Repeat on'>
<div className='flex gap-1'>
{/* A one-row extract of the calendar: seven equal day cells that
reuse its exact day-cell grammar (`primary` fill when on, bare
`--text-body` when off) so the weekday toggles read as a sibling
of the date picker rather than a separate segmented bar. */}
<div className='grid grid-cols-7 gap-1'>
{WEEKDAYS.map((weekday) => {
const selected = selectedWeekdays.includes(weekday.value)
return (
<Chip
<button
key={weekday.value}
active={selected}
flush
className='min-w-0 flex-1 justify-center'
type='button'
aria-pressed={selected}
aria-label={weekday.name}
onClick={() => handleWeekdayToggle(weekday.value)}
className={cn(
chipVariants({ variant: selected ? 'primary' : undefined, flush: true }),
'h-[30px] w-full justify-center p-0',
!selected && 'text-[var(--text-body)]'
)}
>
{weekday.short}
</Chip>
</button>
)
})}
</div>
Expand Down
Loading