Files
awesome-copilot/extensions/diagram-viewer/extension.mjs
T
Aaron Powell 36cdc52037 Add Canvas Extensions website page (#1900)
Generate extensions data, add the extensions listing route/navigation, and include install URL copy actions pinned to the build commit SHA.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 11:17:56 -07:00

391 lines
13 KiB
JavaScript

import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
import { createCanvas, joinSession } from "@github/copilot-sdk/extension";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Per-instance state (ephemeral, lives in memory for session lifetime)
const instances = new Map();
function getInstance(instanceId) {
if (!instances.has(instanceId)) {
instances.set(instanceId, {
currentView: null,
history: [],
selectedNodeId: null,
token: crypto.randomBytes(16).toString("hex"),
});
}
return instances.get(instanceId);
}
function getCurrentView(inst) {
return inst.currentView;
}
function pushView(inst, view) {
if (inst.currentView) {
inst.history.push(inst.currentView);
}
inst.currentView = view;
inst.selectedNodeId = null;
}
function replaceView(inst, view) {
inst.currentView = view;
inst.selectedNodeId = null;
}
function popView(inst) {
if (inst.history.length === 0) return null;
inst.currentView = inst.history.pop();
inst.selectedNodeId = null;
return inst.currentView;
}
// SSE clients per instance
const sseClients = new Map();
function broadcast(instanceId, event, data) {
const clients = sseClients.get(instanceId);
if (!clients) return;
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const res of clients) {
res.write(msg);
}
}
// Broadcast the full view state to the iframe
function broadcastView(instanceId, inst) {
const view = getCurrentView(inst);
broadcast(instanceId, "view", {
...view,
historyDepth: inst.history.length,
breadcrumbs: inst.history.map((v) => v.title).concat(view ? [view.title] : []),
});
}
// HTTP helpers
function readJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => resolve(body ? JSON.parse(body) : {}));
req.on("error", reject);
});
}
function json(res, code, data) {
res.writeHead(code, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
// HTTP server
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const token = url.searchParams.get("token");
const instanceId = url.searchParams.get("instance");
// Serve the HTML page
if (req.method === "GET" && url.pathname === "/") {
if (!instanceId || !validateToken(instanceId, token)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(fs.readFileSync(path.join(__dirname, "public", "index.html"), "utf8"));
return;
}
// SSE endpoint
if (req.method === "GET" && url.pathname === "/events") {
if (!instanceId || !validateToken(instanceId, token)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
if (!sseClients.has(instanceId)) sseClients.set(instanceId, new Set());
sseClients.get(instanceId).add(res);
req.on("close", () => {
const clients = sseClients.get(instanceId);
if (clients) clients.delete(res);
});
// Send current view state immediately
const inst = getInstance(instanceId);
if (inst.currentView) {
const view = getCurrentView(inst);
res.write(`event: view\ndata: ${JSON.stringify({
...view,
historyDepth: inst.history.length,
breadcrumbs: inst.history.map((v) => v.title).concat([view.title]),
})}\n\n`);
if (inst.selectedNodeId) {
res.write(`event: select\ndata: ${JSON.stringify({ nodeId: inst.selectedNodeId })}\n\n`);
}
}
return;
}
// API: get full state
if (req.method === "GET" && url.pathname === "/api/state") {
if (!instanceId || !validateToken(instanceId, token)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
const inst = getInstance(instanceId);
const view = getCurrentView(inst);
json(res, 200, {
view,
historyDepth: inst.history.length,
breadcrumbs: inst.history.map((v) => v.title).concat(view ? [view.title] : []),
selectedNodeId: inst.selectedNodeId,
});
return;
}
// API: node clicked — triggers drill-down
if (req.method === "POST" && url.pathname === "/api/click") {
if (!instanceId || !validateToken(instanceId, token)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
const { nodeId } = await readJson(req);
const inst = getInstance(instanceId);
inst.selectedNodeId = nodeId;
broadcast(instanceId, "select", { nodeId });
// Send prompt to agent to drill into the clicked node
const view = getCurrentView(inst);
const node = view?.diagram?.nodes?.find((n) => n.id === nodeId);
if (node && session) {
const diagramContext = view.diagram.nodes.map((n) => n.label).join(", ");
session.send({
prompt: `The user clicked on the "${node.label}" node in the Diagram Explorer canvas (id: "${node.id}", type: "${node.type || "default"}", description: "${node.description || "none"}"). The current diagram is "${view.title}" which contains: ${diagramContext}.
Do NOT explain in chat. Instead, use the canvas actions to respond visually:
1. Use the render_diagram action with mode "push" to show a detailed sub-diagram of "${node.label}" — break it into its internal components, sub-systems, or key parts with their relationships.
2. Use the show_explanation action to display a brief explanation panel on the canvas.
If you cannot create a meaningful sub-diagram (e.g. the node is already a leaf concept), use show_explanation to provide a detailed description on the canvas instead, without rendering a new diagram.`,
});
}
json(res, 200, { ok: true, selectedNodeId: nodeId });
return;
}
// API: navigate back
if (req.method === "POST" && url.pathname === "/api/back") {
if (!instanceId || !validateToken(instanceId, token)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
const inst = getInstance(instanceId);
const prev = popView(inst);
if (prev) {
broadcastView(instanceId, inst);
}
json(res, 200, { ok: true, view: prev });
return;
}
res.writeHead(404);
res.end("Not found");
});
function validateToken(instanceId, token) {
const inst = instances.get(instanceId);
return inst && inst.token === token;
}
const port = await new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => resolve(server.address().port));
});
// Canvas declaration
const canvas = createCanvas({
id: "diagram",
displayName: "Diagram Explorer",
description:
"Interactive diagram for exploring architecture, data flow, and relationships. Render nodes and edges, then click any node to get a detailed explanation from the agent.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Optional title for the initial diagram" },
},
},
actions: [
{
name: "render_diagram",
description:
"Render an interactive diagram with nodes and edges. Use mode 'push' to drill into a node (adds to history so user can navigate back), or 'replace' (default) to update the current view in place.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Diagram title" },
nodes: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string", description: "Unique node identifier" },
label: { type: "string", description: "Display label" },
description: {
type: "string",
description: "Brief description shown on hover and used when drilling in",
},
type: {
type: "string",
description: "Node type for color coding (e.g. 'service', 'database', 'ui', 'api', 'config', 'external')",
},
},
required: ["id", "label"],
},
},
edges: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "string", description: "Source node id" },
to: { type: "string", description: "Target node id" },
label: { type: "string", description: "Optional edge label" },
},
required: ["from", "to"],
},
},
mode: {
type: "string",
enum: ["push", "replace"],
description: "Navigation mode. 'push' saves current view to history (for drill-down). 'replace' updates in place (default).",
},
explanation: {
type: "object",
properties: {
title: { type: "string", description: "Explanation panel title" },
text: { type: "string", description: "Explanation text (plain text)" },
},
description: "Optional explanation to show alongside the diagram",
},
},
required: ["nodes", "edges"],
},
handler({ instanceId, input }) {
const inst = getInstance(instanceId);
const view = {
title: input.title || "Diagram",
diagram: { title: input.title || "Diagram", nodes: input.nodes, edges: input.edges },
explanation: input.explanation || null,
selectedNodeId: null,
};
if (input.mode === "push") {
pushView(inst, view);
} else {
replaceView(inst, view);
}
broadcastView(instanceId, inst);
return { ok: true, nodeCount: input.nodes.length, edgeCount: input.edges.length, historyDepth: inst.history.length };
},
},
{
name: "show_explanation",
description:
"Display an explanation panel on the canvas alongside the current diagram. Use this to provide context about the current view or a clicked node without changing the diagram.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Explanation panel title" },
text: { type: "string", description: "Explanation content (plain text, can include line breaks)" },
},
required: ["title", "text"],
},
handler({ instanceId, input }) {
const inst = getInstance(instanceId);
const view = getCurrentView(inst);
if (view) {
view.explanation = { title: input.title, text: input.text };
broadcast(instanceId, "explanation", view.explanation);
}
return { ok: true };
},
},
{
name: "get_state",
description:
"Get the current diagram state including which node the user last clicked and the history depth.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
handler({ instanceId }) {
const inst = getInstance(instanceId);
const view = getCurrentView(inst);
const selectedNode = inst.selectedNodeId
? view?.diagram?.nodes?.find((n) => n.id === inst.selectedNodeId)
: null;
return {
currentView: view,
selectedNodeId: inst.selectedNodeId,
selectedNode: selectedNode || null,
historyDepth: inst.history.length,
breadcrumbs: inst.history.map((v) => v.title).concat(view ? [view.title] : []),
};
},
},
{
name: "highlight_node",
description: "Highlight a specific node in the diagram (e.g. while explaining it).",
inputSchema: {
type: "object",
properties: {
nodeId: { type: "string", description: "The node id to highlight" },
},
required: ["nodeId"],
},
handler({ instanceId, input }) {
const inst = getInstance(instanceId);
inst.selectedNodeId = input.nodeId;
broadcast(instanceId, "select", { nodeId: input.nodeId });
return { ok: true, highlightedNodeId: input.nodeId };
},
},
{
name: "clear",
description: "Clear the diagram canvas and all history.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
handler({ instanceId }) {
const inst = getInstance(instanceId);
inst.currentView = null;
inst.history = [];
inst.selectedNodeId = null;
broadcast(instanceId, "clear", {});
return { ok: true };
},
},
],
open({ instanceId, input }) {
const inst = getInstance(instanceId);
const view = getCurrentView(inst);
return {
url: `http://127.0.0.1:${port}?instance=${instanceId}&token=${inst.token}`,
title: input?.title || "Diagram Explorer",
status: view
? `${view.diagram.nodes.length} nodes`
: "Ready",
};
},
});
let session = await joinSession({ canvases: [canvas] });