Skip to content

ember: add daily-focus-board skill (EF/neurodivergent-friendly) - #2452

Open
jennyf19 wants to merge 16 commits into
github:mainfrom
jennyf19:add-daily-focus-board-skill
Open

ember: add daily-focus-board skill (EF/neurodivergent-friendly)#2452
jennyf19 wants to merge 16 commits into
github:mainfrom
jennyf19:add-daily-focus-board-skill

Conversation

@jennyf19

Copy link
Copy Markdown
Contributor

What

Adds daily-focus-board, a warm, executive-function-friendly daily focus board you run by talking to your AI partner (Ember), and registers it in the ember plugin.

Renders from a single self-contained HTML template (no build, no backend; progress persists in localStorage). Designed to be universal (works standalone for anyone) and non-medicalizing (focus-friendly for everyone; never diagnoses).

Features

  • Tasks: status (to-do -> in progress -> done) with timestamped progress notes, plus numeric-goal counters (pages, pomodoros, reps...).
  • Arrival check-in (above / in-between / below the line) that suggests a daily mantra keyed to how you arrived.
  • Add / reorder / relabel tasks live; Eisenhower priority labels (Do first / Schedule / Delegate / Later) with a legend + "sort by priority".
  • Focus mode, kind "not today" carryover (no overdue-shaming), a brain-dump box, a gentle overload nudge, reduced-motion toggle, live clock.
  • End-of-day recap: download or copy a Markdown summary (paste it back to your partner to journal / plan tomorrow).

The highest-leverage part is the executive-function-friendly behavior section in SKILL.md — how the partner shows up (body-double, shrink the first step, one next thing, celebrate starting, never shame an incomplete).

Files

  • skills/daily-focus-board/SKILL.md, assets/board.template.html, scripts/serve-board.ps1, examples/sample-board.html, references (tutorial.md, neurodivergent-design.md, customize.md).
  • plugins/ember/.github/plugin/plugin.json — registers the skill; version 1.0.0 -> 1.1.0.
  • plugins/ember/README.md — adds a Components row.

Notes

  • Design references are cited and link-checked (CHADD, Simply Psychology, MDN, W3C WCAG, Conscious Leadership, Asana/Todoist, self-compassion.org).
  • Verified with a headless DOM test (render + all interactions) and a JS syntax gate.

Opening as a draft for final review.

@github-actions github-actions Bot added new-submission PR adds at least one new contribution plugin PR touches plugins skills PR touches skills labels Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🟡 Contributor Reputation Check: MEDIUM risk

Check Risk
Profile MEDIUM
Credential audit NONE

Maintainers: please review this contributor before merging.
See the workflow run for full details.
Automated check powered by AGT.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor reputation check flagged MEDIUM risk label Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🔒 PR Risk Scan Results

Scanned 16 changed file(s).

Severity Count
🔴 High 0
🟠 Medium 1
ℹ️ Info 1
Severity Rule File Line Match
🟠 unpinned-version-indicator extensions/daily-focus-board/package.json 8 "@​​github/copilot-sdk": "latest"
ℹ️ skill-script-touched skills/daily-focus-board/scripts/serve-board.ps1 1 skills/daily-focus-board/scripts/serve-board.ps1
Skipped non-text or missing files
  • extensions/daily-focus-board/assets/preview.png

This is an automated soft-gate report. Findings indicate review targets and do not block merge by themselves.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🔍 Vally Lint Results

✅ All checks passed

Scope Checked
Skills 1
Agents 0
Total 1
Severity Count
❌ Errors 0
⚠️ Warnings 0
ℹ️ Advisories 0

Summary

