From 5d65616cfbad18c301f8dd656c5ecf309411d561 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:42:13 +0530 Subject: [PATCH] add prompt block --- apps/x/apps/renderer/src/App.tsx | 21 +++ .../src/components/markdown-editor.tsx | 2 + .../renderer/src/extensions/prompt-block.tsx | 145 ++++++++++++++++++ apps/x/packages/shared/src/index.ts | 1 + apps/x/packages/shared/src/prompt-block.ts | 8 + 5 files changed, 177 insertions(+) create mode 100644 apps/x/apps/renderer/src/extensions/prompt-block.tsx create mode 100644 apps/x/packages/shared/src/prompt-block.ts diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 96b409b9..f933b604 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -2791,6 +2791,27 @@ function App() { return () => window.removeEventListener('rowboat:open-copilot-edit-track', handler as EventListener) }, [submitFromPalette]) + // Listener for prompt-block "Run" events + // (dispatched by apps/renderer/src/extensions/prompt-block.tsx) + useEffect(() => { + const handler = (e: Event) => { + const ev = e as CustomEvent<{ + instruction?: string + filePath?: string + label?: string + }> + const instruction = ev.detail?.instruction + const filePath = ev.detail?.filePath + if (!instruction) return + const mention = filePath + ? { path: filePath, displayName: filePath.split('/').pop() ?? filePath } + : null + submitFromPalette(instruction, mention) + } + window.addEventListener('rowboat:open-copilot-prompt', handler as EventListener) + return () => window.removeEventListener('rowboat:open-copilot-prompt', handler as EventListener) + }, [submitFromPalette]) + const toggleKnowledgePane = useCallback(() => { setIsRightPaneMaximized(false) setIsChatSidebarOpen(prev => !prev) diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index f106c0a5..e997141d 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -12,6 +12,7 @@ import type { JSONContent, MarkdownRendererHelpers } from '@tiptap/react' import { ImageUploadPlaceholderExtension, createImageUploadHandler } from '@/extensions/image-upload' import { TaskBlockExtension } from '@/extensions/task-block' import { TrackBlockExtension } from '@/extensions/track-block' +import { PromptBlockExtension } from '@/extensions/prompt-block' import { TrackTargetOpenExtension, TrackTargetCloseExtension } from '@/extensions/track-target' import { ImageBlockExtension } from '@/extensions/image-block' import { EmbedBlockExtension } from '@/extensions/embed-block' @@ -690,6 +691,7 @@ export const MarkdownEditor = forwardRef } + extension: { options: { notePath?: string } } +}) { + const raw = node.attrs.data as string + + const prompt = useMemo | null>(() => { + try { + return PromptBlockSchema.parse(parseYaml(raw)) + } catch { return null } + }, [raw]) + + const notePath = extension.options.notePath + + const handleRun = (e: React.MouseEvent) => { + e.stopPropagation() + if (!prompt) return + window.dispatchEvent(new CustomEvent('rowboat:open-copilot-prompt', { + detail: { + instruction: prompt.instruction, + label: prompt.label, + filePath: notePath, + }, + })) + } + + const handleKey = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleRun(e as unknown as React.MouseEvent) + } + } + + if (!prompt) { + return ( + +
+ Invalid prompt block — expected YAML with label and instruction. +
+
+ ) + } + + return ( + +
e.stopPropagation()} + title={prompt.instruction} + className="flex items-center gap-3 rounded-xl border border-border bg-card p-3 pr-4 text-left transition-colors hover:bg-accent/50 cursor-pointer w-full my-2" + > +
+ +
+
+
{prompt.label}
+
{truncate(prompt.instruction, 80)}
+
+ +
+
+ ) +} + +export const PromptBlockExtension = Node.create({ + name: 'promptBlock', + group: 'block', + atom: true, + selectable: true, + draggable: false, + + addOptions() { + return { + notePath: undefined as string | undefined, + } + }, + + addAttributes() { + return { + data: { + default: '', + }, + } + }, + + parseHTML() { + return [ + { + tag: 'pre', + priority: 60, + getAttrs(element) { + const code = element.querySelector('code') + if (!code) return false + const cls = code.className || '' + if (cls.includes('language-prompt')) { + return { data: code.textContent || '' } + } + return false + }, + }, + ] + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'prompt-block' })] + }, + + addNodeView() { + return ReactNodeViewRenderer(PromptBlockView) + }, + + addStorage() { + return { + markdown: { + serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) { + state.write('```prompt\n' + node.attrs.data + '\n```') + state.closeBlock(node) + }, + parse: { + // handled by parseHTML + }, + }, + } + }, +}) diff --git a/apps/x/packages/shared/src/index.ts b/apps/x/packages/shared/src/index.ts index 5bdc49fd..cde673b8 100644 --- a/apps/x/packages/shared/src/index.ts +++ b/apps/x/packages/shared/src/index.ts @@ -10,6 +10,7 @@ export * as serviceEvents from './service-events.js' export * as inlineTask from './inline-task.js'; export * as blocks from './blocks.js'; export * as trackBlock from './track-block.js'; +export * as promptBlock from './prompt-block.js'; export * as frontmatter from './frontmatter.js'; export * as bases from './bases.js'; export * as browserControl from './browser-control.js'; diff --git a/apps/x/packages/shared/src/prompt-block.ts b/apps/x/packages/shared/src/prompt-block.ts new file mode 100644 index 00000000..a11d1af5 --- /dev/null +++ b/apps/x/packages/shared/src/prompt-block.ts @@ -0,0 +1,8 @@ +import z from 'zod'; + +export const PromptBlockSchema = z.object({ + label: z.string().min(1).describe('Short title shown on the card'), + instruction: z.string().min(1).describe('Full prompt sent to Copilot when Run is clicked'), +}); + +export type PromptBlock = z.infer;