Livenote2 (#440)

* tasks execute immediately

* response formatting

* remove at rowbot block for single tasks

* show last ran time stamp
This commit is contained in:
arkml 2026-03-19 01:34:10 +05:30 committed by GitHub
parent 91030a5fca
commit affc9956f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 372 additions and 39 deletions

View file

@ -39,7 +39,7 @@ import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js'; import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
import { search } from '@x/core/dist/search/search.js'; import { search } from '@x/core/dist/search/search.js';
import { versionHistory, voice } from '@x/core'; import { versionHistory, voice } from '@x/core';
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js'; import { classifySchedule, processRowboatInstruction } from '@x/core/dist/knowledge/inline_tasks.js';
import { getBillingInfo } from '@x/core/dist/billing/billing.js'; import { getBillingInfo } from '@x/core/dist/billing/billing.js';
/** /**
@ -705,6 +705,9 @@ export function setupIpcHandlers() {
const schedule = await classifySchedule(args.instruction); const schedule = await classifySchedule(args.instruction);
return { schedule }; return { schedule };
}, },
'inline-task:process': async (_event, args) => {
return await processRowboatInstruction(args.instruction, args.noteContent, args.notePath);
},
'voice:getConfig': async () => { 'voice:getConfig': async () => {
return voice.getVoiceConfig(); return voice.getVoiceConfig();
}, },

View file

@ -3869,6 +3869,7 @@ function App() {
> >
<MarkdownEditor <MarkdownEditor
content={tabContent} content={tabContent}
notePath={tab.path}
onChange={(markdown) => { if (!isViewingHistory) handleEditorChange(tab.path, markdown) }} onChange={(markdown) => { if (!isViewingHistory) handleEditorChange(tab.path, markdown) }}
onPrimaryHeadingCommit={() => { onPrimaryHeadingCommit={() => {
untitledRenameReadyPathsRef.current.add(tab.path) untitledRenameReadyPathsRef.current.add(tab.path)

View file

@ -232,6 +232,7 @@ interface MarkdownEditorProps {
frontmatter?: string | null frontmatter?: string | null
onFrontmatterChange?: (raw: string | null) => void onFrontmatterChange?: (raw: string | null) => void
onExport?: (format: 'md' | 'pdf' | 'docx') => void onExport?: (format: 'md' | 'pdf' | 'docx') => void
notePath?: string
} }
type WikiLinkMatch = { type WikiLinkMatch = {
@ -323,6 +324,7 @@ export function MarkdownEditor({
frontmatter, frontmatter,
onFrontmatterChange, onFrontmatterChange,
onExport, onExport,
notePath,
}: MarkdownEditorProps) { }: MarkdownEditorProps) {
const isInternalUpdate = useRef(false) const isInternalUpdate = useRef(false)
const wrapperRef = useRef<HTMLDivElement>(null) const wrapperRef = useRef<HTMLDivElement>(null)
@ -928,24 +930,17 @@ export function MarkdownEditor({
} }
if (activeRowboatMention) { if (activeRowboatMention) {
// Classify schedule intent for new blocks // Insert a temporary processing block
const blockData: Record<string, unknown> = { instruction } const blockData: Record<string, unknown> = { instruction, processing: true }
try {
const result = await window.ipc.invoke('inline-task:classifySchedule', { instruction }) const insertFrom = activeRowboatMention.range.from
if (result.schedule) { const insertTo = activeRowboatMention.range.to
const { label, ...rest } = result.schedule
blockData.schedule = rest
blockData['schedule-label'] = label
}
} catch (error) {
console.error('[RowboatAdd] Schedule classification failed:', error)
}
editor editor
.chain() .chain()
.focus() .focus()
.insertContentAt( .insertContentAt(
{ from: activeRowboatMention.range.from, to: activeRowboatMention.range.to }, { from: insertFrom, to: insertTo },
[ [
{ type: 'taskBlock', attrs: { data: JSON.stringify(blockData) } }, { type: 'taskBlock', attrs: { data: JSON.stringify(blockData) } },
{ type: 'paragraph' }, { type: 'paragraph' },
@ -953,17 +948,124 @@ export function MarkdownEditor({
) )
.run() .run()
// Mark note as live
if (onFrontmatterChange) {
const fields = extractAllFrontmatterValues(frontmatter ?? null)
fields['live_note'] = 'true'
onFrontmatterChange(buildFrontmatter(fields))
}
setActiveRowboatMention(null) setActiveRowboatMention(null)
setRowboatAnchorTop(null) setRowboatAnchorTop(null)
// Get editor content for the agent
const editorContent = editor.storage.markdown?.getMarkdown?.() ?? ''
// Helper to find the processing block
const findProcessingBlock = (): number | null => {
let pos: number | null = null
editor.state.doc.descendants((node, p) => {
if (pos !== null) return false
if (node.type.name === 'taskBlock') {
try {
const data = JSON.parse(node.attrs.data || '{}')
if (data.instruction === instruction && data.processing === true) {
pos = p
return false
}
} catch { /* skip */ }
}
})
return pos
}
try {
// Call the copilot assistant for both one-time and recurring tasks
const result = await window.ipc.invoke('inline-task:process', {
instruction,
noteContent: editorContent,
notePath: notePath ?? '',
})
const currentPos = findProcessingBlock()
if (currentPos === null) return
const node = editor.state.doc.nodeAt(currentPos)
if (!node) return
if (result.schedule) {
// Recurring/scheduled task: update block with schedule, write target tags to disk
const targetId = Math.random().toString(36).slice(2, 10)
const updatedData: Record<string, unknown> = {
instruction: result.instruction,
schedule: result.schedule,
'schedule-label': result.scheduleLabel,
targetId,
}
const tr = editor.state.tr.setNodeMarkup(currentPos, undefined, {
data: JSON.stringify(updatedData),
})
editor.view.dispatch(tr)
// Mark note as live
if (onFrontmatterChange) {
const fields = extractAllFrontmatterValues(frontmatter ?? null)
fields['live_note'] = 'true'
onFrontmatterChange(buildFrontmatter(fields))
}
// Write target tags directly to the file on disk after a short delay
// to let the editor save the updated content first
if (notePath) {
setTimeout(async () => {
try {
const file = await window.ipc.invoke('workspace:readFile', { path: notePath })
const content = file.data
const openTag = `<!--task-target:${targetId}-->`
const closeTag = `<!--/task-target:${targetId}-->`
// Only add if not already present
if (content.includes(openTag)) return
// Find the task block in the raw markdown and insert target tags after it
const blockJson = JSON.stringify(updatedData)
const blockStart = content.indexOf('```task\n' + blockJson)
if (blockStart !== -1) {
const blockEnd = content.indexOf('\n```', blockStart + 8)
if (blockEnd !== -1) {
const insertAt = blockEnd + 4 // after the closing ```
const before = content.slice(0, insertAt)
const after = content.slice(insertAt)
const updated = before + '\n\n' + openTag + '\n' + closeTag + after
await window.ipc.invoke('workspace:writeFile', {
path: notePath,
data: updated,
opts: { encoding: 'utf8' },
})
}
}
} catch (err) {
console.error('[RowboatAdd] Failed to write target tags:', err)
}
}, 500)
}
} else {
// One-time task: remove the processing block, insert response in its place
const insertPos = currentPos
const deleteEnd = currentPos + node.nodeSize
editor.chain().focus().deleteRange({ from: insertPos, to: deleteEnd }).run()
if (result.response) {
editor.chain().insertContentAt(insertPos, result.response).run()
}
}
} catch (error) {
console.error('[RowboatAdd] Processing failed:', error)
// Remove the processing block on error
const currentPos = findProcessingBlock()
if (currentPos !== null) {
const node = editor.state.doc.nodeAt(currentPos)
if (node) {
editor.chain().focus().deleteRange({ from: currentPos, to: currentPos + node.nodeSize }).run()
}
}
}
} }
}, [editor, activeRowboatMention, rowboatBlockEdit, frontmatter, onFrontmatterChange]) }, [editor, activeRowboatMention, rowboatBlockEdit, frontmatter, onFrontmatterChange, notePath])
const handleRowboatRemove = useCallback(() => { const handleRowboatRemove = useCallback(() => {
if (!editor || !rowboatBlockEdit) return if (!editor || !rowboatBlockEdit) return

View file

@ -1,22 +1,39 @@
import { mergeAttributes, Node } from '@tiptap/react' import { mergeAttributes, Node } from '@tiptap/react'
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
import { CalendarClock, X } from 'lucide-react' import { CalendarClock, Loader2, X } from 'lucide-react'
import { inlineTask } from '@x/shared' import { inlineTask } from '@x/shared'
function formatDateTime(iso: string): string {
const d = new Date(iso)
return d.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
}
function TaskBlockView({ node, deleteNode }: { node: { attrs: Record<string, unknown> }; deleteNode: () => void }) { function TaskBlockView({ node, deleteNode }: { node: { attrs: Record<string, unknown> }; deleteNode: () => void }) {
const raw = node.attrs.data as string const raw = node.attrs.data as string
let instruction = '' let instruction = ''
let scheduleLabel = '' let scheduleLabel = ''
let processing = false
let lastRunAt = ''
try { try {
const parsed = inlineTask.InlineTaskBlockSchema.parse(JSON.parse(raw)) const parsed = inlineTask.InlineTaskBlockSchema.parse(JSON.parse(raw))
instruction = parsed.instruction instruction = parsed.instruction
scheduleLabel = parsed['schedule-label'] ?? '' scheduleLabel = parsed['schedule-label'] ?? ''
processing = parsed.processing ?? false
lastRunAt = parsed.lastRunAt ?? ''
} catch { } catch {
// Fallback: show raw data // Fallback: show raw data
instruction = raw instruction = raw
} }
const lastRunLabel = lastRunAt ? formatDateTime(lastRunAt) : ''
return ( return (
<NodeViewWrapper className="task-block-wrapper" data-type="task-block"> <NodeViewWrapper className="task-block-wrapper" data-type="task-block">
<div className="task-block-card"> <div className="task-block-card">
@ -29,10 +46,17 @@ function TaskBlockView({ node, deleteNode }: { node: { attrs: Record<string, unk
</button> </button>
<div className="task-block-content"> <div className="task-block-content">
<span className="task-block-instruction"><span className="task-block-prefix">@rowboat</span> {instruction}</span> <span className="task-block-instruction"><span className="task-block-prefix">@rowboat</span> {instruction}</span>
{scheduleLabel && ( {processing && (
<span className="task-block-schedule">
<Loader2 size={12} className="animate-spin" />
processing
</span>
)}
{!processing && scheduleLabel && (
<span className="task-block-schedule"> <span className="task-block-schedule">
<CalendarClock size={12} /> <CalendarClock size={12} />
{scheduleLabel} {scheduleLabel}
{lastRunLabel && <span className="task-block-last-run"> · last ran {lastRunLabel}</span>}
</span> </span>
)} )}
</div> </div>

View file

@ -608,6 +608,10 @@
color: color-mix(in srgb, var(--foreground) 55%, transparent); color: color-mix(in srgb, var(--foreground) 55%, transparent);
} }
.tiptap-editor .ProseMirror .task-block-last-run {
color: color-mix(in srgb, var(--foreground) 38%, transparent);
}
/* Shared block styles (image, embed, chart, table) */ /* Shared block styles (image, embed, chart, table) */
.tiptap-editor .ProseMirror .image-block-wrapper, .tiptap-editor .ProseMirror .image-block-wrapper,
.tiptap-editor .ProseMirror .embed-block-wrapper, .tiptap-editor .ProseMirror .embed-block-wrapper,

View file

@ -5,6 +5,13 @@ export function getRaw(): string {
.map(name => ` ${name}:\n type: builtin\n name: ${name}`) .map(name => ` ${name}:\n type: builtin\n name: ${name}`)
.join('\n'); .join('\n');
const now = new Date();
const defaultEnd = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const localNow = now.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'long' });
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const nowISO = now.toISOString();
const defaultEndISO = defaultEnd.toISOString();
return `--- return `---
model: gpt-5.2 model: gpt-5.2
tools: tools:
@ -12,16 +19,74 @@ ${toolEntries}
--- ---
# Task # Task
You are an inline task execution agent. You receive a @rowboat instruction from within a knowledge note and execute it. You are an inline task execution agent. You receive a @rowboat instruction from within a knowledge note and either execute it immediately or set it up as a recurring task.
# Instructions # Two Modes
1. You will receive the full content of a knowledge note and a specific instruction extracted from a \`@rowboat <instruction>\` line in that note. ## 1. One-Time Tasks (no scheduling intent)
2. Execute the instruction using your full workspace tool set. You have access to read files, edit files, search, run commands, etc. For instructions that should be executed immediately (e.g., "summarize this note", "look up the weather"):
3. Use the surrounding note content as context for the task. - Execute the instruction using your full workspace tool set
4. Your response will be inserted directly into the note below the @rowboat instruction. Write your output as note content it must read naturally as part of the document. - Return the result as markdown content
5. NEVER include meta-commentary, thinking out loud, or narration about what you're doing. No "Let me look that up", "Here are the details", "I found the following", etc. Just write the content itself. - Do NOT include any schedule or instruction markers
6. Keep the result concise and well-formatted in markdown.
7. Do not modify the original note file the service will handle inserting your response. ## 2. Recurring/Scheduled Tasks (has scheduling intent)
For instructions that imply a recurring or future-scheduled task (e.g., "every morning at 8am check emails", "remind me tomorrow at 3pm"):
- Do NOT execute the task only set up the schedule
- You MUST include BOTH markers described below
- Do NOT include any other content besides the markers
# Markers for Scheduled Tasks
When the instruction has scheduling intent, your response MUST contain these markers and nothing else:
## Schedule Marker (required)
<!--rowboat-schedule:{"type":"...","label":"..."}-->
Schedule types:
1. "cron" recurring: \`<!--rowboat-schedule:{"type":"cron","expression":"<5-field cron>","startDate":"<ISO>","endDate":"<ISO>","label":"<label>"}-->\`
"startDate" defaults to now (${nowISO}). "endDate" defaults to 7 days from now (${defaultEndISO}).
Example: "every morning at 8am" \`<!--rowboat-schedule:{"type":"cron","expression":"0 8 * * *","startDate":"${nowISO}","endDate":"${defaultEndISO}","label":"runs daily at 8 AM until ${defaultEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}"}-->\`
2. "window" recurring with time window: \`<!--rowboat-schedule:{"type":"window","cron":"<cron>","startTime":"HH:MM","endTime":"HH:MM","startDate":"<ISO>","endDate":"<ISO>","label":"<label>"}-->\`
3. "once" future one-time: \`<!--rowboat-schedule:{"type":"once","runAt":"<ISO 8601>","label":"<label>"}-->\`
The "label" must be a short plain-English description starting with "runs" (e.g., "runs daily at 8 AM until Mar 24").
## Instruction Marker (required for scheduled tasks)
<!--rowboat-instruction:the refined instruction text-->
This is the instruction that will be executed on each scheduled run. You may refine/clarify the original instruction to make it more specific and actionable for the background agent that will execute it. For example:
- User says "check my emails every morning" \`<!--rowboat-instruction:Check for new emails and summarize any important ones.-->\`
- User says "news about claude daily" \`<!--rowboat-instruction:Search for the latest news about Anthropic's Claude AI and list the top stories with sources.-->\`
If the instruction is already clear and actionable, you can keep it as-is.
# Context
Current local time: ${localNow}
Timezone: ${tz}
Current UTC time: ${nowISO}
# Output Rules
- For one-time tasks: write output as note content it must read naturally as part of the document. NEVER include meta-commentary. Keep concise and well-formatted in markdown.
- For scheduled tasks: output ONLY the two markers (schedule + instruction), nothing else.
- Do not modify the original note file the system handles all insertions.
# Target Regions
For recurring/scheduled tasks, the note will contain a **target region** delimited by HTML comment tags:
\`\`\`
<!--task-target:TARGETID-->
...existing content...
<!--/task-target:TARGETID-->
\`\`\`
When you see a target region associated with your task (during a scheduled run), your response MUST be the replacement content for that region. You should:
- Write content that replaces whatever is currently between the tags
- Use the existing content as context (e.g., to update rather than regenerate from scratch if appropriate)
- Do NOT include the target tags themselves in your response
`; `;
} }

View file

@ -176,6 +176,8 @@ interface InlineTask {
startLine: number; startLine: number;
/** Line index of the closing ``` fence */ /** Line index of the closing ``` fence */
endLine: number; endLine: number;
/** Target region ID for recurring tasks */
targetId: string | null;
} }
/** /**
@ -183,7 +185,7 @@ interface InlineTask {
* Returns { instruction, schedule } or null if not valid JSON. * Returns { instruction, schedule } or null if not valid JSON.
* Also supports legacy @rowboat format. * Also supports legacy @rowboat format.
*/ */
function parseBlockContent(contentLines: string[]): { instruction: string; schedule: InlineTaskSchedule | null; lastRunAt: string | null } | null { function parseBlockContent(contentLines: string[]): { instruction: string; schedule: InlineTaskSchedule | null; lastRunAt: string | null; targetId: string | null } | null {
const raw = contentLines.join('\n').trim(); const raw = contentLines.join('\n').trim();
try { try {
const data = JSON.parse(raw); const data = JSON.parse(raw);
@ -193,6 +195,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
instruction: parsed.data.instruction, instruction: parsed.data.instruction,
schedule: parsed.data.schedule ? { ...parsed.data.schedule, label: parsed.data['schedule-label'] ?? '' } as InlineTaskSchedule : null, schedule: parsed.data.schedule ? { ...parsed.data.schedule, label: parsed.data['schedule-label'] ?? '' } as InlineTaskSchedule : null,
lastRunAt: parsed.data.lastRunAt ?? null, lastRunAt: parsed.data.lastRunAt ?? null,
targetId: parsed.data.targetId ?? null,
}; };
} }
// Fallback for blocks that have instruction but don't fully match schema // Fallback for blocks that have instruction but don't fully match schema
@ -201,6 +204,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
instruction: data.instruction, instruction: data.instruction,
schedule: data.schedule ?? null, schedule: data.schedule ?? null,
lastRunAt: data.lastRunAt ?? null, lastRunAt: data.lastRunAt ?? null,
targetId: data.targetId ?? null,
}; };
} }
} catch { } catch {
@ -227,7 +231,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
const rawInstruction = firstRowboatLine?.trim() ?? instructionLines.join('\n').trim(); const rawInstruction = firstRowboatLine?.trim() ?? instructionLines.join('\n').trim();
const instruction = rawInstruction.replace(/^@rowboat:?\s*/, ''); const instruction = rawInstruction.replace(/^@rowboat:?\s*/, '');
if (!instruction) return null; if (!instruction) return null;
return { instruction, schedule, lastRunAt: null }; return { instruction, schedule, lastRunAt: null, targetId: null };
} }
/** /**
@ -308,16 +312,16 @@ function findPendingTasks(body: string): InlineTask[] {
const parsed = parseBlockContent(contentLines); const parsed = parseBlockContent(contentLines);
if (parsed) { if (parsed) {
const { instruction, schedule, lastRunAt } = parsed; const { instruction, schedule, lastRunAt, targetId } = parsed;
if (schedule) { if (schedule) {
if (isScheduledTaskDue(schedule, lastRunAt)) { if (isScheduledTaskDue(schedule, lastRunAt)) {
tasks.push({ instruction, schedule, startLine, endLine }); tasks.push({ instruction, schedule, startLine, endLine, targetId });
} }
} else { } else {
// One-time task: skip if already ran // One-time task: skip if already ran
if (!lastRunAt) { if (!lastRunAt) {
tasks.push({ instruction, schedule: null, startLine, endLine }); tasks.push({ instruction, schedule: null, startLine, endLine, targetId });
} }
} }
} }
@ -339,6 +343,32 @@ function insertResultBelow(body: string, endLine: number, result: string): strin
} }
/**
* Replace content inside a target region identified by targetId.
* If the target region exists, replaces its content.
* If it doesn't exist, creates the target region below the task block,
* wrapping any existing content between the block and the next block/heading.
*/
function replaceTargetRegion(body: string, targetId: string, result: string, endLine: number): string {
const openTag = `<!--task-target:${targetId}-->`;
const closeTag = `<!--/task-target:${targetId}-->`;
const openIdx = body.indexOf(openTag);
const closeIdx = body.indexOf(closeTag);
if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
// Target region exists — replace content between the tags
const before = body.slice(0, openIdx + openTag.length);
const after = body.slice(closeIdx);
return before + '\n' + result + '\n' + after;
}
// Target region doesn't exist yet — create it below the task block's closing fence
const lines = body.split('\n');
const taggedResult = `${openTag}\n${result}\n${closeTag}`;
lines.splice(endLine + 1, 0, '', taggedResult);
return lines.join('\n');
}
/** /**
* Determine if a note has any "live" tell-rowboat tasks. * Determine if a note has any "live" tell-rowboat tasks.
* A task is live if: * A task is live if:
@ -495,7 +525,13 @@ async function processInlineTasks(): Promise<void> {
const result = await extractAgentResponse(run.id); const result = await extractAgentResponse(run.id);
if (result) { if (result) {
currentBody = insertResultBelow(currentBody, task.endLine, result); if (task.targetId) {
// Recurring task with target region — replace content inside the region
currentBody = replaceTargetRegion(currentBody, task.targetId, result, task.endLine);
} else {
// No target region — insert below the block
currentBody = insertResultBelow(currentBody, task.endLine, result);
}
// Update the block JSON with lastRunAt // Update the block JSON with lastRunAt
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
currentBody = updateBlockData(currentBody, task.startLine, task.endLine, timestamp); currentBody = updateBlockData(currentBody, task.startLine, task.endLine, timestamp);
@ -531,6 +567,85 @@ async function processInlineTasks(): Promise<void> {
} }
} }
/**
* Process a @rowboat instruction via the inline task agent.
* The agent can execute one-off tasks and/or detect scheduling intent.
* Returns schedule info (if any), a schedule label, and optional response text.
*/
type ScheduleWithoutLabel =
| { type: 'cron'; expression: string; startDate: string; endDate: string }
| { type: 'window'; cron: string; startTime: string; endTime: string; startDate: string; endDate: string }
| { type: 'once'; runAt: string };
export async function processRowboatInstruction(
instruction: string,
noteContent: string,
notePath: string,
): Promise<{
instruction: string;
schedule: ScheduleWithoutLabel | null;
scheduleLabel: string | null;
response: string | null;
}> {
const run = await createRun({ agentId: INLINE_TASK_AGENT });
const message = [
`Process the following @rowboat instruction from the note "${notePath}":`,
'',
`**Instruction:** ${instruction}`,
'',
'**Full note content for context:**',
'```markdown',
noteContent,
'```',
].join('\n');
await createMessage(run.id, message);
await waitForRunCompletion(run.id);
const rawResponse = await extractAgentResponse(run.id);
if (!rawResponse) {
return { instruction, schedule: null, scheduleLabel: null, response: null };
}
// Parse out the schedule marker if present (allow multiline JSON)
const scheduleMarkerRegex = /<!--rowboat-schedule:([\s\S]*?)-->/;
const scheduleMatch = rawResponse.match(scheduleMarkerRegex);
// Parse out the instruction marker if present
const instructionMarkerRegex = /<!--rowboat-instruction:([\s\S]*?)-->/;
const instructionMatch = rawResponse.match(instructionMarkerRegex);
let schedule: ScheduleWithoutLabel | null = null;
let scheduleLabel: string | null = null;
let refinedInstruction = instruction;
if (instructionMatch) {
refinedInstruction = instructionMatch[1].trim();
}
if (scheduleMatch) {
try {
const parsed = JSON.parse(scheduleMatch[1]);
if (parsed && typeof parsed === 'object' && parsed.type) {
scheduleLabel = parsed.label || null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { label: _, ...rest } = parsed;
schedule = rest as ScheduleWithoutLabel;
}
} catch {
// Invalid JSON in marker — ignore
}
// Scheduled task — no response content (agent only returns markers)
return { instruction: refinedInstruction, schedule, scheduleLabel, response: null };
}
// One-time task — the full response is the content
const response = rawResponse.trim() || null;
return { instruction: refinedInstruction, schedule: null, scheduleLabel: null, response };
}
/** /**
* Classify whether an instruction contains a scheduling intent using the user's configured LLM. * Classify whether an instruction contains a scheduling intent using the user's configured LLM.
* Returns a schedule object or null for one-time tasks. * Returns a schedule object or null for one-time tasks.

View file

@ -28,6 +28,8 @@ export const InlineTaskBlockSchema = z.object({
schedule: InlineTaskScheduleSchema.optional(), schedule: InlineTaskScheduleSchema.optional(),
'schedule-label': z.string().optional(), 'schedule-label': z.string().optional(),
lastRunAt: z.string().optional(), lastRunAt: z.string().optional(),
processing: z.boolean().optional(),
targetId: z.string().optional(),
}); });
export type InlineTaskBlock = z.infer<typeof InlineTaskBlockSchema>; export type InlineTaskBlock = z.infer<typeof InlineTaskBlockSchema>;

View file

@ -522,6 +522,23 @@ const ipcSchemas = {
]).nullable(), ]).nullable(),
}), }),
}, },
'inline-task:process': {
req: z.object({
instruction: z.string(),
noteContent: z.string(),
notePath: z.string(),
}),
res: z.object({
instruction: z.string(),
schedule: z.union([
z.object({ type: z.literal('cron'), expression: z.string(), startDate: z.string(), endDate: z.string() }),
z.object({ type: z.literal('window'), cron: z.string(), startTime: z.string(), endTime: z.string(), startDate: z.string(), endDate: z.string() }),
z.object({ type: z.literal('once'), runAt: z.string() }),
]).nullable(),
scheduleLabel: z.string().nullable(),
response: z.string().nullable(),
}),
},
// Billing channels // Billing channels
'billing:getInfo': { 'billing:getInfo': {
req: z.null(), req: z.null(),