canvas-extension: new chat cards extension - #2698
Conversation
🔒 PR Risk Scan ResultsScanned 9 changed file(s).
Skipped non-text or missing files
|
There was a problem hiding this comment.
Pull request overview
Adds the Chat Cards canvas extension and packages it for the Awesome Copilot marketplace.
Changes:
- Implements interactive cards, forms, charts, and deck management.
- Adds the canvas UI, local HTTP/SSE transport, and visual assets.
- Registers and documents the extension plugin.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
plugins/chat-cards/README.md |
Documents plugin installation. |
plugins/chat-cards/plugin.json |
Defines plugin metadata. |
extensions/chat-cards/README.md |
Documents features and usage. |
extensions/chat-cards/package.json |
Declares the extension package. |
extensions/chat-cards/extension.mjs |
Implements actions, state, and transport. |
extensions/chat-cards/copilot-extension.json |
Provides extension metadata. |
extensions/chat-cards/cards-core.mjs |
Builds and sanitizes card content. |
extensions/chat-cards/assets/canvas.html |
Implements the interactive canvas UI. |
extensions/chat-cards/assets/icon.png |
Provides the extension icon. |
extensions/chat-cards/assets/preview.png |
Provides the marketplace preview. |
docs/README.plugins.md |
Adds the plugin to documentation. |
.github/plugin/marketplace.json |
Registers the marketplace entry. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return `<label class="mcc-choice" for="${choiceId}"> | ||
| <input type="${type}" id="${choiceId}" name="${name}" value="${escapeHtml(option.value ?? option.label)}"${checked}> | ||
| ${escapeHtml(option.label)}</label>`; |
| function seriesScale(series) { | ||
| const values = series.flatMap((s) => s.values).filter((value) => Number.isFinite(value)); | ||
| return niceScale(Math.max(...values, 0), 6, values.every((value) => Number.isInteger(value))); | ||
| } |
| cardId: { type: "string", description: "The id returned when the card was created" }, | ||
| kind: { type: "string", enum: Object.keys(CARD_BUILDERS) }, | ||
| }, | ||
| required: ["cardId", "kind"], | ||
| additionalProperties: true, |
| document.addEventListener("mouseover", function (event) { | ||
| var term = event.target.closest(".mcc-term"); | ||
| if (!term) return; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (6)
extensions/chat-cards/cards-core.mjs:1219
- Chart cards expose kinds such as
chart-bar, butupdate_cardonly accepts builder keys (chart,tabs, etc.) and tells callers to pass the card's returned kind. Consequently, a chart kind returned by create/list cannot be passed back to update. Keep the stored/public kind aligned with thechartbuilder key; the subtype already remains in the replacement spec'stype.
kind: `chart-${type}`,
extensions/chat-cards/cards-core.mjs:1268
requiredis calculated but never emitted for checkbox or radio inputs. Thereforeform.checkValidity()accepts a required choice group with nothing selected, despite the required marker shown to the user. Apply nativerequiredsemantics for radio groups and explicitly validate at least one selected value for multi-checkbox groups.
<input type="${type}" id="${choiceId}" name="${name}" value="${escapeHtml(option.value ?? option.label)}"${checked}>
plugins/chat-cards/README.md:14
- This fence uses two backticks, so the installation command renders as literal delimiter text instead of a code block. Use a valid Markdown code block.
``bash
copilot plugin install chat-cards@awesome-copilot
``
extensions/chat-cards/cards-core.mjs:116
- Caller-supplied HTML can retain extension-reserved classes, allowing sanitized content to trigger privileged delegated handlers. For example, an allowed
<a class="mcc-remove-card" href="https://example.com">survives sanitization, and clicking it removes the enclosing card. Reject reservedmcc-classes when rebuilding untrusted HTML.
"*": { class: (v) => SAFE_CLASS_PATTERN.test(v), title: () => true },
extensions/chat-cards/extension.mjs:217
- Bar/line series values allow negatives, but both renderers use a zero-only scale: negative bars are clamped to zero and negative line points are placed below the plot. This silently misrepresents valid schema input. Either reject negative values in both the action schema and core builder (including updates), or implement a signed min/max axis.
values: { type: "array", items: { type: "number" } },
extensions/chat-cards/cards-core.mjs:313
- Tutor definitions are exposed only through
mouseover/mouseout, while this generated span is not focusable and has no accessible description. Keyboard and screen-reader users therefore cannot access an advertised card feature. Make terms keyboard-focusable and mirror tooltip behavior on focus/blur, or associate the tip through accessible descriptive markup.
`<span class="mcc-term" data-tip="${escapeHtml(term.tip)}">${match[0]}</span>`;
|
Made edits based on code review, then thought I found a bug with this canvas extension; but turns out that GitHub Copilot has a bug. See github/app issue 2930. I tested several extensions from awesome-copilot. All with the same results that are stated in the issue - when canvas panel is left open, and app closed, then app has to be quit in order to reopen. Extensions tested include:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.
Suppressed comments (6)
extensions/chat-cards/cards-core.mjs:1233
- Chart cards report their kind as
chart-bar,chart-line, etc., butupdate_cardaccepts onlykind: "chart"and dispatches through thechartbuilder. Consequently, using the kind returned by creation orlist_cardsalways makes chart updates fail as an unknown kind. Keep the public kind aligned withKIND_SCHEMAS/CARD_BUILDERS; the chart subtype is already present in the replacement spec.
kind: `chart-${type}`,
extensions/chat-cards/README.md:130
- This description incorrectly calls
copilot-extension.jsonthe marketplace plugin manifest. It is local extension discovery metadata; the Agent Plugin manifest for the Awesome Copilot submission isplugins/chat-cards/plugin.json.
copilot-extension.json Plugin manifest for the awesome-copilot submission
extensions/chat-cards/README.md:93
- The copied directory has no nested
extension/folder, so this installation step points users at a path that does not exist. The local extension root is the copiedchat-cards/directory containingcopilot-extension.jsonandextension.mjs.
This issue also appears on line 130 of the same file.
`npm install` pulls the extension's single dependency (`@github/copilot-sdk`).
Then register the `extension/` folder with your Copilot client as a local
extension, start a session, and ask the agent to open the Chat Cards canvas.
extensions/chat-cards/extension.mjs:166
- A newly created instance leaves
updatedAtundefined, so every unrelated flush serializes it with a fresh current timestamp without saving that timestamp back to the instance. Merely keeping an empty canvas in memory therefore makes it perpetually "newest" and can evict genuinely updated decks from the eight-deck retention set. Initialize the timestamp once when the instance is created.
updatedAt: restored?.updatedAt,
extensions/chat-cards/assets/canvas.html:692
- Template substitution is performed repeatedly over the progressively modified prompt, so a literal placeholder entered as one field's value is interpreted as another template token on a later iteration. For example,
a = "{{b}}"turns{{a}} / {{b}}into the value ofbtwice. Replace tokens in a single pass so inserted user values remain literal.
var prompt = template;
Object.keys(values).forEach(function (name) {
prompt = prompt.split("{{" + name + "}}").join(values[name]);
});
return prompt;
extensions/chat-cards/cards-core.mjs:1453
isPlayableMediaUrlexplicitly acceptsdata:audio/*, so this video-card validation permits an audio-only data URI even though the action schema, documentation, error text, and<video>card contract all requiredata:video/*. Restrict this call to the advertised video schemes.
if (!isPlayableMediaUrl(src)) {
| mkdirSync(path.dirname(STATE_FILE), { recursive: true }); | ||
| writeFileSync(`${STATE_FILE}.tmp`, JSON.stringify({ version: STATE_VERSION, instances: kept })); | ||
| renameSync(`${STATE_FILE}.tmp`, STATE_FILE); |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 2 comments.
Suppressed comments (9)
extensions/chat-cards/cards-core.mjs:1237
- Chart cards report kinds such as
chart-bar, butupdate_cardonly accepts thechartdiscriminant and passes it toCARD_BUILDERS. As a result, the kind returned by create/list cannot be used to update a chart. Keep the stored/API kind aligned with the builder key.
kind: `chart-${type}`,
extensions/chat-cards/extension.mjs:123
- Multiple extension processes share both this fixed temporary path and a whole-store snapshot loaded only at startup. Concurrent sessions can race on
state.json.tmp, and the last process to flush can overwrite newer decks written by another process. Use per-instance state files or an inter-process lock with a read/merge/write operation and unique temporary files.
writeFileSync(`${STATE_FILE}.tmp`, JSON.stringify({ version: STATE_VERSION, instances: kept }));
renameSync(`${STATE_FILE}.tmp`, STATE_FILE);
extensions/chat-cards/cards-core.mjs:882
- This SVG is exposed as an image but has no accessible name; the nested data-point
<title>elements do not name the root graphic. Add anaria-labelso screen-reader users can identify the chart.
`<svg viewBox="0 0 ${CHART_WIDTH} ${CHART_HEIGHT}" role="img" xmlns="http://www.w3.org/2000/svg">` +
extensions/chat-cards/cards-core.mjs:68
- This validator also accepts
data:audio/*, although the video action schema and its error message explicitly allow onlydata:video/*. Such input is therefore accepted into a video card contrary to the API contract.
return isHttpUrl(v) || isDataUrlOfType(v, "video") || isDataUrlOfType(v, "audio") || isBlobUrl(v);
extensions/chat-cards/extension.mjs:389
items: {}accepts values such asnulland numbers, butnormalizeFieldOptionsdereferencesoption.valuefor every non-string option, so schema-valid action input can throw instead of producing a form. Encode the documented string-or-object union in the schema.
items: {},
extensions/chat-cards/README.md:93
- The copied folder contains
extension.mjsat its root; there is no childextension/directory to register. This installation step therefore directs users to a nonexistent path.
Then register the `extension/` folder with your Copilot client as a local
extension, start a session, and ask the agent to open the Chat Cards canvas.
extensions/chat-cards/README.md:11
- Use “inline in the conversation” rather than “inline of the conversation.”
inline of the conversation, instead of rendering the HTML in a separate panel.
extensions/chat-cards/cards-core.mjs:845
- This SVG is exposed as an image but has no accessible name; the nested data-point
<title>elements do not name the root graphic. Add anaria-labelso screen-reader users can identify the chart.
This issue also appears on line 882 of the same file.
`<svg viewBox="0 0 ${CHART_WIDTH} ${CHART_HEIGHT}" role="img" xmlns="http://www.w3.org/2000/svg">` +
extensions/chat-cards/cards-core.mjs:925
- This pie/donut SVG has
role="img"but no accessible name. The slice-level tooltips do not name the root graphic, so add a label for the generated chart type.
return `<svg viewBox="0 0 ${size} ${size}" role="img" xmlns="http://www.w3.org/2000/svg">${paths}</svg>`;
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (7)
extensions/chat-cards/cards-core.mjs:1237
- Chart creation and
list_cardsreturn kinds such aschart-bar, butupdate_cardonly accepts the builder keychart. Passing the kind returned by the public API therefore fails schema validation orbuildCard. Keep the card kind stable and carry the chart subtype intype.
kind: `chart-${type}`,
extensions/chat-cards/cards-core.mjs:505
- A same-indent switch between unordered and ordered markers is emitted as another
<li>in the currently open list. For example,- firstfollowed by1. secondrenders both items in one<ul>, changing the document semantics. Close and reopen the list when the marker type changes at the current indentation.
if (stack.length === 0 || indent > stack[stack.length - 1].indent + 1) {
stack.push({ indent, tag });
html += `<${tag}><li>${renderInline(match[3])}`;
} else {
html += `</li><li>${renderInline(match[3])}`;
extensions/chat-cards/cards-core.mjs:1300
- The text labeling a checkbox/radio group is a plain
<label>with no associated control, so assistive technology does not receive it as the fieldset's group name. A fieldset must use a<legend>for this purpose.
return `<fieldset class="mcc-field" style="border:none;padding:0;margin:0"${requireOne}>
<label>${label}${requiredMark}</label>
${choices}${help}</fieldset>`;
extensions/chat-cards/README.md:93
- There is no child
extension/directory in the copied layout;extension.mjsis directly insidechat-cards/. This instruction sends manual installers to a nonexistent folder. Refer to the copiedchat-cards/folder itself.
Then register the `extension/` folder with your Copilot client as a local
extension, start a session, and ask the agent to open the Chat Cards canvas.
extensions/chat-cards/extension.mjs:392
- The schema description restricts choices to strings or
{ label, value }objects, butitems: {}accepts every JSON value. A value such asnullpasses the action schema and then throws innormalizeFieldOptionswhenoption.valueis read. Encode the documented union in the schema so invalid action input is rejected predictably.
options: {
type: "array",
description: "Choices for select/checkbox/radio; strings or { label, value } objects",
items: {},
},
extensions/chat-cards/extension.mjs:382
required: ["name"]only requires the property to exist, so an empty name is accepted. The browser then creates a nameless control andcollectValuesexplicitly skips it, silently dropping that answer and leaving any matching prompt token unresolved. Require a non-empty field name.
name: { type: "string" },
extensions/chat-cards/README.md:130
- This file is the canvas extension manifest; the plugin manifest is
plugins/chat-cards/plugin.json. Calling it the plugin manifest makes the documented layout misleading for contributors and manual installers.
copilot-extension.json Plugin manifest for the awesome-copilot submission
| writeFileSync(`${STATE_FILE}.tmp`, JSON.stringify({ version: STATE_VERSION, instances: kept }), { | ||
| mode: 0o600, | ||
| }); | ||
| renameSync(`${STATE_FILE}.tmp`, STATE_FILE); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
extensions/chat-cards/cards-core.mjs:1237
- Chart cards are returned as
chart-bar,chart-line, etc., butupdate_cardandCARD_BUILDERSonly accept the kindchart. Becauselist_cardsexposes this stored value and the update action asks callers to pass the card's kind, chart updates will be rejected. Keep the persisted kind aligned with the dispatcher and retain the subtype in the chart spec.
kind: `chart-${type}`,
extensions/chat-cards/extension.mjs:508
- The empty item schema accepts values such as
nulland numbers even though the contract says strings or{label, value}objects;normalizeFieldOptionsthen dereferencesoption.value, so schema-valid input can fail. It also permits select/radio fields with no choices, producing unusable controls. Encode the option union and non-empty requirement in the schema and validate it in the builder.
options: {
type: "array",
description: "Choices for select/checkbox/radio; strings or { label, value } objects",
items: {},
},
extensions/chat-cards/cards-core.mjs:1299
- A
<label>without aforattribute does not provide an accessible name for this fieldset, so screen readers lose the checkbox/radio group's question. Use the fieldset's native<legend>element.
<label>${label}${requiredMark}</label>
extensions/chat-cards/assets/canvas.html:818
- Model-defined actions are exposed only through the pointer context-menu event. Unlike the other card actions, there is no visible focusable trigger, and opening this popup does not move keyboard focus into it. Add an Actions button plus menu semantics and focus management so keyboard and touch users can invoke these prompts.
document.addEventListener("contextmenu", function (event) {
var article = event.target.closest(".mcc-card");
if (!article) { hideMenu(); return; }
var config = configs[article.getAttribute("data-card-id")] || {};
var actions = config.contextActions || [];
event.preventDefault();
extensions/chat-cards/assets/canvas.html:867
- Card ordering is available only through native drag events, so keyboard users cannot perform this persisted operation. Provide focusable Move up/Move down controls or an equivalent keyboard interaction and announce the resulting order change.
document.addEventListener("dragstart", function (event) {
var head = event.target.closest('.mcc-head[draggable="true"]');
if (!head) return;
draggingCard = head.closest(".mcc-card");
draggingCard.classList.add("mcc-dragging");
event.dataTransfer.effectAllowed = "move";
| for (const { name } of ranked.slice(MAX_PERSISTED_DECKS)) { | ||
| // A deck this process holds open outranks the cap: dropping its file | ||
| // would lose cards the user can still see in the canvas. | ||
| if (owned.has(name)) continue; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (8)
extensions/chat-cards/cards-core.mjs:1237
- Chart cards report kinds such as
chart-bar, butupdate_cardandCARD_BUILDERSonly recognizechart. An agent followinglist_cardswill therefore pass a kind that the update schema rejects (or thatbuildCardreports as unknown). Keep the public kind stable aschart; the subtype already remains in the replacement spec'stypefield.
kind: `chart-${type}`,
extensions/chat-cards/cards-core.mjs:1176
- Pie and donut values are not runtime-validated like bar/line values. A negative slice is silently clamped to zero in the SVG and legend while the data table still displays the negative value, so the card presents contradictory data. Reject non-finite and negative slice values before rendering.
if ((type === "pie" || type === "donut") && (!options.values || options.values.length === 0)) {
throw new Error("Pie and donut charts require values.");
extensions/chat-cards/cards-core.mjs:505
- A list-marker change at the same indentation never closes and reopens the list. For example,
- bulletfollowed by1. numberedis rendered entirely as a<ul>, losing the ordered-list semantics. Handle a same-depthul/oltransition by closing the current list and opening the new tag.
if (stack.length === 0 || indent > stack[stack.length - 1].indent + 1) {
stack.push({ indent, tag });
html += `<${tag}><li>${renderInline(match[3])}`;
} else {
html += `</li><li>${renderInline(match[3])}`;
extensions/chat-cards/cards-core.mjs:1457
- This video-specific API uses a general media predicate that also accepts
data:audio/*, despite the schema, README, and error message allowing onlydata:video/*. Such input produces an audio resource inside a video card rather than being rejected. Use a video-specific check here.
if (!isPlayableMediaUrl(src)) {
extensions/chat-cards/README.md:93
- The installation steps copy this directory directly to
.../extensions/chat-cards/, so there is no nestedextension/folder to register. Following this instruction leaves users pointing the client at a nonexistent path; direct them to the copiedchat-cards/directory instead.
Then register the `extension/` folder with your Copilot client as a local
extension, start a session, and ask the agent to open the Chat Cards canvas.
extensions/chat-cards/cards-core.mjs:1076
- The tabs expose ARIA
tabroles but implement only mouse/click activation. All tabs remain in the page tab order and there is no Left/Right/Home/End keyboard navigation or focus transfer, so the widget does not provide the expected keyboard interaction for a tablist. Implement rovingtabindexand keyboard selection alongside the click handler.
`<button type="button" class="mcc-tab" role="tab" id="${groupId}-tab-${index}" ` +
`aria-selected="${selected}" aria-controls="${groupId}-panel-${index}">` +
`${escapeHtml(tab.label ?? `Tab ${index + 1}`)}</button>`
extensions/chat-cards/extension.mjs:1021
- Context-action prompts are also silently shortened to 16,000 characters. Because this endpoint returns success and the UI ignores
delivered, a long selection can change the instruction sent to the conversation with no indication to the user. Reject oversized input rather than silently changing it.
const prompt = String(body.prompt ?? "").slice(0, MAX_PROMPT_CHARS);
extensions/chat-cards/extension.mjs:995
- Form prompts longer than 16,000 characters are silently truncated before sending and persistence, yet the client receives
delivered: trueand tells the user the full response was sent. This loses form data without warning. Reject over-limit prompts (so the existing client fallback exposes the complete prompt) or return an explicit truncation result instead of slicing.
const prompt = String(body.prompt ?? "").slice(0, MAX_PROMPT_CHARS);
| const existing = canvasServers.get(instanceId); | ||
| if (existing) return existing; | ||
|
|
||
| const server = http.createServer(handleCanvasRequest); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (5)
extensions/chat-cards/cards-core.mjs:1237
- Chart cards report kinds such as
chart-bar, butupdate_cardandCARD_BUILDERSonly acceptchart. Following the documented instruction to pass the card's returned/listed kind therefore makes every chart update fail. Keep the stored/returned kind aligned with the update discriminator; the chart subtype is already carried bytype.
kind: `chart-${type}`,
extensions/chat-cards/cards-core.mjs:67
- This predicate accepts every HTTP(S) URL, including streaming-platform page URLs that the action explicitly rejects. Such URLs pass validation but cannot be played by the generated
<video>element, resulting in a broken card. Either validate a supported direct-media URL contract or stop claiming this check enforces one.
export function isPlayableMediaUrl(value) {
const v = String(value ?? "").trim();
return isHttpUrl(v) || isDataUrlOfType(v, "video") || isDataUrlOfType(v, "audio") || isBlobUrl(v);
extensions/chat-cards/cards-core.mjs:1300
- The checkbox/radio group has a
<fieldset>but no<legend>; the unassociated<label>does not provide the group an accessible name. Screen-reader users therefore lose the question/context shared by these choices. Use a legend for the group label.
return `<fieldset class="mcc-field" style="border:none;padding:0;margin:0"${requireOne}>
<label>${label}${requiredMark}</label>
${choices}${help}</fieldset>`;
extensions/chat-cards/README.md:140
- This says the server uses an ephemeral port, but
startCanvasServernormally binds within the fixed 21750–21789 range and only falls back to an ephemeral port. Describe it as a loopback-only port so the security documentation matches the implementation.
- The card server binds to `127.0.0.1` on an ephemeral port; every request must carry the
per-canvas token, and request bodies are size-capped.
extensions/chat-cards/cards-core.mjs:1194
- A series may contain more or fewer values than there are labels. The SVG renderer silently drops extra values with
slice(0, labels.length), while missing values become blank table cells, so accepted input can be misrepresented. Reject mismatched lengths before rendering.
for (const s of options.series ?? []) {
Pull Request Checklist
npm startand verified thatREADME.mdis up to date.mainbranch for this pull request.Description
Interactive HTML card deck for GitHub Copilot canvas.
Prompt
Results
Type of Contribution
By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.