feat: refactoring rendering

This commit is contained in:
2026-03-12 16:56:53 +01:00
parent 79e4586ed1
commit 084863909a
15 changed files with 327 additions and 196 deletions

View File

@@ -1,7 +1,9 @@
/**
* Registry of config node types. Each type defines syntax highlighting,
* insert-menu blocks, and how to render the content (after Nunjucks) for the renderer.
* Add a new entry here and wire its language in ConfigNode to add a new config type.
* Config (output) types: render-step contract and registry.
*
* Each type implements the "Render" step of the pipeline (see lib/graph/rendering.ts):
* it turns resolved content into HTML/SVG and declares how to display it (image vs html).
* Also used by ConfigNode for syntax, insert blocks, and language.
*/
export type ConfigTypeId = 'plantuml' | 'markdown' | 'wireframe'
@@ -38,18 +40,14 @@ export function outputMenuActionRequiresSvg(action: OutputMenuAction): boolean {
return action === 'downloadSvg' || action === 'downloadPng' || action === 'copySvg' || action === 'copyPng'
}
/** Config type: implements {@link IOutputTypeRenderer} and adds editor/insert options for ConfigNode. */
export type ConfigType = {
id: ConfigTypeId
label: string
/** How the RenderingNode should display output: 'html' (div) or 'image' (viewport). */
outputType: ConfigOutputType
/** CodeMirror language key; used to pick the extension in ConfigNode. */
language: ConfigTypeId
/** Options under Insert → [type-specific]. Can be flat blocks or groups. */
insertBlocks: InsertBlockOrGroup[]
/** Render resolved content (after Nunjucks) to HTML/SVG string for the renderer. */
render: (content: string, options?: RenderOptions) => Promise<string>
/** Optional. Output menu items for the Rendering node when this type is shown (e.g. Export submenu for image types). */
outputMenuDescriptor?: OutputMenuDescriptor
}

View File

@@ -1,7 +1,7 @@
/**
* Extensible node type registry. Register node types with registerNodeType() or use NodeTypeBuilder.
* Built-in types are registered in registerBuiltinNodes.ts.
* Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI.
* Extensible node type registry. The descriptor is the central place for what a node type uses:
* rendering logic (when it feeds the Renderer), Output menu content (when it is the Renderer's source),
* and any extra Node menu content. Use the builder in each node's descriptor so all behavior is defined in one place.
*/
import type React from 'react'
@@ -48,8 +48,20 @@ export type NodeTypeDescriptor = {
connectionLabel?: string
/** When true, double-clicking the node header opens a fullscreen dialog for this node. */
supportsFullscreen?: boolean
/** When set, this type can feed the Renderer; registration will also register source rendering logic. */
/**
* When set, this type can feed the Renderer; registration will also register source rendering logic.
* Define resolve step and default update mode here.
*/
sourceRenderingLogic?: SourceRenderingLogic
/**
* When this type is the Renderer's source, provide Output menu content (e.g. Export submenu).
* Called with outputTypeId (e.g. 'plantuml') and context (state + render nodeId). Return null for no extra menu.
*/
getOutputMenuContent?: (outputTypeId: string, ctx: unknown) => React.ReactNode
/**
* Optional extra content in the Node menu (before Delete). Use for type-specific actions.
*/
getNodeMenuExtraContent?: (nodeId: string, data: unknown) => React.ReactNode
}
const registry = new Map<string, NodeTypeDescriptor>()

View File

