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 { search } from '@x/core/dist/search/search.js';
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';
/**
@ -705,6 +705,9 @@ export function setupIpcHandlers() {
const schedule = await classifySchedule(args.instruction);
return { schedule };
},
'inline-task:process': async (_event, args) => {
return await processRowboatInstruction(args.instruction, args.noteContent, args.notePath);
},
'voice:getConfig': async () => {
return voice.getVoiceConfig();
},

View file

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

View file

@ -232,6 +232,7 @@ interface MarkdownEditorProps {
frontmatter?: string | null
onFrontmatterChange?: (raw: string | null) => void
onExport?: (format: 'md' | 'pdf' | 'docx') => void
notePath?: string
}
type WikiLinkMatch = {
@ -323,6 +324,7 @@ export function MarkdownEditor({
frontmatter,
onFrontmatterChange,
onExport,
notePath,
}: MarkdownEditorProps) {
const isInternalUpdate = useRef(false)
const wrapperRef = useRef<HTMLDivElement>(null)
@ -928,24 +930,17 @@ export function MarkdownEditor({
}
if (activeRowboatMention) {
// Classify schedule intent for new blocks
const blockData: Record<string, unknown> = { instruction }
try {
const result = await window.ipc.invoke('inline-task:classifySchedule', { instruction })
if (result.schedule) {
const { label, ...rest } = result.schedule
blockData.schedule = rest
blockData['schedule-label'] = label
}
} catch (error) {
console.error('[RowboatAdd] Schedule classification failed:', error)
}
// Insert a temporary processing block
const blockData: Record<string, unknown> = { instruction, processing: true }
const insertFrom = activeRowboatMention.range.from
const insertTo = activeRowboatMention.range.to
editor
.chain()
.focus()
.insertContentAt(
{ from: activeRowboatMention.range.from, to: activeRowboatMention.range.to },
{ from: insertFrom, to: insertTo },
[
{ type: 'taskBlock', attrs: { data: JSON.stringify(blockData) } },
{ type: 'paragraph' },
@ -953,17 +948,124 @@ export function MarkdownEditor({
)
.run()
// Mark note as live
if (onFrontmatterChange) {
const fields = extractAllFrontmatterValues(frontmatter ?? null)
fields['live_note'] = 'true'
onFrontmatterChange(buildFrontmatter(fields))
}
setActiveRowboatMention(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(() => {
if (!editor || !rowboatBlockEdit) return

View file

@ -1,22 +1,39 @@
import { mergeAttributes, Node } 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'
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 }) {
const raw = node.attrs.data as string
let instruction = ''
let scheduleLabel = ''
let processing = false
let lastRunAt = ''
try {
const parsed = inlineTask.InlineTaskBlockSchema.parse(JSON.parse(raw))
instruction = parsed.instruction
scheduleLabel = parsed['schedule-label'] ?? ''
processing = parsed.processing ?? false
lastRunAt = parsed.lastRunAt ?? ''
} catch {
// Fallback: show raw data
instruction = raw
}
const lastRunLabel = lastRunAt ? formatDateTime(lastRunAt) : ''
return (
<NodeViewWrapper className="task-block-wrapper" data-type="task-block">
<div className="task-block-card">
@ -29,10 +46,17 @@ function TaskBlockView({ node, deleteNode }: { node: { attrs: Record<string, unk
</button>
<div className="task-block-content">
<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">
<CalendarClock size={12} />
{scheduleLabel}
{lastRunLabel && <span className="task-block-last-run"> · last ran {lastRunLabel}</span>}
</span>
)}
</div>

View file

@ -608,6 +608,10 @@
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) */
.tiptap-editor .ProseMirror .image-block-wrapper,
.tiptap-editor .ProseMirror .embed-block-wrapper,