From f0ff080ffe4892a1375b7578b03e285d11937acf Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 07:08:28 -0700
Subject: [PATCH 01/28] feat: integrate excalidraw-mcp as first-class
diagramming feature
Wire up MCPAppsMiddleware on the LangGraph agent so Excalidraw widget
HTML is properly sent to the frontend (fixes blank canvas). Add
progressive iframe streaming via postMessage to preserve JS state
during updates, and a 75% focused modal for expanding widgets.
Include an Excalidraw diagram skill for the agent with camera reveal
patterns, color grammar, and element reference snippets.
Closes #11
---
.../agent/skills/excalidraw-diagram-skill.txt | 211 ++++++++++++++++++
apps/app/src/app/api/copilotkit/route.ts | 21 +-
.../generative-ui/widget-renderer.tsx | 192 +++++++++++-----
3 files changed, 358 insertions(+), 66 deletions(-)
create mode 100644 apps/agent/skills/excalidraw-diagram-skill.txt
diff --git a/apps/agent/skills/excalidraw-diagram-skill.txt b/apps/agent/skills/excalidraw-diagram-skill.txt
new file mode 100644
index 0000000..eb52786
--- /dev/null
+++ b/apps/agent/skills/excalidraw-diagram-skill.txt
@@ -0,0 +1,211 @@
+# Excalidraw Diagram Skill
+
+Create beautiful, professional, animated Excalidraw diagrams with progressive camera reveals, color-coded zones, and polished visual design. Use this skill whenever a user asks to diagram, visualize, map, chart, illustrate, or draw anything — including architecture diagrams, flowcharts, sequence diagrams, concept explainers, system maps, process flows, and technical overviews. Also trigger for requests like "show me how X works", "draw a diagram of", "create a visual for", "make an Excalidraw of", or any time a visual explanation would be clearer than text alone.
+
+---
+
+## Step 1 — Always call read_me first
+
+Before emitting ANY elements, call `Excalidraw:read_me`. Do not skip this step, even for simple diagrams. It provides the color palette, camera sizes, font rules, and element syntax required to produce clean output.
+
+```
+Excalidraw:read_me()
+```
+
+Then proceed directly to `Excalidraw:create_view` with your elements array — no narration about the read_me call.
+
+---
+
+## Step 2 — Plan the diagram before writing elements
+
+Before writing elements, mentally sketch:
+
+1. **What are the layers / zones?** (e.g. Frontend / Backend / Database, or Input / Process / Output)
+2. **What color grammar makes sense?** Assign one color per layer and keep it consistent throughout
+3. **How many camera positions do I need?** Plan 3–6 camera stops minimum for a reveal effect
+4. **What's the reading order?** Left-to-right or top-to-bottom; pick one and stick to it
+
+---
+
+## Step 3 — Core design rules (MUST follow)
+
+### Camera rules
+- **Always start with `cameraUpdate` as the first element**
+- Camera sizes MUST be exact 4:3 ratios: `400x300`, `600x450`, `800x600`, `1200x900`, `1600x1200`
+- Use **multiple cameraUpdates** throughout the array — pan to each section as you draw it
+- Leave padding: if content is 500px wide, use 800x600 camera
+- Final element should be a wide cameraUpdate showing the full diagram
+
+### Color grammar (use consistently)
+
+| Zone / Role | Fill | Stroke |
+|---------------------|---------------|-----------|
+| UI / Frontend | `#dbe4ff` | `#4a9eed` |
+| Logic / Agent | `#e5dbff` | `#8b5cf6` |
+| Data / Storage | `#d3f9d8` | `#22c55e` |
+| External / API | `#ffd8a8` | `#f59e0b` |
+| Error / Alert | `#ffc9c9` | `#ef4444` |
+| Notes / Decisions | `#fff3bf` | `#f59e0b` |
+
+Zone background rectangles: use `opacity: 40`, `fillStyle: "solid"`
+
+Node shapes: use pastel fills (`#a5d8ff`, `#b2f2bb`, `#d0bfff`, `#ffd8a8`, `#c3fae8`, `#eebefa`)
+
+### Typography rules
+- Title: `fontSize: 26–28`, `strokeColor: "#1e1e1e"`
+- Subtitle / annotation: `fontSize: 16`, `strokeColor: "#757575"`
+- Shape labels: `fontSize: 16–18` via `label` property on the shape
+- NEVER use fontSize below 14
+- NEVER use light gray on white backgrounds (minimum text color: `#757575`)
+
+### Shape rules
+- Use `label: { "text": "...", "fontSize": 16 }` directly on shapes — no separate text elements
+- Minimum shape size: `120x60` for labeled boxes
+- Add `roundness: { type: 3 }` for rounded corners (preferred for nodes)
+- Leave 20–30px gaps between elements
+
+### Drawing order (z-order, critical)
+Emit in this sequence per section:
+1. Zone background rectangle (drawn first = sits behind)
+2. Zone label text
+3. Node shapes (with labels)
+4. Arrows between nodes
+5. Then next section
+
+NEVER dump all rectangles, then all text, then all arrows.
+
+### Arrow rules
+- Always include `endArrowhead: "arrow"` for directional flow
+- Use `strokeStyle: "dashed"` for responses, return values, optional paths
+- Keep arrow labels short (under 20 chars) or omit — long labels overflow
+- Use `startBinding` / `endBinding` with `fixedPoint` to attach to shapes
+
+---
+
+## Step 4 — Diagram type patterns
+
+### Architecture / System Diagram
+Zones as swim lanes (left-to-right or top-to-bottom). Each zone = one architectural layer. Arrows show data/request flow between layers. End with a full-width cameraUpdate.
+
+**Camera pattern:** Title zoom (M) → pan right zone by zone (S/M) → final overview (XL)
+
+### Sequence / Flow Diagram
+Actors as header boxes with dashed vertical lifelines. Horizontal arrows show messages. Pan camera downward as messages progress.
+
+**Camera pattern:** Title (M) → pan right per actor drawing header + lifeline → zoom out (L) → pan down per message group → final overview (XL)
+
+### Concept Explainer
+Start zoomed on the title, then reveal parts of the concept one at a time. Use annotations (`#fff3bf` boxes) as callouts. Simple left-to-right flow.
+
+**Camera pattern:** Title zoom (S) → zoom out (M) → pan section by section → final (L)
+
+### Process / Flowchart
+Diamonds for decisions, rectangles for steps. Top-to-bottom flow. Color-code by stage (e.g. initiation=blue, processing=purple, output=green).
+
+**Camera pattern:** Top zoom → pan down per stage group → final overview
+
+---
+
+## Step 5 — The camera reveal technique (what makes diagrams feel alive)
+
+The secret to great Excalidraw diagrams is **drawing section by section with camera moves**:
+
+```json
+// 1. Start with title, zoomed in
+{"type":"cameraUpdate","width":600,"height":450,"x":100,"y":0},
+{"type":"text","id":"t1","x":200,"y":20,"text":"My Diagram","fontSize":28},
+
+// 2. Pan to first zone and draw it
+{"type":"cameraUpdate","width":400,"height":300,"x":20,"y":60},
+{"type":"rectangle","id":"zone1", ...zone background...},
+{"type":"rectangle","id":"node1", ...node with label...},
+
+// 3. Pan to second zone
+{"type":"cameraUpdate","width":400,"height":300,"x":280,"y":60},
+{"type":"rectangle","id":"zone2", ...},
+{"type":"rectangle","id":"node2", ...},
+
+// 4. Draw connecting arrows (camera stays or pans to show both ends)
+{"type":"cameraUpdate","width":800,"height":600,"x":0,"y":40},
+{"type":"arrow","id":"a1", ...arrow from node1 to node2...},
+
+// 5. Final wide overview
+{"type":"cameraUpdate","width":1200,"height":900,"x":-20,"y":-10}
+```
+
+This creates the "drawing itself" animation effect users love.
+
+---
+
+## Step 6 — Common mistakes to avoid
+
+- **No cameraUpdate first** → diagram appears un-framed, elements clip
+- **Wrong aspect ratio** → `700x500` causes distortion; use `800x600`
+- **All elements at once, no panning** → loses the reveal animation
+- **Overlapping elements** → check y-coordinates leave 60–80px between rows
+- **Long arrow labels** → overflow the arrow; keep under 20 chars or use a note box instead
+- **Emoji in text** → don't render in Excalidraw's font
+- **Light text on white** → `#b0b0b0` on white is invisible; minimum `#757575`
+- **Zone label covered by nodes** → put zone label text at top-left of zone (y + 8px from zone top), nodes start 40px below
+- **Title not centered** → estimate `text.length x fontSize x 0.5` for width, then set `x = diagramCenterX - estimatedWidth/2`
+
+---
+
+## Step 7 — Quality checklist before emitting
+
+- [ ] `Excalidraw:read_me` called
+- [ ] First element is `cameraUpdate`
+- [ ] All camera sizes are valid 4:3 ratios
+- [ ] Minimum 3 camera positions used (more = better animation)
+- [ ] Color grammar is consistent across zones
+- [ ] All shape labels use `label` property, not separate text elements
+- [ ] No font sizes below 14
+- [ ] Zone backgrounds are drawn BEFORE the nodes inside them
+- [ ] Arrows drawn AFTER both source and target shapes
+- [ ] Final element is a wide cameraUpdate revealing the full diagram
+- [ ] No emoji in any text strings
+
+---
+
+## Reference: Element snippets
+
+**Zone background:**
+```json
+{"type":"rectangle","id":"zone_bg","x":20,"y":80,"width":220,"height":380,"backgroundColor":"#dbe4ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":1,"opacity":40}
+```
+
+**Zone label:**
+```json
+{"type":"text","id":"zone_lbl","x":40,"y":88,"text":"FRONTEND","fontSize":14,"strokeColor":"#2563eb"}
+```
+
+**Node:**
+```json
+{"type":"rectangle","id":"n1","x":60,"y":130,"width":150,"height":55,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"label":{"text":"API Gateway","fontSize":16}}
+```
+
+**Arrow (solid, directed):**
+```json
+{"type":"arrow","id":"a1","x":210,"y":157,"width":100,"height":0,"points":[[0,0],[100,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","startBinding":{"elementId":"n1","fixedPoint":[1,0.5]},"endBinding":{"elementId":"n2","fixedPoint":[0,0.5]}}
+```
+
+**Arrow (dashed, response):**
+```json
+{"type":"arrow","id":"a2","x":310,"y":157,"width":-100,"height":0,"points":[[0,0],[-100,0]],"strokeColor":"#757575","strokeWidth":2,"strokeStyle":"dashed","endArrowhead":"arrow"}
+```
+
+**Annotation note:**
+```json
+{"type":"rectangle","id":"note1","x":80,"y":200,"width":200,"height":36,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":1,"opacity":80,"label":{"text":"Caches for 5 min","fontSize":14}}
+```
+
+**Title text:**
+```json
+{"type":"text","id":"title","x":150,"y":15,"text":"System Architecture","fontSize":28,"strokeColor":"#1e1e1e"}
+```
+
+**Stick figure (user icon):**
+```json
+{"type":"ellipse","id":"fig_head","x":58,"y":110,"width":20,"height":20,"backgroundColor":"#a5d8ff","fillStyle":"solid","strokeColor":"#4a9eed","strokeWidth":2},
+{"type":"rectangle","id":"fig_body","x":57,"y":132,"width":22,"height":26,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2}
+```
diff --git a/apps/app/src/app/api/copilotkit/route.ts b/apps/app/src/app/api/copilotkit/route.ts
index 9c73d9b..832d831 100644
--- a/apps/app/src/app/api/copilotkit/route.ts
+++ b/apps/app/src/app/api/copilotkit/route.ts
@@ -4,6 +4,7 @@ import {
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
+import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware";
import { NextRequest } from "next/server";
// 1. Define the agent connection to LangGraph
@@ -13,21 +14,25 @@ const defaultAgent = new LangGraphAgent({
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
+// 2. Wire up MCP apps middleware so widget HTML is sent to the frontend
+defaultAgent.use(
+ new MCPAppsMiddleware({
+ mcpServers: [{
+ type: "http",
+ url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
+ serverId: "example_mcp_app",
+ }],
+ })
+);
+
// 3. Define the route and CopilotRuntime for the agent
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
- agents: { default: defaultAgent, },
+ agents: { default: defaultAgent },
a2ui: { injectA2UITool: true },
- mcpApps: {
- servers: [{
- type: "http",
- url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
- serverId: "example_mcp_app",
- }],
- },
}),
});
diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx
index f978385..0cc367f 100644
--- a/apps/app/src/components/generative-ui/widget-renderer.tsx
+++ b/apps/app/src/components/generative-ui/widget-renderer.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useRef, useState, useCallback } from "react";
+import { useEffect, useLayoutEffect, useRef, useState, useCallback } from "react";
import { z } from "zod";
// ─── Zod Schema (CopilotKit parameter contract) ─────────────────────
@@ -358,6 +358,17 @@ window.addEventListener('load', reportHeight);
// Periodic reports during initial load
var _resizeInterval = setInterval(reportHeight, 200);
setTimeout(function() { clearInterval(_resizeInterval); }, 15000);
+
+// Patch: receive incremental HTML updates without full reload
+window.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'update-content' && typeof e.data.html === 'string') {
+ var content = document.getElementById('content');
+ if (content) {
+ content.innerHTML = e.data.html;
+ reportHeight();
+ }
+ }
+});
`;
// ─── Document Assembly ───────────────────────────────────────────────
@@ -425,8 +436,10 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
const iframeRef = useRef(null);
const [height, setHeight] = useState(0);
const [loaded, setLoaded] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
// Track what html has been committed to the iframe to avoid redundant reloads
const committedHtmlRef = useRef("");
+ const isFirstRenderRef = useRef(true);
const handleMessage = useCallback((e: MessageEvent) => {
// Only handle messages from our own iframe
@@ -445,19 +458,37 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
return () => window.removeEventListener("message", handleMessage);
}, [handleMessage]);
- // Write to iframe imperatively — bypasses React reconciliation so the
- // iframe only reloads when the html *content* truly changes, preserving
- // internal JS state (Three.js scenes, step counters, etc.) across
- // CopilotKit re-renders.
- useEffect(() => {
+ // Write to iframe imperatively — first render sets srcdoc (executes scripts),
+ // subsequent streaming updates patch #content.innerHTML via postMessage
+ // to preserve JS state (Three.js scenes, step counters, etc.).
+ useLayoutEffect(() => {
if (!html || !iframeRef.current) return;
if (html === committedHtmlRef.current) return;
committedHtmlRef.current = html;
- iframeRef.current.srcdoc = assembleDocument(html);
- setLoaded(false);
- setHeight(0);
+
+ if (isFirstRenderRef.current) {
+ isFirstRenderRef.current = false;
+ iframeRef.current.srcdoc = assembleDocument(html);
+ setLoaded(false);
+ setHeight(0);
+ } else {
+ iframeRef.current.contentWindow?.postMessage(
+ { type: "update-content", html },
+ "*"
+ );
+ }
}, [html]);
+ // Escape key exits fullscreen
+ useEffect(() => {
+ if (!isFullscreen) return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") setIsFullscreen(false);
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [isFullscreen]);
+
// Fallback: if iframe has html but hasn't reported ready after 4s, force-show
useEffect(() => {
if (!html || (loaded && height > 0)) return;
@@ -474,65 +505,110 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
const loadingPhrase = useLoadingPhrase(showLoading);
return (
-
- )}
- {/* Iframe: always mounted so ref is stable; srcdoc set imperatively.
- No srcDoc React prop — prevents React from reloading the iframe
- on parent re-renders. */}
-
+ )}
+ {/* Single iframe — always mounted so ref is stable */}
+
+ >
+
);
}
From 27e91f73092e283cd7d61f03aa3f24c8b33335fa Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 07:12:35 -0700
Subject: [PATCH 02/28] fix: give excalidraw skill its own section in agent
system prompt
The excalidraw skill targets MCP tools (read_me, create_view) not the
widgetRenderer component, so it needs its own top-level section in the
system prompt rather than being nested under widgetRenderer instructions.
---
apps/agent/main.py | 20 ++++++++++++++++----
apps/agent/skills/__init__.py | 11 +++++++++--
2 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/apps/agent/main.py b/apps/agent/main.py
index 35aadda..e7bf0ef 100644
--- a/apps/agent/main.py
+++ b/apps/agent/main.py
@@ -10,10 +10,11 @@
from src.query import query_data
from src.todos import AgentState, todo_tools
from src.form import generate_form
-from skills import load_all_skills
+from skills import load_all_skills, load_skill
-# Load all visualization skills
-_skills_text = load_all_skills()
+# Load visualization skills (excalidraw loaded separately — it targets MCP tools, not widgetRenderer)
+_widget_skills_text = load_all_skills(exclude=["excalidraw-diagram-skill"])
+_excalidraw_skill_text = load_skill("excalidraw-diagram-skill")
agent = create_agent(
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
@@ -46,7 +47,18 @@
Follow the skills below for how to produce high-quality visuals:
- {_skills_text}
+ {_widget_skills_text}
+
+ ## Excalidraw Diagramming Skills
+
+ You also have access to Excalidraw MCP tools (`Excalidraw:read_me` and
+ `Excalidraw:create_view`) for creating animated, interactive diagrams.
+
+ When a user asks you to draw a diagram, flowchart, architecture map, or any
+ visual that would benefit from the Excalidraw canvas — use the Excalidraw MCP
+ tools instead of `widgetRenderer`. Follow the skill below exactly:
+
+ {_excalidraw_skill_text}
""",
)
diff --git a/apps/agent/skills/__init__.py b/apps/agent/skills/__init__.py
index a871edf..b4a5a88 100644
--- a/apps/agent/skills/__init__.py
+++ b/apps/agent/skills/__init__.py
@@ -17,10 +17,17 @@ def load_skill(name: str) -> str:
return path.read_text()
-def load_all_skills() -> str:
- """Load and concatenate all .txt skill files in this directory."""
+def load_all_skills(exclude: list[str] | None = None) -> str:
+ """Load and concatenate all .txt skill files in this directory.
+
+ Args:
+ exclude: Optional list of skill names (without extension) to skip.
+ """
+ skip = set(exclude or [])
parts: list[str] = []
for path in sorted(_SKILLS_DIR.glob("*.txt")):
+ if path.stem in skip:
+ continue
parts.append(f"\n\n{'='*60}\n# SKILL: {path.stem}\n{'='*60}\n\n")
parts.append(path.read_text())
return "".join(parts)
From e20a40983b0fd1173595915c78feba5b33f84df2 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 07:15:15 -0700
Subject: [PATCH 03/28] fix: center widget content in iframe
---
apps/app/src/components/generative-ui/widget-renderer.tsx | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx
index 0cc367f..5426af1 100644
--- a/apps/app/src/components/generative-ui/widget-renderer.tsx
+++ b/apps/app/src/components/generative-ui/widget-renderer.tsx
@@ -228,6 +228,12 @@ body {
-webkit-font-smoothing: antialiased;
}
+#content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
button {
font-family: inherit;
font-size: 14px;
From 7fd3913a30882e38bf8d17cc89170fe81711268a Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 07:20:20 -0700
Subject: [PATCH 04/28] fix: resolve hydration mismatch and cap widget height
Defer CopilotChat render to client-only to avoid Radix UI generating
different IDs on server vs client. Cap widget iframe height at 800px
to reduce whitespace from MCP widgets with large viewports.
---
apps/app/src/app/page.tsx | 18 ++++++++++++------
.../generative-ui/widget-renderer.tsx | 2 +-
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx
index 7994182..b5d3770 100644
--- a/apps/app/src/app/page.tsx
+++ b/apps/app/src/app/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { ExampleLayout } from "@/components/example-layout";
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
import { ExplainerCardsPortal } from "@/components/explainer-cards";
@@ -11,6 +11,10 @@ export default function HomePage() {
useGenerativeUIExamples();
useExampleSuggestions();
+ // Defer CopilotChat to client-only to avoid Radix hydration ID mismatch
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+
// Widget bridge: handle openLink from widget iframes
useEffect(() => {
const handler = (e: MessageEvent) => {
@@ -75,11 +79,13 @@ export default function HomePage() {
+ mounted ? (
+
+ ) : null
} />
diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx
index 5426af1..4c96ab5 100644
--- a/apps/app/src/components/generative-ui/widget-renderer.tsx
+++ b/apps/app/src/components/generative-ui/widget-renderer.tsx
@@ -455,7 +455,7 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
e.data?.type === "widget-resize" &&
typeof e.data.height === "number"
) {
- setHeight(Math.max(50, Math.min(e.data.height + 8, 4000)));
+ setHeight(Math.max(50, Math.min(e.data.height + 8, 800)));
}
}, []);
From 293135aace845d3b08980a2e0ca1fd0a6d80b6e6 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 07:26:20 -0700
Subject: [PATCH 05/28] feat: add zoom controls to widget renderer
Zoom in/out/fit buttons in the toolbar with CSS transform scaling.
Zoom persists across streaming updates and works in both inline
and fullscreen modes.
---
.../generative-ui/widget-renderer.tsx | 91 +++++++++++++++----
1 file changed, 73 insertions(+), 18 deletions(-)
diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx
index 4c96ab5..a617779 100644
--- a/apps/app/src/components/generative-ui/widget-renderer.tsx
+++ b/apps/app/src/components/generative-ui/widget-renderer.tsx
@@ -443,6 +443,7 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
const [height, setHeight] = useState(0);
const [loaded, setLoaded] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
+ const [zoom, setZoom] = useState(1);
// Track what html has been committed to the iframe to avoid redundant reloads
const committedHtmlRef = useRef("");
const isFirstRenderRef = useRef(true);
@@ -510,6 +511,22 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
const showLoading = !!html && !ready;
const loadingPhrase = useLoadingPhrase(showLoading);
+ const toolbarBtnStyle: React.CSSProperties = {
+ background: "transparent",
+ border: "none",
+ cursor: "pointer",
+ padding: "2px 0",
+ color: "var(--text-secondary, #6b7280)",
+ fontSize: 13,
+ fontFamily: "inherit",
+ width: 24,
+ height: 24,
+ display: "inline-flex",
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: 4,
+ };
+
return (
<>
{/* Fullscreen backdrop */}
@@ -547,12 +564,42 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
+ gap: 4,
padding: isFullscreen ? "8px 16px" : "0 0 4px 0",
...(isFullscreen ? { borderBottom: "1px solid var(--border-secondary, #e5e7eb)" } : {}),
}}>
+ {ready && (
+ <>
+
+
+ {Math.round(zoom * 100)}%
+
+
+
+
+ >
+ )}
@@ -596,23 +643,31 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
)}
- {/* Single iframe — always mounted so ref is stable */}
-
+
+ Get started
+
From 2ddc6bbd8bccae6eef8dbd1458bebdd7eff5da6b Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:05:07 -0700
Subject: [PATCH 19/28] refactor: simplify and strengthen Excalidraw skill with
overlap prevention focus
- Remove verbose sections and redundant explanations
- Add strict spacing requirements: 60px minimum vertical gaps, 40px horizontal
- Emphasize overlap prevention as core design principle
- Simplify diagram type patterns to essentials
- Reduce total length while maintaining critical information
- Add checklist for spacing/overlap verification
- Clarify y-coordinate calculations to prevent overlaps
---
.../agent/skills/excalidraw-diagram-skill.txt | 233 +++++++-----------
1 file changed, 93 insertions(+), 140 deletions(-)
diff --git a/apps/agent/skills/excalidraw-diagram-skill.txt b/apps/agent/skills/excalidraw-diagram-skill.txt
index eb52786..c41d10c 100644
--- a/apps/agent/skills/excalidraw-diagram-skill.txt
+++ b/apps/agent/skills/excalidraw-diagram-skill.txt
@@ -1,211 +1,164 @@
# Excalidraw Diagram Skill
-Create beautiful, professional, animated Excalidraw diagrams with progressive camera reveals, color-coded zones, and polished visual design. Use this skill whenever a user asks to diagram, visualize, map, chart, illustrate, or draw anything — including architecture diagrams, flowcharts, sequence diagrams, concept explainers, system maps, process flows, and technical overviews. Also trigger for requests like "show me how X works", "draw a diagram of", "create a visual for", "make an Excalidraw of", or any time a visual explanation would be clearer than text alone.
+Create clean, simple Excalidraw diagrams with progressive camera reveals. Use this skill when a user asks to diagram, visualize, flowchart, or illustrate anything.
---
## Step 1 — Always call read_me first
-Before emitting ANY elements, call `Excalidraw:read_me`. Do not skip this step, even for simple diagrams. It provides the color palette, camera sizes, font rules, and element syntax required to produce clean output.
-
-```
-Excalidraw:read_me()
-```
-
-Then proceed directly to `Excalidraw:create_view` with your elements array — no narration about the read_me call.
+Before creating any elements, call `Excalidraw:read_me()` to get the color palette, camera sizes, and element syntax. Then proceed directly to `Excalidraw:create_view`.
---
-## Step 2 — Plan the diagram before writing elements
+## Step 2 — Planning (critical for spacing)
-Before writing elements, mentally sketch:
+Before writing elements:
-1. **What are the layers / zones?** (e.g. Frontend / Backend / Database, or Input / Process / Output)
-2. **What color grammar makes sense?** Assign one color per layer and keep it consistent throughout
-3. **How many camera positions do I need?** Plan 3–6 camera stops minimum for a reveal effect
-4. **What's the reading order?** Left-to-right or top-to-bottom; pick one and stick to it
+1. **Sketch zones/layers** — What are the main sections? (Frontend/Backend, Input/Process/Output, etc.)
+2. **Assign colors** — One color per zone, used consistently
+3. **Plan camera positions** — 3–6 stops for the reveal effect
+4. **Plan spacing** — Draw it on paper first. Leave **minimum 60px vertical gaps** between rows, **40px horizontal gaps** between columns
---
-## Step 3 — Core design rules (MUST follow)
-
-### Camera rules
-- **Always start with `cameraUpdate` as the first element**
-- Camera sizes MUST be exact 4:3 ratios: `400x300`, `600x450`, `800x600`, `1200x900`, `1600x1200`
-- Use **multiple cameraUpdates** throughout the array — pan to each section as you draw it
-- Leave padding: if content is 500px wide, use 800x600 camera
-- Final element should be a wide cameraUpdate showing the full diagram
-
-### Color grammar (use consistently)
-
-| Zone / Role | Fill | Stroke |
-|---------------------|---------------|-----------|
-| UI / Frontend | `#dbe4ff` | `#4a9eed` |
-| Logic / Agent | `#e5dbff` | `#8b5cf6` |
-| Data / Storage | `#d3f9d8` | `#22c55e` |
-| External / API | `#ffd8a8` | `#f59e0b` |
-| Error / Alert | `#ffc9c9` | `#ef4444` |
-| Notes / Decisions | `#fff3bf` | `#f59e0b` |
-
-Zone background rectangles: use `opacity: 40`, `fillStyle: "solid"`
-
-Node shapes: use pastel fills (`#a5d8ff`, `#b2f2bb`, `#d0bfff`, `#ffd8a8`, `#c3fae8`, `#eebefa`)
-
-### Typography rules
-- Title: `fontSize: 26–28`, `strokeColor: "#1e1e1e"`
-- Subtitle / annotation: `fontSize: 16`, `strokeColor: "#757575"`
-- Shape labels: `fontSize: 16–18` via `label` property on the shape
-- NEVER use fontSize below 14
-- NEVER use light gray on white backgrounds (minimum text color: `#757575`)
-
-### Shape rules
-- Use `label: { "text": "...", "fontSize": 16 }` directly on shapes — no separate text elements
-- Minimum shape size: `120x60` for labeled boxes
-- Add `roundness: { type: 3 }` for rounded corners (preferred for nodes)
-- Leave 20–30px gaps between elements
-
-### Drawing order (z-order, critical)
-Emit in this sequence per section:
-1. Zone background rectangle (drawn first = sits behind)
+## Step 3 — Core rules (MUST follow)
+
+### Camera
+- Start with `cameraUpdate` as the **first element**
+- Use exact 4:3 ratios: `400x300`, `600x450`, `800x600`, `1200x900`, `1600x1200`
+- Pan to each section as you draw it
+- End with a wide view showing the full diagram
+
+### Spacing (prevent overlaps)
+- **Minimum vertical gap between rows: 60px**
+- **Minimum horizontal gap between columns: 40px**
+- Check all y-coordinates: if row 1 ends at y=180, row 2 starts at y=240 or later
+- Zone label at top-left (y+8px from zone top), nodes start 40px below label
+- Never place text directly on shape edges — add 8–10px padding
+
+### Color grammar
+| Zone | Fill | Stroke |
+|------|------|--------|
+| UI / Frontend | `#dbe4ff` | `#4a9eed` |
+| Logic / Agent | `#e5dbff` | `#8b5cf6` |
+| Data / Storage | `#d3f9d8` | `#22c55e` |
+| External / API | `#ffd8a8` | `#f59e0b` |
+| Error / Alert | `#ffc9c9` | `#ef4444` |
+| Notes | `#fff3bf` | `#f59e0b` |
+
+### Typography
+- Title: `fontSize: 28`, `strokeColor: "#1e1e1e"`
+- Labels: `fontSize: 16–18` via `label` property
+- **Never use fontSize below 14**
+- Minimum text color: `#757575` (never light gray on white)
+
+### Drawing order (z-order)
+Per section, emit in this order:
+1. Zone background rectangle (sits behind)
2. Zone label text
3. Node shapes (with labels)
4. Arrows between nodes
-5. Then next section
-NEVER dump all rectangles, then all text, then all arrows.
+**Never dump all rectangles first, then all text.**
+
+### Shapes
+- Use `label: { "text": "...", "fontSize": 16 }` on shapes — no separate text elements
+- Minimum size: `120x60` for labeled boxes
+- Add `roundness: { type: 3 }` for rounded corners
-### Arrow rules
-- Always include `endArrowhead: "arrow"` for directional flow
-- Use `strokeStyle: "dashed"` for responses, return values, optional paths
-- Keep arrow labels short (under 20 chars) or omit — long labels overflow
-- Use `startBinding` / `endBinding` with `fixedPoint` to attach to shapes
+### Arrows
+- Include `endArrowhead: "arrow"` for direction
+- Use `strokeStyle: "dashed"` for responses/optional paths
+- Keep labels **under 15 characters** or omit
+- Use `startBinding` / `endBinding` with `fixedPoint` to attach cleanly
---
## Step 4 — Diagram type patterns
-### Architecture / System Diagram
-Zones as swim lanes (left-to-right or top-to-bottom). Each zone = one architectural layer. Arrows show data/request flow between layers. End with a full-width cameraUpdate.
-
-**Camera pattern:** Title zoom (M) → pan right zone by zone (S/M) → final overview (XL)
+**Architecture** — Zones as swim lanes (left-to-right). Arrows show data flow.
-### Sequence / Flow Diagram
-Actors as header boxes with dashed vertical lifelines. Horizontal arrows show messages. Pan camera downward as messages progress.
+**Sequence** — Actors as header boxes with lifelines. Horizontal arrows = messages. Pan down as flow progresses.
-**Camera pattern:** Title (M) → pan right per actor drawing header + lifeline → zoom out (L) → pan down per message group → final overview (XL)
+**Process/Flowchart** — Top-to-bottom. Diamonds for decisions, rectangles for steps. Color-code by stage.
-### Concept Explainer
-Start zoomed on the title, then reveal parts of the concept one at a time. Use annotations (`#fff3bf` boxes) as callouts. Simple left-to-right flow.
-
-**Camera pattern:** Title zoom (S) → zoom out (M) → pan section by section → final (L)
-
-### Process / Flowchart
-Diamonds for decisions, rectangles for steps. Top-to-bottom flow. Color-code by stage (e.g. initiation=blue, processing=purple, output=green).
-
-**Camera pattern:** Top zoom → pan down per stage group → final overview
+**Concept** — Start zoomed on title, reveal parts progressively. Use annotation boxes as callouts.
---
-## Step 5 — The camera reveal technique (what makes diagrams feel alive)
+## Step 5 — The reveal animation
-The secret to great Excalidraw diagrams is **drawing section by section with camera moves**:
+Draw section by section with camera moves:
```json
-// 1. Start with title, zoomed in
+// 1. Title, zoomed
{"type":"cameraUpdate","width":600,"height":450,"x":100,"y":0},
{"type":"text","id":"t1","x":200,"y":20,"text":"My Diagram","fontSize":28},
-// 2. Pan to first zone and draw it
+// 2. Pan to zone 1, draw it
{"type":"cameraUpdate","width":400,"height":300,"x":20,"y":60},
-{"type":"rectangle","id":"zone1", ...zone background...},
-{"type":"rectangle","id":"node1", ...node with label...},
+{"type":"rectangle","id":"zone1","x":20,"y":80,"width":220,"height":380, ...},
+{"type":"rectangle","id":"node1","x":60,"y":130, ...},
-// 3. Pan to second zone
+// 3. Pan to zone 2
{"type":"cameraUpdate","width":400,"height":300,"x":280,"y":60},
-{"type":"rectangle","id":"zone2", ...},
-{"type":"rectangle","id":"node2", ...},
+{"type":"rectangle","id":"node2","x":320,"y":130, ...},
-// 4. Draw connecting arrows (camera stays or pans to show both ends)
+// 4. Draw arrows
{"type":"cameraUpdate","width":800,"height":600,"x":0,"y":40},
-{"type":"arrow","id":"a1", ...arrow from node1 to node2...},
+{"type":"arrow","id":"a1", ...},
-// 5. Final wide overview
+// 5. Final wide view
{"type":"cameraUpdate","width":1200,"height":900,"x":-20,"y":-10}
```
-This creates the "drawing itself" animation effect users love.
-
---
-## Step 6 — Common mistakes to avoid
+## Step 6 — Overlap prevention checklist
-- **No cameraUpdate first** → diagram appears un-framed, elements clip
-- **Wrong aspect ratio** → `700x500` causes distortion; use `800x600`
-- **All elements at once, no panning** → loses the reveal animation
-- **Overlapping elements** → check y-coordinates leave 60–80px between rows
-- **Long arrow labels** → overflow the arrow; keep under 20 chars or use a note box instead
-- **Emoji in text** → don't render in Excalidraw's font
-- **Light text on white** → `#b0b0b0` on white is invisible; minimum `#757575`
-- **Zone label covered by nodes** → put zone label text at top-left of zone (y + 8px from zone top), nodes start 40px below
-- **Title not centered** → estimate `text.length x fontSize x 0.5` for width, then set `x = diagramCenterX - estimatedWidth/2`
+- [ ] All shapes have **at least 60px vertical separation**
+- [ ] Zone labels don't overlap with nodes (40px minimum gap below label)
+- [ ] Arrows don't cross unrelated zones
+- [ ] Text is 8–10px inside shape bounds (not on edges)
+- [ ] Zone backgrounds are drawn BEFORE nodes
+- [ ] Arrows drawn AFTER both source and target
+- [ ] No shape dimensions smaller than `100x50`
+- [ ] All camera sizes are valid 4:3 ratios
+- [ ] Final element is a wide cameraUpdate
+- [ ] Minimum 3 camera positions for animation
---
-## Step 7 — Quality checklist before emitting
+## Step 7 — Common mistakes
-- [ ] `Excalidraw:read_me` called
-- [ ] First element is `cameraUpdate`
-- [ ] All camera sizes are valid 4:3 ratios
-- [ ] Minimum 3 camera positions used (more = better animation)
-- [ ] Color grammar is consistent across zones
-- [ ] All shape labels use `label` property, not separate text elements
-- [ ] No font sizes below 14
-- [ ] Zone backgrounds are drawn BEFORE the nodes inside them
-- [ ] Arrows drawn AFTER both source and target shapes
-- [ ] Final element is a wide cameraUpdate revealing the full diagram
-- [ ] No emoji in any text strings
+- **Overlapping text** — Check y-coordinates strictly; use 60px gaps minimum
+- **No cameraUpdate first** — Elements clip and look wrong
+- **All elements at once** — Loses the animation; use multiple camerUpdates
+- **Long arrow labels** — Overflow; keep under 15 chars
+- **Light text on white** — Use `#757575` minimum
+- **Zone label covered by nodes** — Put label 8px from zone top, nodes 40px below
+- **Shapes touching edges** — Leave padding; awkward layout
---
-## Reference: Element snippets
+## Reference: Snippets
**Zone background:**
```json
-{"type":"rectangle","id":"zone_bg","x":20,"y":80,"width":220,"height":380,"backgroundColor":"#dbe4ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":1,"opacity":40}
-```
-
-**Zone label:**
-```json
-{"type":"text","id":"zone_lbl","x":40,"y":88,"text":"FRONTEND","fontSize":14,"strokeColor":"#2563eb"}
+{"type":"rectangle","id":"z1","x":20,"y":80,"width":220,"height":380,"backgroundColor":"#dbe4ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":1,"opacity":40}
```
**Node:**
```json
-{"type":"rectangle","id":"n1","x":60,"y":130,"width":150,"height":55,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"label":{"text":"API Gateway","fontSize":16}}
+{"type":"rectangle","id":"n1","x":60,"y":130,"width":150,"height":55,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"label":{"text":"API","fontSize":16}}
```
-**Arrow (solid, directed):**
+**Arrow:**
```json
{"type":"arrow","id":"a1","x":210,"y":157,"width":100,"height":0,"points":[[0,0],[100,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","startBinding":{"elementId":"n1","fixedPoint":[1,0.5]},"endBinding":{"elementId":"n2","fixedPoint":[0,0.5]}}
```
-**Arrow (dashed, response):**
-```json
-{"type":"arrow","id":"a2","x":310,"y":157,"width":-100,"height":0,"points":[[0,0],[-100,0]],"strokeColor":"#757575","strokeWidth":2,"strokeStyle":"dashed","endArrowhead":"arrow"}
-```
-
-**Annotation note:**
-```json
-{"type":"rectangle","id":"note1","x":80,"y":200,"width":200,"height":36,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":1,"opacity":80,"label":{"text":"Caches for 5 min","fontSize":14}}
-```
-
-**Title text:**
-```json
-{"type":"text","id":"title","x":150,"y":15,"text":"System Architecture","fontSize":28,"strokeColor":"#1e1e1e"}
-```
-
-**Stick figure (user icon):**
+**Annotation:**
```json
-{"type":"ellipse","id":"fig_head","x":58,"y":110,"width":20,"height":20,"backgroundColor":"#a5d8ff","fillStyle":"solid","strokeColor":"#4a9eed","strokeWidth":2},
-{"type":"rectangle","id":"fig_body","x":57,"y":132,"width":22,"height":26,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2}
+{"type":"rectangle","id":"note1","x":80,"y":200,"width":200,"height":36,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":1,"opacity":80,"label":{"text":"Note here","fontSize":14}}
```
From b8619ce0c0a03d35b172352ad475c0555955777f Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:21:29 -0700
Subject: [PATCH 20/28] fix: add isMountedRef checks to all state-updating
effects
- Add mounted check to focus/blur handlers to prevent state updates on unmounted component
- Add mounted check to loading state transitions effect
- Add mounted check to streaming updates effect
- Prevents 'signal is aborted without reason' errors from async state updates
---
apps/app/src/app/canvas/page.tsx | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index 4be3eaf..690b5d7 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -132,8 +132,12 @@ const DocumentEditor = () => {
useEffect(() => {
if (!editor) return;
- const handleFocus = () => setIsFocused(true);
- const handleBlur = () => setIsFocused(false);
+ const handleFocus = () => {
+ if (isMountedRef.current) setIsFocused(true);
+ };
+ const handleBlur = () => {
+ if (isMountedRef.current) setIsFocused(false);
+ };
editor.on("focus", handleFocus);
editor.on("blur", handleBlur);
@@ -177,6 +181,8 @@ const DocumentEditor = () => {
// Handle loading state transitions
useEffect(() => {
+ if (!isMountedRef.current) return;
+
if (isLoading) {
setCurrentDocument(editor?.getText() || "");
}
@@ -200,6 +206,8 @@ const DocumentEditor = () => {
// Handle streaming updates while agent is running
useEffect(() => {
+ if (!isMountedRef.current) return;
+
if (isLoading) {
if (currentDocument.trim().length > 0) {
const newDocument = agentState?.document || "";
From de80d48f5af290dfa2c384644765e6787f47d1a0 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:40:18 -0700
Subject: [PATCH 21/28] feat: add WebSocket document as default content on
canvas page
- Set placeholder text to 'How do WebSockets work?'
- Initialize canvas with comprehensive WebSocket explanation document
- Document includes handshake process, persistent connections, frames, HTTP comparison, and use cases
- Provides starting point for user to ask agent to generate Excalidraw diagram
---
apps/app/src/app/canvas/page.tsx | 51 ++++++++++++++++++++++++++++++--
1 file changed, 49 insertions(+), 2 deletions(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index 690b5d7..f781919 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -106,6 +106,46 @@ interface AgentState {
document: string;
}
+const DEFAULT_DOCUMENT = `# How do WebSockets Work?
+
+## 1. The Handshake (HTTP Upgrade)
+
+It starts as a regular HTTP request. The client sends a special header asking to "upgrade" the connection:
+
+- GET /chat HTTP/1.1
+- Upgrade: websocket
+- Connection: Upgrade
+
+The server responds with 101 Switching Protocols, and from that point on, the connection is no longer HTTP — it's a WebSocket.
+
+## 2. The Persistent Connection
+
+Unlike HTTP (where each request opens and closes a connection), the WebSocket connection stays open. Both sides can now send messages to each other at any time without waiting for the other to ask first.
+
+## 3. Frames, Not Requests
+
+Data is sent as lightweight "frames" — small packets that can carry text, binary data, or control signals (like ping/pong to keep the connection alive).
+
+## HTTP vs WebSocket
+
+| Aspect | HTTP | WebSocket |
+|--------|------|-----------|
+| Direction | One-way (request → response) | Two-way (either side) |
+| Connection | Opens and closes each time | Stays open |
+| Overhead | Headers sent every request | Minimal after handshake |
+| Use case | Loading pages, REST APIs | Chat, live feeds, games |
+
+## A simple mental model
+
+Think of HTTP like sending letters — you write one, wait for a reply, then write another. WebSocket is like a phone call — once connected, both people can speak freely at any time without hanging up between each sentence.
+
+## Common use cases
+
+- Chat apps — messages appear instantly without polling
+- Live dashboards — stock prices, sports scores, analytics
+- Multiplayer games — real-time position and state sync
+- Collaborative tools — like Google Docs, where edits appear live`;
+
const DocumentEditor = () => {
const editor = useEditor({
extensions,
@@ -117,10 +157,17 @@ const DocumentEditor = () => {
const [placeholderVisible, setPlaceholderVisible] = useState(false);
const [isFocused, setIsFocused] = useState(false);
- const [currentDocument, setCurrentDocument] = useState("");
+ const [currentDocument, setCurrentDocument] = useState(DEFAULT_DOCUMENT);
const wasRunning = useRef(false);
const isMountedRef = useRef(true);
+ // Initialize editor with default document on mount
+ useEffect(() => {
+ if (editor && currentDocument) {
+ editor.commands.setContent(fromMarkdown(currentDocument));
+ }
+ }, [editor]);
+
// Cleanup on unmount to prevent state updates after component is removed
useEffect(() => {
return () => {
@@ -301,7 +348,7 @@ const DocumentEditor = () => {
{placeholderVisible && (
- Write whatever you want here in Markdown format...
+ How do WebSockets work?
)}
From 45fd46d560756b29939b8b4f643cd042dcde1726 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:41:41 -0700
Subject: [PATCH 22/28] fix: replace suggestions with single WebSocket document
chip
- Change from 4 generic suggestions to 1 focused suggestion
- Suggestion prompts agent to generate WebSocket document
- Aligns with canvas page purpose
---
apps/app/src/app/canvas/page.tsx | 16 ++--------------
1 file changed, 2 insertions(+), 14 deletions(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index f781919..c41ef92 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -198,20 +198,8 @@ const DocumentEditor = () => {
useConfigureSuggestions({
suggestions: [
{
- title: "Write a pirate story",
- message: "Please write a story about a pirate named Candy Beard.",
- },
- {
- title: "Write a mermaid story",
- message: "Please write a story about a mermaid named Luna.",
- },
- {
- title: "Add character",
- message: "Please add a character named Courage.",
- },
- {
- title: "Create documentation",
- message: "Create technical documentation for a REST API.",
+ title: "Generate WebSocket document",
+ message: "Create a comprehensive document explaining how WebSockets work, including the handshake process, persistent connections, frames, comparison with HTTP, and common use cases.",
},
],
available: "always",
From ef85a6dbf06a9e74215e522a5f806970f541997a Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:42:56 -0700
Subject: [PATCH 23/28] feat: add more technical document generation prompts
- REST API architecture
- OAuth 2.0 authentication flow
- Microservices architecture
- GraphQL explanation
- CI/CD pipelines
Provides diverse examples for users to generate technical documentation and diagrams
---
apps/app/src/app/canvas/page.tsx | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index c41ef92..28f2907 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -201,6 +201,26 @@ const DocumentEditor = () => {
title: "Generate WebSocket document",
message: "Create a comprehensive document explaining how WebSockets work, including the handshake process, persistent connections, frames, comparison with HTTP, and common use cases.",
},
+ {
+ title: "Explain REST API architecture",
+ message: "Write a detailed document about REST API design principles, HTTP methods, status codes, request/response structure, and best practices.",
+ },
+ {
+ title: "How does OAuth 2.0 work?",
+ message: "Create a document explaining OAuth 2.0 authentication flow, including authorization servers, access tokens, refresh tokens, and common scenarios.",
+ },
+ {
+ title: "Microservices architecture",
+ message: "Write a comprehensive guide to microservices architecture, covering service decomposition, inter-service communication, data consistency, and deployment patterns.",
+ },
+ {
+ title: "What is GraphQL?",
+ message: "Create a document explaining GraphQL, including how it differs from REST, query language basics, schema definition, and resolver functions.",
+ },
+ {
+ title: "CI/CD pipeline explained",
+ message: "Write a detailed document about continuous integration and continuous deployment, covering stages, automated testing, build artifacts, and deployment strategies.",
+ },
],
available: "always",
});
From 036554f9121df1e5c9be73dd8e9c0d99411340a6 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:44:21 -0700
Subject: [PATCH 24/28] fix: hide suggestions when chat has messages
- Change suggestion availability from 'always' to 'on-empty'
- Prompts now only show in empty chat
- Hides clutter when conversation is ongoing
---
apps/app/src/app/canvas/page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index 28f2907..130e2f5 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -222,7 +222,7 @@ const DocumentEditor = () => {
message: "Write a detailed document about continuous integration and continuous deployment, covering stages, automated testing, build artifacts, and deployment strategies.",
},
],
- available: "always",
+ available: "on-empty",
});
const { agent } = useAgent({
From 280e8f2dfc1d8d0c81e4b7ab77c4155bf16c8ff7 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:45:00 -0700
Subject: [PATCH 25/28] fix: show only 3 prompt suggestions initially
- Keep most impactful examples: WebSocket, REST API, Microservices
- Cleaner UI with fewer choices
- Easier to scan and select for new users
---
apps/app/src/app/canvas/page.tsx | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index 130e2f5..f7c84eb 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -205,22 +205,10 @@ const DocumentEditor = () => {
title: "Explain REST API architecture",
message: "Write a detailed document about REST API design principles, HTTP methods, status codes, request/response structure, and best practices.",
},
- {
- title: "How does OAuth 2.0 work?",
- message: "Create a document explaining OAuth 2.0 authentication flow, including authorization servers, access tokens, refresh tokens, and common scenarios.",
- },
{
title: "Microservices architecture",
message: "Write a comprehensive guide to microservices architecture, covering service decomposition, inter-service communication, data consistency, and deployment patterns.",
},
- {
- title: "What is GraphQL?",
- message: "Create a document explaining GraphQL, including how it differs from REST, query language basics, schema definition, and resolver functions.",
- },
- {
- title: "CI/CD pipeline explained",
- message: "Write a detailed document about continuous integration and continuous deployment, covering stages, automated testing, build artifacts, and deployment strategies.",
- },
],
available: "on-empty",
});
From 0c729255e7df95d762e01676aaae869b6e43ab90 Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:46:22 -0700
Subject: [PATCH 26/28] fix: resolve AbortError and fix suggestion chips
visibility
- Add isMountedRef guard to editor initialization effect
- Fix initialization effect dependency to properly watch currentDocument
- Remove invalid 'on-empty' parameter from suggestions (restore defaults)
- Prevents state updates after unmount that caused AbortError
---
apps/app/src/app/canvas/page.tsx | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index f7c84eb..bbdec19 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -163,10 +163,9 @@ const DocumentEditor = () => {
// Initialize editor with default document on mount
useEffect(() => {
- if (editor && currentDocument) {
- editor.commands.setContent(fromMarkdown(currentDocument));
- }
- }, [editor]);
+ if (!editor || !isMountedRef.current) return;
+ editor.commands.setContent(fromMarkdown(currentDocument));
+ }, [editor, currentDocument]);
// Cleanup on unmount to prevent state updates after component is removed
useEffect(() => {
@@ -210,7 +209,6 @@ const DocumentEditor = () => {
message: "Write a comprehensive guide to microservices architecture, covering service decomposition, inter-service communication, data consistency, and deployment patterns.",
},
],
- available: "on-empty",
});
const { agent } = useAgent({
From 6eb355aa9df34b2bee9905a5e2fe91ee0cd5cdbc Mon Sep 17 00:00:00 2001
From: jerelvelarde
Date: Thu, 19 Mar 2026 10:47:47 -0700
Subject: [PATCH 27/28] feat: add streaming animation to document editor during
generation
- Add pulsing glow animation (streamingGlow) on editor border
- Animation activates when agent is running (isLoading state)
- Subtle purple glow effect with inset shadow
- Provides visual feedback that document is being generated
---
apps/app/src/app/canvas/page.tsx | 5 ++++-
apps/app/src/app/canvas/style.css | 18 ++++++++++++++++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx
index bbdec19..adf9922 100644
--- a/apps/app/src/app/canvas/page.tsx
+++ b/apps/app/src/app/canvas/page.tsx
@@ -345,7 +345,10 @@ const DocumentEditor = () => {
How do WebSockets work?