@@ -1,19 +1,15 @@
/**
* Fluent builder for NodeTypeDescriptor. Use to define node types with common behavior
* and optional source rendering logic (for types that feed the Renderer).
* Fluent builder for NodeTypeDescriptor. Use to define node types with common behavior,
* rendering logic, and menus in one place. Looking at the builder chain shows everything the node uses.
*
* - sourceRenderingLogic: when this type feeds the Renderer (resolve step, update mode).
* - outputMenuContent: when this type is the Renderer's source, provide Output menu (e.g. Export).
* - nodeMenuExtraContent: extra items in the Node menu (before Delete).
*
* Example:
* createNodeTypeBuilder('config', ConfigNode, { width: 320, height: 320 }, { configType: 'plantuml', content: '', title: '' })
* .idPrefix('cfg_')
* .withInputOutput(true, true)
* .classification('psyche')
* .allowedSourceTypes(['config', 'variable', 'function', 'data'])
* .allowedTargetTypes(['config', 'render', 'agent'])
* .help(NODE_HELP.config)
* .menu('Config', <ScrollText />)
* .connectionLabel('adding input')
* .withFullscreen()
* createNodeTypeBuilder('config', ConfigNode, ...)
* .sourceRenderingLogic({ defaultUpdateMode: 'auto', getResolvedContent: ... })
* .outputMenuContent((outputTypeId, ctx) => <ExportSubmenu ... />)
* .build()
*/
@@ -116,6 +112,18 @@ export class NodeTypeBuilder {
return this
}
/** When this type is the Renderer's source, provide Output menu content (e.g. Export submenu). */
outputMenuContent(fn: (outputTypeId: string, ctx: unknown) => React.ReactNode): this {
this.partial.getOutputMenuContent = fn
return this
}
/** Extra content in the Node menu (before Delete). Use for type-specific actions. */
nodeMenuExtraContent(fn: (nodeId: string, data: unknown) => React.ReactNode): this {
this.partial.getNodeMenuExtraContent = fn
return this
}
build(): NodeTypeDescriptor {
const {
idPrefix,
@@ -125,6 +133,8 @@ export class NodeTypeBuilder {
menuLabel,
menuIcon,
sourceRenderingLogic,
getOutputMenuContent,
getNodeMenuExtraContent,
...rest
} = this.partial
if (idPrefix == null || hasInput == null || hasOutput == null || !help || !menuLabel || menuIcon == null) {
@@ -145,9 +155,9 @@ export class NodeTypeBuilder {
menuIcon,
...rest,
}
if (sourceRenderingLogic != null) {
descriptor.sourceRenderingLogic = sourceRenderingLogic
}
if (sourceRenderingLogic != null) descriptor.sourceRenderingLogic = sourceRenderingLogic
if (getOutputMenuContent != null) descriptor.getOutputMenuContent = getOutputMenuContent
if (getNodeMenuExtraContent != null) descriptor.getNodeMenuExtraContent = getNodeMenuExtraContent
return descriptor
}
}

View File

@@ -0,0 +1,105 @@
/**
* Rendering pipeline interface and public API.
*
* ## Pipeline (3 steps)
*
* 1. **Resolve** — A source node (config, agent) implements `IRenderingSource`. Given a
* `IRenderingContext` (graph, source id, callbacks), it produces a `IResolveResult`:
* resolved string, which output type to use, and optional reasoning text.
*
* 2. **Render** — The output type (e.g. plantuml, markdown, wireframe) implements
* `IOutputTypeRenderer`. It turns the resolved string into HTML or SVG via `render()`.
* Config types in configTypes.ts are the built-in implementations.
*
* 3. **Display** — The Rendering node picks a view (image viewport vs markdown content)
* from `outputType` on the renderer and shows the result. Output menu items (e.g. Export)
* come from the source/renderer via `outputMenuDescriptor`.
*
* ## Implementing a new source
* - Implement `IRenderingSource` (resolve + optional output menu).
* - Register with `registerSourceRenderingLogic(nodeType, logic)`.
*
* ## Implementing a new output type
* - Add a config type (or equivalent) fulfilling `IOutputTypeRenderer`.
* - Register in CONFIG_TYPES and use `getConfigType(id)` in the resolve step.
*/
import type {
ConfigTypeId,
ConfigOutputType,
OutputMenuDescriptor,
OutputMenuItemDescriptor,
OutputMenuAction,
RenderOptions,
} from './configTypes'
import type {
SourceRenderingLogicContext,
ResolvedContentResult,
SourceRenderingLogic,
} from './sourceRenderingLogic'
// ---------------------------------------------------------------------------
// Contracts (interfaces)
// ---------------------------------------------------------------------------
/** Input to the resolve step: graph state and callbacks for the source to use. */
export type IRenderingContext = SourceRenderingLogicContext
/**
* Result of the resolve step. The source produces a resolved string and declares
* which output type (renderer) to use for the render step.
*/
export interface IResolveResult {
/** Resolved content (e.g. after Nunjucks, or agent output). */
resolved: string
/** Which output type will render this (e.g. 'plantuml', 'markdown'). */
outputTypeId: ConfigTypeId
/** Optional reasoning block for the UI (e.g. agent reasoning). */
reasoning?: string
}
/**
* Contract for a node type that can feed the Rendering node (config, agent, etc.).
* Register via registerSourceRenderingLogic(nodeType, logic).
* Output menu content is registered per output type in the frontend (outputMenuRegistry).
*/
export interface IRenderingSource {
/** When to re-run: 'auto' on upstream changes, 'manual' only on Run. */
defaultUpdateMode: 'auto' | 'manual'
/** Resolve step: produce content and choose output type. */
getResolvedContent(context: IRenderingContext): Promise<IResolveResult>
}
/**
* Contract for an output type that turns resolved content into HTML/SVG.
* Config types (plantuml, markdown, wireframe) implement this.
*/
export interface IOutputTypeRenderer {
id: ConfigTypeId
/** How to display: 'image' (viewport) or 'html' (scrollable div). */
outputType: ConfigOutputType
/** Render resolved content to HTML or SVG string. */
render(content: string, options?: RenderOptions): Promise<string>
/** Optional: output menu items when this type is shown (e.g. Export). */
outputMenuDescriptor?: OutputMenuDescriptor
}
// ---------------------------------------------------------------------------
// Re-exports: source logic (resolve step)
// ---------------------------------------------------------------------------
export type { SourceRenderingLogic, ResolvedContentResult, SourceRenderingLogicContext }
export { registerSourceRenderingLogic, getSourceRenderingLogic } from './sourceRenderingLogic'
// ---------------------------------------------------------------------------
// Re-exports: output types (render step) and menu
// ---------------------------------------------------------------------------
export type { ConfigTypeId, ConfigOutputType, OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction, RenderOptions }
export { getConfigType, getConfigTypeId, getConfigContent, outputMenuActionRequiresSvg } from './configTypes'
// ---------------------------------------------------------------------------
// Re-exports: shared utils (pure helpers used by hook and views)
// ---------------------------------------------------------------------------
export { parseThinkSections, processSvgDisplay, stripTemplateSyntax } from './renderingUtils'

View File

@@ -1,16 +1,15 @@
/**
* Source rendering logic: each node type that can feed the Rendering node
* registers how to get "resolved" content and default update behavior.
* The Rendering node uses this to run the right logic and respect auto vs manual updates.
* Source rendering logic: resolve-step contract and registry.
*
* Register in registerBuiltinNodes (or at app init) via registerSourceRenderingLogic(nodeType, logic).
* Node types that are allowed sources for the Renderer should register here (see nodeRegistry NodeTypeDescriptor).
* Implements the "Resolve" step of the rendering pipeline (see lib/graph/rendering.ts).
* Each node type that can feed the Rendering node registers an implementation of
* {@link IRenderingSource} via registerSourceRenderingLogic(nodeType, logic).
*/
import type { ConfigTypeId, OutputMenuDescriptor } from './configTypes'
import type { ConfigTypeId } from './configTypes'
export type { OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction } from './configTypes'
/** Context passed into the resolve step (graph, source id, callbacks). See {@link IRenderingContext}. */
export type SourceRenderingLogicContext = {
nodes: { id: string; type?: string; data?: unknown }[]
edges: { id: string; source: string; target: string }[]
@@ -18,38 +17,23 @@ export type SourceRenderingLogicContext = {
renderNodeId: string
viewportWidth?: number
viewportHeight?: number
/** Optional: allows source logic to update other nodes (e.g. agent run updates agent node). */
setNodes?: (updater: (nodes: { id: string; type?: string; data?: unknown }[]) => { id: string; type?: string; data?: unknown }[]) => void
/** Optional: AI connection for agent source (used when Rendering node runs the agent). */
aiConnection?: unknown
/** Optional: called when agent streaming starts (Rendering node can show streaming preview). */
onStreamingStart?: () => void
/** Optional: called with each text chunk during agent stream (Rendering node can update preview). */
onStreamingChunk?: (chunk: string) => void
}
/**
* Result of getResolvedContent: resolved string plus the config type to use for final render.
* When the source is an agent with reasoning enabled, reasoning may be set for a collapsible section.
*/
/** Result of the resolve step. See {@link IResolveResult}. */
export type ResolvedContentResult = {
resolved: string
outputTypeId: ConfigTypeId
/** Optional reasoning section (e.g. agent with reasoning enabled); renderer shows it in a collapsible. */
reasoning?: string
}
/**
* Rendering logic provided by a source node type (e.g. config, agent).
* - defaultUpdateMode: 'auto' = re-render on upstream changes; 'manual' = only on Run
* - getResolvedContent: async resolve step; returns resolved string and which renderer (ConfigTypeId) to use
* - getOutputMenuDescriptor: optional; returns output menu items for the Rendering node (e.g. Export submenu for image types)
*/
/** Source logic implementation. Implements {@link IRenderingSource}. Output menu content is provided per output type via the frontend registry (see outputMenuRegistry). */
export type SourceRenderingLogic = {
defaultUpdateMode: 'auto' | 'manual'
getResolvedContent: (context: SourceRenderingLogicContext) => Promise<ResolvedContentResult>
/** Optional. When the renderer displays this source's output, which extra output menu items to show (e.g. Export SVG/PNG). */
getOutputMenuDescriptor?: (outputTypeId: ConfigTypeId) => OutputMenuDescriptor | null
}
const registry = new Map<string, SourceRenderingLogic>()