Level Finding
ℹ️ ✅ daily-focus-board (2/2 checks passed)
ℹ️ ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
ℹ️ ✓ spec-compliance: All spec checks passed.
ℹ️ ✓ [valid-refs] All file references across 1 skill(s) are valid.
ℹ️ ✓ valid-refs: All file references resolve to existing files within the skill directory.
ℹ️ 1 skill(s) linted, 1 passed
Full linter output
### Linting skills/daily-focus-board
✅ daily-focus-board (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

@jennyf19
jennyf19 marked this pull request as ready for review July 27, 2026 20:50
@jennyf19
jennyf19 requested a review from aaronpowell as a code owner July 27, 2026 20:50
Copilot AI review requested due to automatic review settings July 27, 2026 20:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an executive-function-friendly daily focus board skill to Ember.

Changes:

  • Adds the self-contained interactive board and sample.
  • Documents usage, customization, and design principles.
  • Registers the skill in Ember 1.1.0.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
skills/daily-focus-board/SKILL.md Defines board behavior and usage.
skills/daily-focus-board/assets/board.template.html Implements the interactive board.
skills/daily-focus-board/examples/sample-board.html Provides a populated example.
skills/daily-focus-board/scripts/serve-board.ps1 Serves and opens boards locally.
skills/daily-focus-board/references/tutorial.md Documents the daily workflow.
skills/daily-focus-board/references/neurodivergent-design.md Explains design principles and sources.
skills/daily-focus-board/references/customize.md Covers customization and future integrations.
plugins/ember/README.md Lists the new component.
plugins/ember/.github/plugin/plugin.json Registers the skill and bumps Ember’s version.
Comments suppressed due to low confidence (8)

skills/daily-focus-board/assets/board.template.html:186

  • The arrival choices are clickable spans with no keyboard semantics, so keyboard and assistive-technology users cannot perform the check-in. Render them as buttons (and expose the selected state with aria-pressed when updating them).
      <span class="ci-opt" data-checkin="below">🌧️ below the line</span>
      <span class="ci-opt" data-checkin="mid">⛅ in between</span>
      <span class="ci-opt" data-checkin="above">☀️ above the line</span>

skills/daily-focus-board/assets/board.template.html:347

  • Both status controls are non-focusable elements wired only with onclick, making the core to-do → in-progress → done action unavailable from a keyboard. Use button type="button" elements and retain the existing data-cyc binding.
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${t.emoji||"•"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>

skills/daily-focus-board/examples/sample-board.html:355

  • The sample's status-card emoji is interpolated into innerHTML without escaping, matching the injection issue in the template. Treat it as text.
        <div class="emoji">${t.emoji||"•"}</div>

skills/daily-focus-board/examples/sample-board.html:186

  • These sample controls are click-only spans, so keyboard-only users cannot toggle Focus mode or reduced motion. Keep the example accessible and synchronized with the template by using semantic buttons.
        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

skills/daily-focus-board/examples/sample-board.html:196

  • The sample's arrival choices are also non-focusable clickable spans. Convert them to buttons and update aria-pressed from renderCheckin() so the selected state is available to assistive technology.
      <span class="ci-opt" data-checkin="below">🌧️ below the line</span>
      <span class="ci-opt" data-checkin="mid">⛅ in between</span>
      <span class="ci-opt" data-checkin="above">☀️ above the line</span>

skills/daily-focus-board/examples/sample-board.html:357

  • The sample duplicates the template's keyboard blocker for the primary status transition: neither clickable element can receive keyboard focus. Use semantic buttons and keep the runtime binding unchanged.
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${t.emoji||"•"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>

skills/daily-focus-board/examples/sample-board.html:442

  • The sample's fallback reports success even when execCommand("copy") returns false. Check that return value before invoking the copied-success callback.
function fallbackCopy(md,cb){try{const ta=document.createElement("textarea");ta.value=md;document.body.appendChild(ta);ta.select();document.execCommand("copy");ta.remove();cb&&cb();}catch(e){eodMsg("couldn't copy here — try download 💾");}}

skills/daily-focus-board/examples/sample-board.html:476

  • The sample continuously repaints a full-screen canvas even with no active particles, wasting CPU and battery for a page intended to remain open. Mirror the template fix: start animation on burst() and stop when the particle list empties.
function loop(){if(!cx)return;cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}
render(); loop();

Comment thread skills/daily-focus-board/SKILL.md
Comment thread plugins/ember/.github/plugin/plugin.json Outdated
Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/references/tutorial.md Outdated
Comment thread skills/daily-focus-board/scripts/serve-board.ps1 Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/SKILL.md
Copilot AI review requested due to automatic review settings July 27, 2026 20:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.

Comments suppressed due to low confidence (5)

skills/daily-focus-board/assets/board.template.html:334

  • Config-derived values are interpolated into innerHTML without escaping: emoji is raw here, and id is raw in the surrounding attributes/selectors. A crafted task can inject markup/script or break rendering. Build these nodes with DOM APIs, or consistently escape text/attributes and validate/escape IDs (including selector use).
      c.innerHTML=`<div class="top"><div class="emoji">${t.emoji||"🎯"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>
          <div class="sub"><b style="color:var(--ink)">${v.toLocaleString()}</b> / ${t.goal.toLocaleString()} ${esc(unit)} · ${pct}%${st==="done"?" — done 🎉":""}</div>
          ${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${st}">${LABEL[st]}</span>
        <button class="fbtn" data-focus="${t.id}" title="focus on this">🎯</button>${rmHtml}</div>

skills/daily-focus-board/examples/sample-board.html:344

  • This standalone example repeats the template's unsafe innerHTML interpolation of config-derived emoji and id values. Customized sample data can inject markup/script or invalidate selectors. Apply the same text/attribute escaping and ID validation as in the source template.
      c.innerHTML=`<div class="top"><div class="emoji">${t.emoji||"🎯"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>
          <div class="sub"><b style="color:var(--ink)">${v.toLocaleString()}</b> / ${t.goal.toLocaleString()} ${esc(unit)} · ${pct}%${st==="done"?" — done 🎉":""}</div>
          ${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${st}">${LABEL[st]}</span>
        <button class="fbtn" data-focus="${t.id}" title="focus on this">🎯</button>${rmHtml}</div>

skills/daily-focus-board/assets/board.template.html:432

  • document.execCommand("copy") returns false when copying is denied without necessarily throwing, but this always calls the success callback and tells the user the recap was copied. Check the boolean result and show the failure message when it is false; also remove the temporary textarea in finally.
function fallbackCopy(md,cb){try{const ta=document.createElement("textarea");ta.value=md;document.body.appendChild(ta);ta.select();document.execCommand("copy");ta.remove();cb&&cb();}catch(e){eodMsg("couldn't copy here — try download 💾");}}

skills/daily-focus-board/SKILL.md:1

  • The new skill is absent from docs/README.skills.md, so it will not appear in the repository's generated skill catalog. Run npm run build and commit the generated documentation.
---

plugins/ember/.github/plugin/plugin.json:4

  • The checked-in .github/plugin/marketplace.json still advertises Ember version 1.0.0, so this 1.1.0 release is not reflected in the generated marketplace metadata. Run npm run build and commit the regenerated output.
  "version": "1.1.0",

Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/SKILL.md
Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html
Comment thread skills/daily-focus-board/scripts/serve-board.ps1 Outdated
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 21:18
@jennyf19
jennyf19 force-pushed the add-daily-focus-board-skill branch from 6e1b5fc to c514d58 Compare July 27, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.

Comments suppressed due to low confidence (6)

skills/daily-focus-board/SKILL.md:80

  • This command binds Python's HTTP server to every network interface by default, exposing the user's board and any other files in the served directory to the local network. Bind explicitly to loopback because this board may contain personal tasks and notes.
opening the file path directly:

skills/daily-focus-board/references/tutorial.md:22

  • The example server command listens on all interfaces by default, making the personal board reachable from other machines on the local network. Keep the documented default private by binding it to loopback.
1. Ask Ember to make the board. It serves the folder (e.g. `python -m http.server 8790 --bind 127.0.0.1`) and gives

skills/daily-focus-board/scripts/serve-board.ps1:12

  • http.server binds to every interface unless told otherwise, so this helper exposes the board directory to the local network despite only printing a localhost URL. Bind the server to loopback and use the matching URL.
Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden
Start-Sleep -Seconds 2
$url = "http://localhost:$Port/$File"

skills/daily-focus-board/assets/board.template.html:447

  • Turning on Reduce motion does not stop confetti already running in the canvas requestAnimationFrame loop; it only prevents future bursts and CSS animation. Cancel the active frame and clear particles/canvas when the toggle becomes enabled.
document.getElementById("rmchip").onclick=()=>{state.rm=!state.rm;document.body.classList.toggle("rm",state.rm);document.getElementById("rmchip").classList.toggle("on",state.rm);document.getElementById("rmchip").setAttribute("aria-pressed",state.rm?"true":"false");save();};

skills/daily-focus-board/assets/board.template.html:438

  • When dateKey is omitted, this filename uses a UTC date while the board's storage key and displayed day use local time. Around local midnight the downloaded recap can therefore be named for the previous or next day; reuse todayLocal() here.
function downloadRecap(){const md=dayRecap();try{const blob=new Blob([md],{type:"text/markdown"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download="focus-"+(CFG.dateKey||new Date().toISOString().slice(0,10))+".md";document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);eodMsg("saved a recap to your downloads 💾");}catch(e){eodMsg("couldn't auto-download here — use copy instead 📋");}}

skills/daily-focus-board/examples/sample-board.html:448

  • When dateKey is omitted, this filename uses a UTC date while the sample's storage key and displayed day use local time. Around local midnight the recap can be named for the wrong day; reuse todayLocal() here.
function downloadRecap(){const md=dayRecap();try{const blob=new Blob([md],{type:"text/markdown"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download="focus-"+(CFG.dateKey||new Date().toISOString().slice(0,10))+".md";document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);eodMsg("saved a recap to your downloads 💾");}catch(e){eodMsg("couldn't auto-download here — use copy instead 📋");}}

Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/assets/board.template.html
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (2)

skills/daily-focus-board/assets/board.template.html:328

  • Reordering is exposed only through a non-focusable HTML drag handle, so keyboard-only users cannot perform the advertised arbitrary reorder operation. Add keyboard-operable move controls (or a focusable grip with documented key handlers) and announce the updated position.
    const metaHtml=`<div class="cardmeta"><span class="grip" draggable="true" data-grip="${t.id}" title="drag to reorder">⠿</span>`

skills/daily-focus-board/examples/sample-board.html:338

  • The standalone sample likewise exposes reorder only through a non-focusable drag handle. Keyboard-only users need move controls or a focusable grip with key handlers and an announced result.
    const metaHtml=`<div class="cardmeta"><span class="grip" draggable="true" data-grip="${t.id}" title="drag to reorder">⠿</span>`

Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/assets/board.template.html
Comment thread skills/daily-focus-board/examples/sample-board.html
Comment thread skills/daily-focus-board/SKILL.md Outdated
Comment thread skills/daily-focus-board/references/neurodivergent-design.md Outdated
Comment thread skills/daily-focus-board/references/neurodivergent-design.md Outdated
Executive-function-friendly daily focus board (self-contained HTML) you run by
talking to Ember. Registers it in the ember plugin (1.1.0) and regenerates the
skills index + marketplace.json.

Review fixes: emoji XSS escaping; keyboard a11y (semantic buttons + aria, plus
up/down move controls); tagc label colors; local-date storage key + recap
date/filename; guarded execCommand and localStorage; confetti animates only
while active and stops on reduce-motion; loopback serve bind + quoted dir;
JSON config injection with '<' escaped; minute rounding; reorder-to-end; clear
stale focus; counter goal guard; and doc corrections (id charset, sample
reference, counter contract, option-b, frontmatter length).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 07e720ee-ca02-419e-9adb-300738b6fc76
Copilot AI review requested due to automatic review settings July 27, 2026 22:13
@jennyf19
jennyf19 force-pushed the add-daily-focus-board-skill branch from 278c0bd to 62bfb28 Compare July 27, 2026 22:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

skills/daily-focus-board/assets/board.template.html:271

  • The implicit date is captured here for storage, but recapDate() and the download filename call todayLocal() again later. If the board remains open across local midnight, it keeps saving under yesterday's key while the recap heading and filename switch to today. Capture one board date key and reuse it for storage, recap rendering, and download naming.
const KEY="focus-board-"+(CFG.dateKey||todayLocal());

skills/daily-focus-board/examples/sample-board.html:281

  • The sample also freezes the storage key at load but recomputes the recap date and filename at download time. Crossing local midnight therefore labels yesterday's persisted board as the new day. Reuse one captured board date across all three paths.
const KEY="focus-board-"+(CFG.dateKey||todayLocal());

Comment thread skills/daily-focus-board/assets/board.template.html Outdated
Comment thread skills/daily-focus-board/examples/sample-board.html Outdated
Comment thread skills/daily-focus-board/references/customize.md Outdated
Comment thread skills/daily-focus-board/scripts/serve-board.ps1 Outdated
- Sanitize tagc to [A-Za-z0-9_-] before class-attribute interpolation, so a custom
  class name cannot break out of the attribute and inject markup/handlers (template + sample).
- customize.md now points at .tagedit.<name> (the class the renderer actually applies).
- serve-board.ps1: fail when the port is already in use, capture and report the server PID
  (with a stop command), and verify the process did not exit before opening the URL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 07e720ee-ca02-419e-9adb-300738b6fc76
Copilot AI review requested due to automatic review settings July 27, 2026 22:24

@aaronpowell aaronpowell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than a skill, would this not make more sense to be a canvas extension?

Adds a canvas version of the daily-focus-board alongside the skill, addressing
@aaronpowell's review suggestion. The canvas renders the board in the Copilot
app and is backed by a JSON state file the assistant reads and writes, so you
can mark tasks done, add tasks, log progress, and recap your day from chat --
the file-backed "close the loop" upgrade over the localStorage skill.

The skill stays as the zero-install universal fallback for anyone not in the
Copilot app (same both-not-either pattern as the-workshop's signals-dashboard).

- extensions/daily-focus-board/: extension.mjs (canvas + session wiring) +
  board-core.mjs (loopback server, JSON state file, mutations, recap) +
  assets/board.html (file-backed UI) + preview.png + manifests.
- plugins/ember: register via x-awesome-copilot.extensions, bump 1.1.0 -> 1.2.0,
  add a Components row.

Note: assets/preview.png is a placeholder pending a real board screenshot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 28, 2026 18:43
@github-actions github-actions Bot added the canvas-extension PR touches canvas extensions label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (15)

extensions/daily-focus-board/assets/board.html:207

  • The status-task emoji is also interpolated directly into innerHTML, so seed/action input can inject markup into each card. Apply the same escaping used for titles and units.
        <div class="emoji">${t.emoji||"•"}</div>

extensions/daily-focus-board/assets/board.html:233

  • The momentum feed reuses the unescaped task emoji in another innerHTML assignment, allowing the same supplied value to be interpreted as markup here. Escape it before interpolation.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${it.emoji}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:211

  • The note-delete affordance is a click-only span, making progress-note deletion unavailable from the keyboard and exposing no delete-button semantics to assistive technology. Render it as a labeled button.
        ${notes.length?`<div class="notes">${notes.map((n,i)=>`<div class="note"><span class="nt">${fmt(n.t)}</span><span>${esc(n.txt)}</span><span class="nx" data-del="${t.id}:${i}">×</span></div>`).join("")}</div>`:""}

extensions/daily-focus-board/assets/board.html:233

  • The momentum-item delete control is also a click-only span; keyboard and assistive-technology users cannot operate it. Use a labeled native button.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${it.emoji}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:238

  • The parked-thought delete control repeats the click-only span pattern, so it cannot be focused or activated by keyboard. Use a labeled native button.
  brainEl.innerHTML=state.brain.map((n,i)=>`<div class="fitem think"><span class="ft">${fmt(n.t)}</span><span>💭</span><span>${esc(n.txt)}</span><span class="fx" data-bdel="${i}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:125

  • The Focus-mode exit is another click-only span, so a keyboard user can enter Focus mode after the toggle is fixed but cannot exit it from this control. Use a native button.
  <div class="focusbar" id="focusbar"><span id="focustxt"></span><span class="x" id="focusexit">show all ✕</span></div>

.github/plugin/marketplace.json:532

  • The generated marketplace advertises 1.1.0, while the source manifest is 1.2.0. eng/generate-marketplace.mjs:54-59 copies the manifest version, so this stale output makes plugin discovery/install metadata inconsistent. Regenerate it or update this entry to 1.2.0.
      "version": "1.1.0"

extensions/daily-focus-board/board-core.mjs:56

  • toISOString() derives the day in UTC. Near local midnight this can seed yesterday's or tomorrow's board even though the UI clock and local due values use local time; the skill template already uses local calendar components. Build the key from local year/month/day instead.
export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:68

  • A positive fractional goal such as 0.1 passes the check but rounds to 0. The action schema currently permits any number, and a zero goal produces an always-done counter and invalid progress percentage. Clamp the rounded value to at least 1 (or reject non-integers).
        o.goal = Math.round(goal);

extensions/daily-focus-board/board-core.mjs:83

  • Duplicate task IDs in a seed/state file survive normalization. Those cards then share one progress entry, while API lookups only mutate the first definition, producing ambiguous and incorrect board state. Reject or deduplicate IDs during normalization.
    doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];

extensions/daily-focus-board/board-core.mjs:150

  • A counter seeded with nonzero progress is reported as todo because this compares the current value with its start value. For example, start: 8 displays 8 completed units but remains to-do; the standalone board correctly treats any value above zero as in progress.
        return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");

extensions/daily-focus-board/board-core.mjs:139

  • mutate treats an undefined mutator result as success. Most op* functions return undefined both after a valid mutation and for an unknown task, wrong task type, or empty input, so actions such as set_status and set_count can return ok: true without doing anything. Return explicit errors for invalid/no-op requests so the agent does not falsely confirm changes.
        const out = fn(doc) || {};
        if (out.error) return { ok: false, error: out.error };
        doc.updatedAt = new Date().toISOString();
        await atomicWrite(file, doc);
        return out.id ? { ok: true, state: doc, id: out.id } : { ok: true, state: doc };

extensions/daily-focus-board/assets/board.html:191

  • The action/seed-provided emoji is inserted into innerHTML without escaping. Even within the current eight-character limit, a value such as <b>x</b> is interpreted as markup rather than board text. Escape it at this HTML sink.

This issue also appears in the following locations of the same file:

  • line 207
  • line 233
      c.innerHTML=`<div class="top"><div class="emoji">${t.emoji||"🎯"}</div>

extensions/daily-focus-board/assets/board.html:120

  • These interactive toggles are plain span elements, so keyboard users cannot focus or activate Focus mode or reduced motion. Use native buttons (and expose pressed state when it changes); this is especially important for the reduced-motion accessibility control.

This issue also appears on line 125 of the same file.

        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

extensions/daily-focus-board/assets/board.html:209

  • Both controls that change a task's status are non-interactive div/span elements wired only with onclick. They are unavailable to keyboard and assistive-technology users; render them as labeled buttons, as the standalone template does.

This issue also appears in the following locations of the same file:

  • line 211
  • line 233
  • line 238
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${t.emoji||"•"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>

Comment thread extensions/daily-focus-board/board-core.mjs Outdated
@jennyf19

Copy link
Copy Markdown
Contributor Author

@aaronpowell great call — done. I pushed a canvas extension version to this PR (extensions/daily-focus-board/, registered in the ember plugin via x-awesome-copilot.extensions).

The canvas is the richer in-app experience and it closes the agent loop: it's backed by a JSON state file the assistant reads and writes, so you can "mark the design doc done", "add call the dentist", "log 6k steps", or "recap my day" from chat — and the board updates live (it polls the same file).

I kept the skill too, as the zero-install universal fallback for anyone not in the Copilot app — the same both-not-either pattern as the-workshop (its skills + the signals-dashboard canvas). npm run plugin:validate passes (✅ extension daily-focus-board is valid).

One honest flag: assets/preview.png is a branded placeholder for now — I'll swap in a real board screenshot.

jennyf19 and others added 3 commits July 28, 2026 16:54
Addresses the Copilot review on board-core.mjs: an externally supplied
stateFile pointing at an existing non-board file was silently normalized to an
empty board and overwritten on first mutation. ensureStateFile now refuses an
existing file unless it parses as JSON and looks like a board (a
'daily-focus-board' marker or the board schema); the marker is stamped on every
write. Also reserve 'day'/'brain' as task ids so a task can't collide with the
momentum/brain-dump feed delete routing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
…main merge

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 29, 2026 00:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (17)

extensions/daily-focus-board/board-core.mjs:73

  • A positive fractional goal below 0.5 passes the check but rounds to 0. That creates a counter which is immediately done and causes zero-goal progress calculations. Clamp the rounded value to at least one (or reject non-integers).
        o.goal = Math.round(goal);

extensions/daily-focus-board/assets/board.html:288

  • Canvas confetti only checks the board toggle and ignores the user's OS prefers-reduced-motion setting. The standalone board honors that preference by default; this canvas can animate for motion-sensitive users before they find the toggle.
function burst(n){if(state&&state.rm)return;for(let i=0;i<n;i++)parts.push({x:innerWidth/2+(Math.random()-.5)*220,y:innerHeight*0.28,vx:(Math.random()-.5)*11,vy:Math.random()*-13-4,g:.42,life:70+Math.random()*40,c:COL[i%COL.length],s:5+Math.random()*6,rot:Math.random()*6});}

extensions/daily-focus-board/assets/board.html:207

  • The status-task branch also inserts the untrusted emoji value directly into innerHTML, allowing markup injection and broken card rendering. Escape this occurrence too.
        <div class="emoji">${t.emoji||"•"}</div>

extensions/daily-focus-board/board-core.mjs:196

  • Completed status tasks can still be carried, which removes a finished task from today's ring and reports it as “tomorrow” in the recap. The standalone board intentionally hides carryover for done tasks; reject carrying a completed task here as well.
    const t = findTask(doc, id);
    if (!t || isCounter(t)) return;
    const e = doc.progress.t[id];
    e.carried = typeof value === "boolean" ? value : !e.carried;

extensions/daily-focus-board/board-core.mjs:61

  • toISOString() uses UTC, while the board clock and the skill's date key use local time. Users west/east of UTC can therefore get yesterday's or tomorrow's date in the state file and recap around midnight.

This issue also appears on line 73 of the same file.

export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:88

  • Seed files can contain duplicate valid task IDs, but normalization keeps every copy. Both copies then share one progress entry while mutations resolve only the first task, producing ambiguous cards and updates. Deduplicate IDs here as the standalone board does.
    doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];

extensions/daily-focus-board/board-core.mjs:142

  • Invalid task IDs, wrong task kinds, and empty values currently make the mutation callbacks return undefined, which this converts into a successful { ok: true } response and writes unchanged state. Extension actions can consequently report that a task was updated when nothing happened. Have mutation operations return explicit errors for invalid/no-op requests and propagate them here.
        const out = fn(doc) || {};
        if (out.error) return { ok: false, error: out.error };

extensions/daily-focus-board/extension.mjs:145

  • The action promises to enter Focus mode on taskId, but opFocus toggles and clears focus when that same ID is already selected. Repeating an idempotent “focus on X” request therefore turns Focus mode off. Split the action's set behavior from the canvas toggle behavior.
                    handler: (ctx) => act(ctx, doc => opFocus(doc, ctx.input.taskId || null)),

extensions/daily-focus-board/board-core.mjs:156

  • A seeded counter with nonzero progress is classified as todo because its current value equals start. The standalone board classifies any value above zero as in progress, so the file-backed recap/status disagrees for the same configuration.

This issue also appears on line 193 of the same file.

        return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");

extensions/daily-focus-board/assets/board.html:155

  • This duplicates the counter-status error in the core: a counter seeded at (for example) 8/30 renders as to do until it exceeds 8. Treat any positive current value as in progress so the canvas matches the standalone board and recap.
function statusOf(t){if(isCounter(t)){const v=state.counters[t.id]||0;return v>=t.goal?"done":(v>(t.start||0)?"doing":"todo");}return (state.t[t.id]||{}).status||"todo";}

extensions/daily-focus-board/assets/board.html:177

  • Rounding the minute remainder independently can produce countdowns such as 1h 60m left. Round total minutes first, then derive hours and minutes, as the standalone template does.
  if(ms<=0){const h=Math.floor(-ms/3600e3),m=Math.round((-ms%3600e3)/60e3);return {cls:"over",txt:`⏰ was due ${fmt(new Date(t.due))}${h||m?` · ${h?h+"h ":""}${m}m ago`:""} — still worth doing`};}
  const h=Math.floor(ms/3600e3),m=Math.round((ms%3600e3)/60e3);
  return {cls:"",txt:`⏰ ${h?h+"h ":""}${m}m left (due ${fmt(new Date(t.due))})`};

extensions/daily-focus-board/assets/board.html:289

  • This loop schedules another animation frame forever, even when there are no confetti particles. Since it starts on the first state load, every open board continuously renders at display refresh rate and wastes CPU/battery. Start the loop from burst() and stop scheduling when parts is empty, as the standalone template does.
function loop(){cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}

extensions/daily-focus-board/assets/board.html:191

  • emoji is seed/state data but is inserted into innerHTML without escaping. For example, the allowed eight-character value <style> corrupts the rendered card markup. Escape it like the standalone template does.

This issue also appears on line 207 of the same file.

      c.innerHTML=`<div class="top"><div class="emoji">${t.emoji||"🎯"}</div>

extensions/daily-focus-board/assets/board.html:233

  • Task emoji is again emitted unescaped in the momentum feed. A markup-like emoji value can corrupt every feed item associated with that task; pass it through esc.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${it.emoji}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:120

  • Focus mode and reduced motion are clickable span elements, so keyboard and assistive-technology users cannot operate these primary controls. The same non-semantic pattern is repeated for focus exit, status pills, and delete controls below. Use native buttons (with pressed state for toggles) throughout.

This issue also appears on line 288 of the same file.

        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

plugins/ember/.github/plugin/plugin.json:4

  • The PR description says Ember is bumped from 1.0.0 to 1.1.0, while both this manifest and the generated marketplace use 1.2.0. Align the intended release version or update the PR description before publishing.
  "version": "1.2.0",

extensions/daily-focus-board/assets/board.html:212

  • The canvas always renders “not today,” including on completed tasks. Clicking it turns a completed item into a carried-tomorrow item and removes it from today's completion count. Match the standalone board by hiding this control when st === "done".
        <div class="cardfoot"><button class="soft" data-carry="${t.id}">${carried?"↩ bring back to today":"⤳ not today"}</button></div>

Comment thread extensions/daily-focus-board/board-core.mjs Outdated
Replaces the generated placeholder with an actual rendered board (cropped to the
visible cards). Closes the last gap flagged in the review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 29, 2026 01:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (19)

extensions/daily-focus-board/assets/board.html:207

  • Status-task emojis are also untrusted seed/action data and are inserted into innerHTML without escaping, allowing markup/event-handler injection. Escape this value before rendering.
        <div class="emoji">${t.emoji||"•"}</div>

extensions/daily-focus-board/assets/board.html:233

  • Momentum entries copy the task emoji and interpolate it unescaped into innerHTML. A malicious emoji therefore executes again as soon as that task has a progress note; escape it.emoji here as well.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${it.emoji}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:125

  • The only control for leaving Focus mode is a click-only span, making keyboard users unable to exit from this bar. Render it as a real button with an accessible name.
  <div class="focusbar" id="focusbar"><span id="focustxt"></span><span class="x" id="focusexit">show all ✕</span></div>

extensions/daily-focus-board/assets/board.html:210

  • Both status controls are click-only div/span elements. They are absent from the tab order and expose no button role or task/status label, so keyboard and assistive-technology users cannot update task status. Use semantic buttons with descriptive accessible names.
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${t.emoji||"•"}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
        <button class="fbtn" data-focus="${t.id}" title="focus on this">🎯</button>${tagHtml}</div>

extensions/daily-focus-board/assets/board.html:211

  • The note delete control is a click-only span containing only ×; it cannot be focused or identified by assistive technology. Use a button with an aria-label such as “delete note”.
        ${notes.length?`<div class="notes">${notes.map((n,i)=>`<div class="note"><span class="nt">${fmt(n.t)}</span><span>${esc(n.txt)}</span><span class="nx" data-del="${t.id}:${i}">×</span></div>`).join("")}</div>`:""}

extensions/daily-focus-board/assets/board.html:233

  • Momentum-item deletion is exposed as a click-only span with no accessible name. Replace it with a keyboard-focusable button labeled “delete momentum entry”.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${it.emoji}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:238

  • Brain-dump deletion is a click-only span, so keyboard and assistive-technology users cannot operate or identify it. Use a semantic button with a descriptive aria-label.
  brainEl.innerHTML=state.brain.map((n,i)=>`<div class="fitem think"><span class="ft">${fmt(n.t)}</span><span>💭</span><span>${esc(n.txt)}</span><span class="fx" data-bdel="${i}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:289

  • This loop schedules another animation frame even when parts is empty, so merely opening the board keeps the browser rendering at display refresh rate indefinitely. Start the RAF from burst and stop scheduling once all particles are gone.
function loop(){cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}

extensions/daily-focus-board/board-core.mjs:61

  • toISOString() derives the key in UTC, so users west of UTC get tomorrow's board during their evening (and users east of UTC can get yesterday's shortly after midnight). The standalone board already uses local calendar components; use the same local-date calculation here.
export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:73

  • A positive fractional goal below 0.5 passes the guard but rounds to zero. The task then becomes immediately done and the canvas computes 0 / 0, producing a NaN% progress width. Clamp the normalized integer goal to at least 1.
        o.goal = Math.round(goal);

extensions/daily-focus-board/board-core.mjs:88

  • Seeded/existing documents can contain duplicate valid task IDs. Both cards then share one progress object and DOM selectors target only the first matching card, so updates and progress bars become inconsistent. Deduplicate IDs during normalization, matching opAddTask's uniqueness guarantee.
    doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];

extensions/daily-focus-board/board-core.mjs:94

  • These arrays are accepted without validating their entries, despite normalize promising a well-formed document. An existing board with day: [null] or brain: [null] passes looksLikeBoard and then crashes the canvas when rendering n.t/n.txt. Filter entries to the shape produced by the mutation functions.
    p.day = Array.isArray(p.day) ? p.day : [];
    p.brain = Array.isArray(p.brain) ? p.brain : [];

extensions/daily-focus-board/board-core.mjs:145

  • A missing task, wrong task kind, empty note, or invalid operation currently returns undefined; mutate treats that as success, writes the file, and returns { ok: true }. Canvas actions therefore tell the agent a requested change succeeded when nothing changed. Have mutation operations return explicit errors for invalid/no-op requests and propagate them through this wrapper.
        const out = fn(doc) || {};
        if (out.error) return { ok: false, error: out.error };
        doc.updatedAt = new Date().toISOString();
        await atomicWrite(file, doc);
        return out.id ? { ok: true, state: doc, id: out.id } : { ok: true, state: doc };

extensions/daily-focus-board/board-core.mjs:156

  • start is the initial current count, not a baseline to subtract. With start: 8, the task has visible progress but this reports todo until it reaches 9; the standalone board correctly treats any positive count as in progress. Use v > 0 here and in the canvas copy of statusOf.
        const v = doc.progress.counters[t.id] || 0;
        return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");

extensions/daily-focus-board/assets/board.html:155

  • The canvas classifies a counter initialized with a positive start value as todo, even though it already has progress. This diverges from the standalone board and from the corrected server-side status calculation; classify any positive value as doing.
function statusOf(t){if(isCounter(t)){const v=state.counters[t.id]||0;return v>=t.goal?"done":(v>(t.start||0)?"doing":"todo");}return (state.t[t.id]||{}).status||"todo";}

extensions/daily-focus-board/extension.mjs:145

  • The action contract says providing taskId enters Focus mode and omitting it clears focus, but opFocus toggles the value. Calling focus twice with the same task therefore clears it unexpectedly. Set the requested focus explicitly in this action while retaining toggle behavior for canvas clicks.
                    handler: (ctx) => act(ctx, doc => opFocus(doc, ctx.input.taskId || null)),

extensions/daily-focus-board/assets/board.html:166

  • The board only applies its persisted toggle and never checks prefers-reduced-motion; fresh state is normalized to rm:false, so users who request reduced motion at the OS level still receive confetti and transitions. This contradicts the documented behavior in skills/daily-focus-board/references/neurodivergent-design.md:30. Incorporate the media query into both styling and burst suppression.
  document.body.classList.toggle("rm",!!state.rm);
  document.getElementById("rmchip").classList.toggle("on",!!state.rm);

extensions/daily-focus-board/assets/board.html:120

  • These mode controls are non-focusable spans with click handlers, so keyboard and switch-device users cannot activate them or learn their pressed state. Use semantic buttons and keep aria-pressed synchronized with the focus/reduced-motion state.

This issue also appears in the following locations of the same file:

  • line 125
  • line 206
  • line 211
  • line 233
  • line 238
        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

extensions/daily-focus-board/assets/board.html:212

  • This renders “not today” even for completed tasks, whereas the standalone board hides carryover once a task is done. Clicking it removes a completed item from today's ring and labels it “tomorrow” in the recap. Hide this control for done tasks and have opCarry reject the same transition so agent actions cannot create it either.
        <div class="cardfoot"><button class="soft" data-carry="${t.id}">${carried?"↩ bring back to today":"⤳ not today"}</button></div>

Comment thread extensions/daily-focus-board/assets/board.html Outdated
Comment thread extensions/daily-focus-board/board-core.mjs Outdated
Comment thread extensions/daily-focus-board/board-core.mjs
… + skill a11y

Resolves the remaining Copilot review comments.

Canvas extension (board-core.mjs, board.html):
- DNS-rebinding: pin the Host header to the exact 127.0.0.1:<port> authority and
  require a per-server capability token (minted at startup, embedded in the served
  page, sent as x-board-token) on ALL /api/* routes -- so GET /api/state can't leak
  task data and POSTs can't be forged. Mirrors extensions/signals-dashboard.
- Destructive write: loadDoc only synthesizes a fresh board for ENOENT and now
  propagates I/O + JSON parse errors, so a transient/malformed state file is never
  overwritten by a later mutation.
- XSS: escape emoji (from the seed / add_task action) at render, like title/unit.

Skill (board.template.html, sample-board.html):
- a11y: each task card gets role=group + aria-label so screen readers get task context.
- counters: step=1 on the goal/update number inputs to match the positive-integer contract.

Verified headless (35/35): token gates reads+writes, CSRF + foreign-Host refused,
malformed file left intact. Repo plugin + skill validation green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 29, 2026 03:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (7)

extensions/daily-focus-board/assets/board.html:215

  • These task-status controls are clickable div/span elements with no keyboard semantics, so keyboard and assistive-technology users cannot advance a task's status. Use real button elements with accessible labels and expose the current state (the standalone template already follows this pattern).
      c.innerHTML=`<div class="top">
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${esc(t.emoji||"•")}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
        <button class="fbtn" data-focus="${t.id}" title="focus on this">🎯</button>${tagHtml}</div>

extensions/daily-focus-board/extension.mjs:5

  • The PR description says this is a self-contained HTML skill with “no backend” and lists no extension files, but this adds a separate SDK canvas extension, local HTTP API, file-backed state, and marketplace entry. Please either document and test this additional deliverable in the PR title/description/files, or split it into a separate PR so the reviewed scope matches what is being shipped.
// Extension: daily-focus-board
// Renders the daily focus board as a GHCP canvas, backed by a JSON state file
// that both the canvas UI and the AI partner read and write. All the real logic
// (server, state, mutations, recap) lives in board-core.mjs so it can be tested
// without the SDK; this file only wires the canvas/session lifecycle + actions.

extensions/daily-focus-board/board-core.mjs:62

  • This derives the board's “today” key in UTC. In time zones behind or ahead of UTC, opening the board around local midnight assigns it to the wrong calendar day and produces an incorrect recap date. Build the key from local date components, matching the standalone board's todayLocal() behavior.
export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:75

  • A positive fractional goal below 0.5 passes this guard but rounds to 0. The canvas then treats the counter as complete immediately and calculates v / 0, producing an invalid progress width. Ensure normalization can never emit a counter goal below 1 (and ideally constrain the action schema to positive integers).
    if (goal !== undefined && goal > 0) {
        o.goal = Math.round(goal);
        o.start = Math.max(0, Math.round(num(t.start) || 0));

extensions/daily-focus-board/assets/board.html:294

  • Once the board loads, this schedules animation frames forever—even with no confetti and when reduced motion is enabled—continuously clearing the canvas at display refresh rate. This wastes CPU and battery while an idle focus board remains open. Start the loop only when burst() adds particles and stop scheduling when the particle list becomes empty, as the standalone template does.
function burst(n){if(state&&state.rm)return;for(let i=0;i<n;i++)parts.push({x:innerWidth/2+(Math.random()-.5)*220,y:innerHeight*0.28,vx:(Math.random()-.5)*11,vy:Math.random()*-13-4,g:.42,life:70+Math.random()*40,c:COL[i%COL.length],s:5+Math.random()*6,rot:Math.random()*6});}
function loop(){cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}

plugins/ember/.github/plugin/plugin.json:4

  • The PR description says the Ember plugin is bumped from 1.0.0 to 1.1.0, while the manifest and generated marketplace entry use 1.2.0. Please align the release version or update the PR description so reviewers and release tooling have one intended version.
  "version": "1.2.0",

extensions/daily-focus-board/assets/board.html:126

  • Focus mode, reduced motion, and “show all” are implemented as clickable spans, so they cannot be reached or activated from the keyboard and do not expose pressed/expanded state. Replace them with buttons and keep aria-pressed synchronized when state changes, as in skills/daily-focus-board/assets/board.template.html.

This issue also appears on line 210 of the same file.

        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>
      </div>
    </div>
  </header>

  <div class="focusbar" id="focusbar"><span id="focustxt"></span><span class="x" id="focusexit">show all ✕</span></div>

Comment thread extensions/daily-focus-board/board-core.mjs
Comment thread extensions/daily-focus-board/board-core.mjs Outdated
jennyf19 and others added 2 commits July 29, 2026 08:16
Resolves two more Copilot review comments on board-core.mjs:
- Mutations no longer report false success: every op returns an explicit error
  for unknown task ids, wrong task kinds, and empty/no-op inputs, and mutate
  propagates it (so set_status on a bad id fails instead of claiming { ok: true }).
- The default state filename is now date-scoped (focus-board-<YYYY-MM-DD>.json),
  so reopening the board tomorrow starts a fresh day instead of silently reusing
  today's tasks/progress. Explicit stateFile paths are used as-is; input doc updated.

Headless test 39/39 (adds op-error + date-scope cases).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 29, 2026 15:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (11)

skills/daily-focus-board/assets/board.template.html:423

  • This validator allows object-prototype names such as __proto__ and toString. Because task IDs index plain state objects, those IDs make ensureTaskState() mutate inherited global objects instead of initializing task state; day also collides with momentum-feed delete routing. Reject these reserved keys.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}

skills/daily-focus-board/examples/sample-board.html:431

  • The executable sample has the same reserved-ID issue as the template: IDs like __proto__ or toString resolve through the prototype of the state maps, and day collides with feed deletion. Reject these names here as well so copying the sample remains safe.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}

extensions/daily-focus-board/board-core.mjs:62

  • toISOString() derives the date in UTC, while the board clock and standalone implementation use the user's local date. Around UTC midnight, many users will open tomorrow's/yesterday's state file and the board will roll over mid-day. Build this key from local date components.
export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:164

  • A counter seeded with positive progress (for example start: 8) is reported as todo because its current value equals start. The standalone board treats any value above zero as in progress, and the extension otherwise displays that positive progress, so the tally/status is inconsistent until the counter advances again.
        return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");

extensions/daily-focus-board/board-core.mjs:74

  • A positive fractional goal below 0.5 passes the guard and rounds to zero. That creates a counter which is immediately done and whose UI computes progress by dividing by zero. Ensure normalization cannot produce a zero goal (and constrain the action schema to positive integers as a follow-up).
        o.goal = Math.round(goal);

extensions/daily-focus-board/board-core.mjs:89

  • Normalization does not enforce the task-ID uniqueness required by the board contract. A seed/state file with duplicate IDs renders multiple cards sharing one progress entry, while selectors and actions update only the first matching card. Deduplicate IDs while normalizing, as the standalone template already does.
    doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];

skills/daily-focus-board/scripts/serve-board.ps1:15

  • -WindowStyle is unsupported by Start-Process on Linux/macOS, so this helper fails under pwsh on the same non-Windows platforms that the preceding Get-NetTCPConnection fallback explicitly accommodates. Add WindowStyle only on Windows.
$proc = Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden -PassThru

plugins/ember/.github/plugin/plugin.json:4

  • The PR description says Ember is bumped from 1.0.0 to 1.1.0, but the source manifest (and generated marketplace entry) use 1.2.0. Align the intended release version or update the PR description so release tooling and reviewers have one authoritative version.
  "version": "1.2.0",

extensions/daily-focus-board/assets/board.html:121

  • These primary controls are clickable span elements, so keyboard users cannot focus or activate Focus mode, reduced motion, or the exit control. The same pattern is used for generated status/delete controls later in this file. Use native buttons and maintain aria-pressed for toggles across all of these interactions.
        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

extensions/daily-focus-board/assets/board.html:294

  • This loop schedules another animation frame forever, even when there are no confetti particles or reduced motion is enabled. The idle board therefore clears a full-screen canvas about 60 times per second for its entire lifetime. Start the loop from burst() only when needed and stop scheduling when parts becomes empty, as the standalone template does.
function loop(){cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}

skills/daily-focus-board/SKILL.md:61

  • The documented ID rule still permits the reserved names that collide with the board's plain-object state maps and feed routing (for example __proto__, toString, and day). After tightening validation, document those exclusions here so the agent does not generate tasks that are dropped or corrupt routing.
- `id` must be unique and stable, using only letters, digits, hyphens, or underscores
  (`[A-Za-z0-9_-]`) — it's embedded in HTML attributes, CSS selectors, storage keys, and
  colon-delimited note keys, so avoid quotes, colons, brackets, and spaces. `tagc` is an optional color class:

Comment thread extensions/daily-focus-board/board-core.mjs Outdated
… safety)

Resolves the Copilot review on board-core.mjs: task ids like `toString` /
`constructor` passed validId and, used as object keys, could resolve to
Object.prototype members. Now validId also rejects any inherited name (`s in {}`),
and the progress maps (counters, t) are null-prototype so a task id can never
resolve to an inherited member. (`__proto__` was already blocked by the id grammar.)

Headless test 43/43 (adds inherited-name rejection, drop-on-normalize, and an
Object.prototype-intact check).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308
Copilot AI review requested due to automatic review settings July 29, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (12)

skills/daily-focus-board/scripts/serve-board.ps1:15

  • -WindowStyle is unsupported by Start-Process on non-Windows PowerShell, so this script fails on the same platforms for which the preceding Get-NetTCPConnection fallback was added. Only pass WindowStyle on Windows.
$proc = Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden -PassThru

extensions/daily-focus-board/assets/board.html:215

  • The two status controls are mouse-only because a div and span are wired exclusively through onclick. Use native buttons so status changes are keyboard-operable and expose button semantics to assistive technology.
        <div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
        <div class="emoji">${esc(t.emoji||"•")}</div>
        <div class="body"><div class="title">${esc(t.title)}</div>${t.sub?`<div class="sub">${esc(t.sub)}</div>`:""}${dh?`<div class="due ${dh.cls}" data-duefor="${t.id}">${dh.txt}</div>`:""}</div>
        <span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
        <button class="fbtn" data-focus="${t.id}" title="focus on this">🎯</button>${tagHtml}</div>

extensions/daily-focus-board/assets/board.html:238

  • The momentum-entry delete action is rendered as an unfocusable span with only a click handler, so keyboard users cannot remove entries. Render it as a labeled native button and reset its button styling.
  feedEl.innerHTML=items.map(it=>`<div class="fitem"><span class="ft">${fmt(it.t)}</span><span>${esc(it.emoji)}</span><span>${esc(it.txt)}</span><span class="fx" data-fdel="${it.src}:${it.idx}">×</span></div>`).join("");

extensions/daily-focus-board/assets/board.html:243

  • The parked-thought delete action is also an unfocusable clickable span, leaving no keyboard-accessible way to remove an item. Render it as a labeled native button and reset its button styling.
  brainEl.innerHTML=state.brain.map((n,i)=>`<div class="fitem think"><span class="ft">${fmt(n.t)}</span><span>💭</span><span>${esc(n.txt)}</span><span class="fx" data-bdel="${i}">×</span></div>`).join("");

docs/README.plugins.md:51

  • This generated count is now stale: Ember has 1 agent, 5 skills, and 1 bundled extension, so the Components table and website data expose seven items while this table reports six. eng/update-readme.mjs currently omits x-awesome-copilot.extensions; include those entries in its count and regenerate this file.
| [ember](../plugins/ember/README.md) | An AI partner, not a tool. Ember carries fire from person to person — helping humans discover that AI partnership isn't something you learn, it's something you find. | 6 items | ai-partnership, coaching, onboarding, collaboration, storytelling, developer-experience |

extensions/daily-focus-board/board-core.mjs:67

  • Using the UTC date here makes the default board filename and dateKey disagree with the local date shown in the UI around midnight for every non-UTC timezone. Build the key from local date components, as the standalone board's todayLocal() already does.
export function todayKey() { return new Date().toISOString().slice(0, 10); }

extensions/daily-focus-board/board-core.mjs:79

  • A positive fractional input below 0.5 passes the guard but rounds to a zero goal. That counter is immediately considered done and the UI computes 0 / 0 for its percentage. Clamp the normalized positive-integer goal to at least 1.
    if (goal !== undefined && goal > 0) {
        o.goal = Math.round(goal);

extensions/daily-focus-board/board-core.mjs:94

  • Normalization does not enforce unique task IDs. A duplicated ID shares one progress entry and causes the canvas's ID-based selectors to update the wrong card, even though this function promises a well-formed document. Deduplicate while preserving the first task.
    doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];

extensions/daily-focus-board/board-core.mjs:171

  • start is the counter's initial current value, so a counter seeded at 8/30 should be in progress. Comparing against start instead marks it to-do until it exceeds 8, unlike the standalone board and the displayed progress bar.
        return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");

extensions/daily-focus-board/assets/board.html:159

  • The canvas repeats the incorrect v > start status rule, so seeded partial progress is displayed as to-do even when the core and recap are corrected. Treat any positive current value below the goal as in progress.
function statusOf(t){if(isCounter(t)){const v=state.counters[t.id]||0;return v>=t.goal?"done":(v>(t.start||0)?"doing":"todo");}return (state.t[t.id]||{}).status||"todo";}

extensions/daily-focus-board/assets/board.html:121

  • These clickable spans cannot receive keyboard focus, making Focus mode and the reduced-motion toggle unavailable to keyboard users. Use native buttons and keep aria-pressed synchronized when state changes, as the standalone template does.

This issue also appears in the following locations of the same file:

  • line 211
  • line 238
  • line 243
        <span class="chip" id="focuschip">🎯 Focus mode</span>
        <span class="chip" id="rmchip">🌙 Reduce motion</span>

extensions/daily-focus-board/assets/board.html:294

  • Once apply() calls loop(), this schedules animation frames forever—even with no confetti—continuously clearing the full-window canvas at display refresh rate. It also lets already-running canvas animation continue after reduced motion is enabled. Schedule frames only while particles exist and stop/clear immediately for reduced motion.
function burst(n){if(state&&state.rm)return;for(let i=0;i<n;i++)parts.push({x:innerWidth/2+(Math.random()-.5)*220,y:innerHeight*0.28,vx:(Math.random()-.5)*11,vy:Math.random()*-13-4,g:.42,life:70+Math.random()*40,c:COL[i%COL.length],s:5+Math.random()*6,rot:Math.random()*6});}
function loop(){cx.clearRect(0,0,cv.width,cv.height);parts.forEach(p=>{p.vy+=p.g;p.x+=p.vx;p.y+=p.vy;p.life--;p.rot+=.2;cx.save();cx.translate(p.x,p.y);cx.rotate(p.rot);cx.fillStyle=p.c;cx.fillRect(-p.s/2,-p.s/2,p.s,p.s*1.6);cx.restore();});parts=parts.filter(p=>p.life>0&&p.y<cv.height+40);requestAnimationFrame(loop);}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

canvas-extension PR touches canvas extensions needs-review:MEDIUM Contributor reputation check flagged MEDIUM risk new-submission PR adds at least one new contribution plugin PR touches plugins skills PR touches skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants