ember: add daily-focus-board skill (EF/neurodivergent-friendly) - #2452
ember: add daily-focus-board skill (EF/neurodivergent-friendly)#2452jennyf19 wants to merge 22 commits into
Conversation
🔒 PR Risk Scan ResultsScanned 16 changed file(s).
Skipped non-text or missing files
|
🔍 Vally Lint Results✅ All checks passed
Summary
Full linter output |
There was a problem hiding this comment.
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-pressedwhen 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. Usebutton type="button"elements and retain the existingdata-cycbinding.
<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
innerHTMLwithout 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-pressedfromrenderCheckin()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")returnsfalse. 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();
There was a problem hiding this comment.
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
innerHTMLwithout escaping:emojiis raw here, andidis 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
innerHTMLinterpolation of config-derivedemojiandidvalues. 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")returnsfalsewhen 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 infinally.
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. Runnpm run buildand commit the generated documentation.
---
plugins/ember/.github/plugin/plugin.json:4
- The checked-in
.github/plugin/marketplace.jsonstill advertises Ember version 1.0.0, so this 1.1.0 release is not reflected in the generated marketplace metadata. Runnpm run buildand commit the regenerated output.
"version": "1.1.0",
6e1b5fc to
c514d58
Compare
There was a problem hiding this comment.
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.serverbinds 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
requestAnimationFrameloop; 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
dateKeyis 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; reusetodayLocal()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
dateKeyis 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; reusetodayLocal()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 📋");}}
There was a problem hiding this comment.
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>`
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
278c0bd to
62bfb28
Compare
There was a problem hiding this comment.
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 calltodayLocal()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());
- 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
There was a problem hiding this comment.
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
-WindowStyleis unsupported byStart-Processon non-Windows PowerShell, so this script fails on the same platforms for which the precedingGet-NetTCPConnectionfallback was added. Only passWindowStyleon 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
divandspanare wired exclusively throughonclick. 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.mjscurrently omitsx-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
dateKeydisagree 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'stodayLocal()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 / 0for 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
startis the counter's initial current value, so a counter seeded at 8/30 should be in progress. Comparing againststartinstead 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 > startstatus 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-pressedsynchronized 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()callsloop(), 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);}
aaronpowell
left a comment
There was a problem hiding this comment.
I think you've actually hit a use-case we're yet to properly cater for in the repo - a plugin that includes an extension, not just agents and/or skills.
The plugin materialisation process should pull the canvas locally so when you install the ember plugin you get the canvas included, but that I think won't happen right now.
But there's another scenario that runs the risk of confusion, you probably don't want the daily-focus-board as a standalone canvas, it should be part of the ember plugin only, but because of the repo design/requirements in place, it has to ship as its own plugin.
Let me have a ponder on how we can handle this, we might need to rejig part of the repo design, but that'll give you time to resolve the merge conflicts and the placeholder preview image.
Resolve the .codespellrc conflict by retaining both the daily-focus-board checkin key and upstream ACI term. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fd1eae93-cc9f-4777-812c-a2a9872e1c2b
|
@aaronpowell thanks — I merged current On the preview: the generated placeholder was already replaced in I'll defer to your repo-design decision on whether an extension bundled by Ember should also remain independently discoverable. |
There was a problem hiding this comment.
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 (24)
skills/daily-focus-board/assets/board.template.html:423
- Regex-only validation accepts prototype keys such as
__proto__andconstructor;ensureTaskStatethen reads/writes those inherited members throughstate.t[id], polluting page-wide prototypes instead of creating task state. It also accepts thedayfeed sentinel, which misroutes note deletion. Apply the same reserved/inherited-name guard already used by the extension core.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}
extensions/daily-focus-board/assets/board.html:214
- This second status control is also a click-only span, so keyboard users cannot advance task status from the labeled pill. Render it as a semantic button and preserve the pill styling with the button reset.
<span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
skills/daily-focus-board/examples/sample-board.html:431
- The sample repeats the permissive ID validator, so editing its config to use
__proto__,constructor, ordaycauses the same prototype-state corruption or feed deletion misrouting as the template. Keep this runnable copy aligned with the hardened validator.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}
extensions/daily-focus-board/assets/board.html:126
- The Focus-mode exit remains a click-only span, so it cannot be reached or activated from the keyboard. Convert it to a semantic button and reset the button styling to retain the current appearance.
<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:216
- Task-note deletion is exposed as a click-only span, so keyboard and assistive-technology users cannot operate it. Render a labeled button and add button-reset styling for
.nx.
${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:243
- Parked-thought deletion is also rendered as a click-only span, making it unavailable to keyboard users. Convert it to a labeled button and apply the same button-reset styling as the other delete controls.
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/board-core.mjs:67
- The default board key is derived in UTC, although the board is presented as a local daily planner. In negative-offset time zones this rolls over to tomorrow during the local evening, selecting the wrong state file and recap date. Build the key from local date components, as the standalone board does.
export function todayKey() { return new Date().toISOString().slice(0, 10); }
extensions/daily-focus-board/board-core.mjs:79
- A positive fractional goal below 0.5 passes validation but rounds to zero. The UI then treats the counter as already done and calculates a
NaN%progress width. Clamp the normalized integer goal to at least 1 (or reject non-positive rounded values).
o.goal = Math.round(goal);
extensions/daily-focus-board/board-core.mjs:94
- Seed and existing-file normalization does not enforce unique task IDs. Duplicate IDs share one progress entry and DOM selectors, while mutations update only the first task, so the board cannot represent them correctly. Deduplicate IDs while normalizing.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/board-core.mjs:216
- The mutation allows a completed task to be carried, which removes a genuine completion from today's ring and labels it as tomorrow's work. The standalone board only offers carryover for unfinished tasks; enforce the same invariant in the shared backend so chat actions cannot create this inconsistent state.
const e = doc.progress.t[id];
e.carried = typeof value === "boolean" ? value : !e.carried;
extensions/daily-focus-board/board-core.mjs:102
- Unlike task notes,
dayandbrainentries are accepted without checking their shape. An existing board containing a non-object or non-stringtxtpasseslooksLikeBoard, thenesc(n.txt)throws and leaves the canvas blank. Filter these arrays to the entry shape consumed by the UI.
p.day = Array.isArray(p.day) ? p.day : [];
p.brain = Array.isArray(p.brain) ? p.brain : [];
extensions/daily-focus-board/assets/board.html:217
- This renders “not today” even after a task is done, so the normal UI can move a completion out of today's totals. Hide carryover for completed tasks, matching the standalone board, and retain the backend guard for action callers.
<div class="cardfoot"><button class="soft" data-carry="${t.id}">${carried?"↩ bring back to today":"⤳ not today"}</button></div>
extensions/daily-focus-board/assets/board.html:121
- These click handlers are attached to non-focusable spans, so keyboard and assistive-technology users cannot operate the two main mode toggles. Use semantic buttons and keep
aria-pressedsynchronized withstate.focusandstate.rm.
This issue also appears on line 126 of the same file.
<span class="chip" id="focuschip">🎯 Focus mode</span>
<span class="chip" id="rmchip">🌙 Reduce motion</span>
skills/daily-focus-board/SKILL.md:61
- The documented ID contract permits
__proto__,constructor,toString, and the feed sentinelday. The board stores task state in plain objects and routes feed deletion by ID, so these values can mutate inherited objects or delete from the wrong feed. Document the reserved/prototype-name exclusions and enforce them in both runnable HTML copies.
- `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:
extensions/daily-focus-board/assets/board.html:294
- Once the first state load calls
loop, it schedules another animation frame forever, even whenpartsis empty. Leaving this side-panel open therefore keeps the renderer waking at display refresh rate all day. Start the loop fromburstonly when particles exist and stop scheduling when the array becomes empty.
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:265
- Enabling reduced motion does not stop particles already animating on the canvas; the CSS toggle cannot affect canvas drawing, so motion continues until every particle expires. Clear the particle list and canvas immediately when turning the setting on.
document.getElementById("rmchip").onclick=()=>api("api/rm",{value:!state.rm});
plugins/ember/.github/plugin/plugin.json:4
- The PR description says Ember changes from 1.0.0 to 1.1.0 and lists only the new skill, but this manifest bumps to 1.2.0 and bundles an additional standalone extension with a local server and file-backed state. Update the title/description, file list, and verification notes so the reviewed scope and release version match the actual changes.
"version": "1.2.0",
extensions/daily-focus-board/assets/board.html:211
- The status checkbox is a clickable
div, making this primary task action unavailable from the keyboard and exposing no control semantics. Use the semantic button pattern from the standalone template and add the corresponding button reset styling.
This issue also appears in the following locations of the same file:
- line 214
- line 216
<div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
extensions/daily-focus-board/extension.mjs:145
- This action explicitly supports omitting
taskId, but the handler dereferencesctx.inputunconditionally. If the SDK supplies no input object for that valid call, clearing focus throws before reachingopFocus. Use optional chaining as the other extension handlers do.
handler: (ctx) => act(ctx, doc => opFocus(doc, ctx.input.taskId || null)),
extensions/daily-focus-board/assets/board.html:238
- Momentum-feed deletion is a click-only span with no accessible name or keyboard behavior. Use a labeled semantic button and preserve the icon styling with a button reset.
This issue also appears on line 243 of the same file.
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("");
plugins/ember/README.md:23
- Calling this “the same board” is misleading: the canvas variant omits the skill board's arrival check-in, mantra, Eisenhower priorities, live reordering/relabeling, overload nudge, and in-canvas recap controls. Describe it as a file-backed variant with its actual feature subset so users do not expect the preceding row's UI.
| Extension | [Daily Focus Board (canvas)](../../extensions/daily-focus-board/) | The same board as a canvas, backed by a JSON file Ember reads and writes — so you can mark done, add tasks, log progress, and recap your day from chat. Needs the Copilot app; the skill is the zero-install fallback |
extensions/daily-focus-board/board-core.mjs:222
- The action contract says a provided task enters Focus mode and omission clears it, but the shared
opFocustoggles off when that task is already focused. Repeating an idempotent chat command such as “focus on X” therefore unexpectedly clears focus. Separate the action's set semantics from the canvas toggle behavior, or update the action contract.
p.focus = p.focus === id ? null : id;
extensions/daily-focus-board/board-core.mjs:171
- A counter seeded with non-zero
startis incorrectly reported astodountil it exceeds that initial value. Herestartis the initial current count (the standalone board treats any value above zero as in progress), so a board opened at 8/30 should already bedoing.
return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");
extensions/daily-focus-board/assets/board.html:159
- The canvas duplicates the counter-status bug from the core: a seeded counter such as 8/30 is shown as “to do” because its current value equals
start. Classify any positive current count below the goal as in progress so the UI and recaps agree with the standalone board.
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";}
Resolve the .codespellrc conflict by keeping the daily-focus-board checkin key alongside upstream ACI and soruce ignore terms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fd1eae93-cc9f-4777-812c-a2a9872e1c2b
There was a problem hiding this comment.
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 (15)
skills/daily-focus-board/assets/board.template.html:423
- The regex admits IDs such as
__proto__,constructor, andtoString, but task state is stored in ordinary objects. Those IDs resolve inherited members, causing task operations to mutate prototypes/functions or crash;dayalso collides with the momentum-feed delete sentinel. Reject reserved and inherited object-key names, matching the extension core's guard.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}
skills/daily-focus-board/examples/sample-board.html:431
- The example repeats the template's permissive ID validation, so copying it as a starting point retains the inherited-key/sentinel collision bug. Reject
day,brain, and inherited object property names here as well.
function validId(id){return typeof id==="string"&&/^[A-Za-z0-9_-]+$/.test(id);}
extensions/daily-focus-board/board-core.mjs:79
- A positive fractional goal below
0.5passes this condition but rounds to0. It then becomes a counter that is immediately done, and the canvas computes progress by dividing by zero. Clamp the rounded positive goal to at least 1.
o.goal = Math.round(goal);
extensions/daily-focus-board/board-core.mjs:67
todayKey()uses the UTC calendar date, while the canvas clock and the standalone board use local time. Around midnight this opens/seeds the wrong day's file for users outside UTC. Build the key from local date components instead.
export function todayKey() { return new Date().toISOString().slice(0, 10); }
extensions/daily-focus-board/board-core.mjs:94
- Normalization retains duplicate valid task IDs from a seed or existing board file. Those cards then share one progress entry, and DOM selectors can update the first matching card instead of the intended one. Deduplicate IDs here, as the standalone template already does.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/board-core.mjs:102
- These arrays are accepted without validating their entries. An existing board containing
nullin either array passeslooksLikeBoard(), but the canvas later dereferencesn.t/n.txtand aborts rendering. Keep only the timestamped text entries produced by the mutation operations.
p.day = Array.isArray(p.day) ? p.day : [];
p.brain = Array.isArray(p.brain) ? p.brain : [];
extensions/daily-focus-board/board-core.mjs:171
startis the counter's initial current value, not a baseline to subtract. A seeded task at 8/30 is therefore shown as “to do” until it reaches 9, unlike the standalone board and despite already having progress. Classify any positive value below the goal as in progress.
return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");
extensions/daily-focus-board/board-core.mjs:137
- Each atomic replacement creates a new file with the process's default permissions, commonly
0644, so personal task data may be readable by other local users and an existing restrictive file can become broader after a mutation. Create the replacement with owner-only permissions.
await writeFile(tmp, JSON.stringify(obj, null, 2), "utf-8");
extensions/daily-focus-board/assets/board.html:294
- This animation loop schedules another frame forever, even when
partsis empty or reduced motion is enabled. Keeping the canvas at 60 FPS for the entire board session wastes CPU/battery. Start RAF when a burst adds particles and stop when none remain, 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);}
extensions/daily-focus-board/assets/board.html:121
- These controls are click-only
<span>elements, so keyboard users cannot focus or activate them. The same pattern recurs forfocusexit, task status pills, and delete controls later in this file. Use native buttons (with accessible labels/state where needed) for every interactive control.
<span class="chip" id="focuschip">🎯 Focus mode</span>
<span class="chip" id="rmchip">🌙 Reduce motion</span>
skills/daily-focus-board/scripts/serve-board.ps1:15
-WindowStyleis unsupported byStart-Processon PowerShell Core for Linux/macOS, so this helper fails on those platforms despite explicitly handling another platform-specific cmdlet above. AddWindowStyleonly on Windows.
$proc = Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden -PassThru
plugins/ember/README.md:23
- Calling this “the same board” is inaccurate: the canvas omits the skill board's arrival check-in/mantra, live add/reorder/relabel controls, priorities, overload nudge, and end-of-day UI. Describe it as a reduced file-backed variant, or implement feature parity, so users know which experience they are installing.
| Extension | [Daily Focus Board (canvas)](../../extensions/daily-focus-board/) | The same board as a canvas, backed by a JSON file Ember reads and writes — so you can mark done, add tasks, log progress, and recap your day from chat. Needs the Copilot app; the skill is the zero-install fallback |
plugins/ember/.github/plugin/plugin.json:31
- The PR description says this is a self-contained, localStorage-only skill, lists no extension files, and states the Ember bump is 1.1.0. These lines also bundle a separate SDK/server/file-backed extension and the manifest is now 1.2.0. Update the PR scope/files/version and extension verification details, or split the extension into a separate PR.
"x-awesome-copilot": {
"extensions": [
"./extensions/daily-focus-board/"
]
extensions/daily-focus-board/extension.mjs:145
- The action contract says a supplied
taskIdenters Focus mode and omission clears it, butopFocustoggles when the same ID is already focused. Repeating an idempotent “focus task X” action therefore unexpectedly exits Focus mode. Make this action set the requested state explicitly; the canvas can keep its separate toggle behavior.
handler: (ctx) => act(ctx, doc => opFocus(doc, ctx.input.taskId || null)),
skills/daily-focus-board/SKILL.md:61
- This schema currently tells the agent that every regex-matching ID is valid, but the board must reject
day/brainsentinels and inherited object keys such asconstructor,toString, and__proto__. Document those exclusions so generated configurations are not silently dropped or routed incorrectly.
- `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:
There was a problem hiding this comment.
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)
skills/daily-focus-board/scripts/serve-board.ps1:15
- The script anticipates non-Windows PowerShell above, but
Start-Process -WindowStyleis unsupported on non-Windows PowerShell, so serving fails on macOS/Linux before Python starts. Add this option only 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:126
- The focus-mode exit is click-only because this
<span>has no keyboard semantics. Make it a native button so focus mode can be exited without a pointer.
<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:243
- Parked-thought deletion is exposed as a click-only
<span>, making it unavailable from the keyboard. Use a native button with an accessible 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:214
- The second status control is also a click-only
<span>, so it is skipped by keyboard navigation and exposed as plain text. Render it as a native button to preserve the visible label while providing interaction semantics.
<span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
extensions/daily-focus-board/assets/board.html:238
- Momentum entries expose deletion only through a clickable
<span>. In particular, day-level notes have no alternate delete control, so keyboard users cannot remove them. Use a native button and label the action.
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:216
- The note delete affordance is a click-only
<span>, so keyboard users cannot remove progress notes. Render it as a button with an accessible label.
${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/board-core.mjs:67
todayKey()uses the UTC date, so users outside UTC can open the board near local midnight and get yesterday's or tomorrow's state file/date. The standalone board already uses local calendar components; use the same approach here so “daily” state follows the user's day.
export function todayKey() { return new Date().toISOString().slice(0, 10); }
extensions/daily-focus-board/board-core.mjs:80
- A positive fractional goal below
0.5passes this condition but rounds to0. That creates a counter which is immediately “done” and whose UI percentage divides by zero. Round first, then only retain the counter fields when the rounded goal is still positive.
const goal = num(t.goal);
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/board-core.mjs:94
- Seeded or manually edited state can contain duplicate valid task IDs. Both cards then share one progress entry, and selector-based rendering only updates the first matching card. Deduplicate IDs while normalizing so the state invariant matches
opAddTask's uniqueness check.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/assets/board.html:121
- These clickable
<span>elements are not focusable or operable from the keyboard, so keyboard users cannot toggle two primary board modes. Use native buttons; the existing click handlers and.chipstyling will continue to work.
This issue also appears in the following locations of the same file:
- line 126
- line 214
- line 216
<span class="chip" id="focuschip">🎯 Focus mode</span>
<span class="chip" id="rmchip">🌙 Reduce motion</span>
extensions/daily-focus-board/assets/board.html:217
- Completed tasks still render “not today,” unlike the standalone board, which hides carryover once
st === "done". Clicking it removes a completed task from today's totals and recap and relabels it as tomorrow while retaining its done state. Hide carryover for completed tasks.
This issue also appears in the following locations of the same file:
- line 238
- line 243
<div class="cardfoot"><button class="soft" data-carry="${t.id}">${carried?"↩ bring back to today":"⤳ not today"}</button></div>
extensions/daily-focus-board/assets/board.html:294
- The animation loop reschedules itself forever, even when
partsis empty, so an idle board performs canvas work every frame for its entire lifetime. Start the loop only when a burst adds particles and stop scheduling once they are exhausted, as the standalone template already 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 version changes to
1.1.0and describes only the skill, while this manifest publishes1.2.0and also bundles a new extension. Update the PR description/release intent so reviewers and release consumers can tell which version and scope are intentional.
"version": "1.2.0",
extensions/daily-focus-board/board-core.mjs:216
- The action API can carry an already completed task. That removes it from today's totals and reports it as “tomorrow” while its status remains
done, producing contradictory state even if the UI hides the carry control. Reject transitions that carry completed tasks.
const e = doc.progress.t[id];
e.carried = typeof value === "boolean" ? value : !e.carried;
extensions/daily-focus-board/assets/board.html:211
- This is a primary status control but is rendered as a
<div>with only anonclickhandler, making it unavailable to keyboard and assistive-technology users. Use a button with an explicit action label.
<div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
…ze()
A state file with a non-string txt (e.g. {"txt":1}) previously loaded, then esc(n.txt) in the canvas threw (numbers have no .replace) and the whole board failed to render. Filter day/brain entries to require a string txt, mirroring the existing task-notes filter. Verified with a headless regression test (45/45).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fd1eae93-cc9f-4777-812c-a2a9872e1c2b
There was a problem hiding this comment.
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 (14)
skills/daily-focus-board/scripts/serve-board.ps1:15
- If
pythonis missing,Start-Processemits a non-terminating error and$procremains null; the script then skips theHasExitedbranch, prints a blank PID, and opens a URL with no server. Make startup failure terminating and report it before continuing.
$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:126
- The “show all” control is a click-only span and cannot be reached or activated from the keyboard. Replace it with a native button (including the necessary reset styling).
<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:238
- The feed delete action is also a click-only, unlabeled span. Render it as a native button with an accessible delete label so momentum entries can be managed without a pointer.
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 repeats the click-only span pattern, leaving keyboard and screen-reader users unable to remove entries. Use a native button with an accessible delete 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/board-core.mjs:79
- A positive fractional goal such as
0.4passes this condition but rounds to zero. The action schema currently permits that input, after which the UI divides byt.goaland renders aNaN%progress bar while considering the counter complete. Normalize every accepted counter goal to at least 1 (and ideally declare an integer minimum in the schema).
o.goal = Math.round(goal);
extensions/daily-focus-board/board-core.mjs:94
- Duplicate valid IDs from an initial
seedare retained, even though progress is keyed solely by ID. This renders duplicate cards sharing one progress record, and selectors/actions for the second card target the first. Reject or deterministically deduplicate IDs during normalization, asopAddTaskalready does for live additions.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/board-core.mjs:171
- This treats the configured
startvalue as a baseline rather than progress, so a counter seeded at 8/30 is labeled “to do.” The standalone board labels any positive counter “in progress” (board.template.html:306), making the canvas inconsistent with the companion board and its displayed progress. Usev > 0here as well.
return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");
extensions/daily-focus-board/board-core.mjs:222
focusis documented as a setter—passing a task ID enters focus and omitting it clears focus—but this operation toggles off when the same ID is sent twice. Repeated agent requests therefore unexpectedly clear focus. Make this operation assign the requested ID; if the canvas still needs toggle behavior, have that caller explicitly sendnullwhen the selected card is clicked again.
p.focus = p.focus === id ? null : id;
extensions/daily-focus-board/assets/board.html:288
- The poll is skipped whenever an input remains focused, not only while the user is typing. Focus can remain on an input after the user moves back to chat, so agent-side edits may stop appearing indefinitely. Check that the canvas document itself still has focus before suppressing the poll.
setInterval(()=>{const ae=document.activeElement;if(ae&&(ae.tagName==="INPUT"||ae.tagName==="TEXTAREA"))return;pull();},4000);
extensions/daily-focus-board/assets/board.html:294
loop()schedules itself forever, even when there are no confetti particles, causing a continuous ~60 fps canvas repaint while the board is idle or reduced motion is enabled. Schedule animation only whenburst()adds particles and stop once the particle list is empty, matching the standalone board’s on-demand loop.
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);}
extensions/daily-focus-board/assets/board.html:121
- These state-changing controls are non-focusable spans with click-only handlers, so keyboard users cannot enter Focus mode or toggle reduced motion. Use native buttons and keep their
aria-pressedstate synchronized with the board state.
This issue also appears on line 126 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:214
- Both task status controls are rendered as a
div/spanwith click handlers. Consequently, the board’s primary interaction—advancing task status—is unavailable to keyboard users. Render these as native buttons and expose a task-specific accessible name.
<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>
extensions/daily-focus-board/assets/board.html:216
- The progress-note delete action is a click-only span with no accessible name, so it is unavailable to keyboard and screen-reader users. Use a native button with an
aria-labelidentifying the delete action.
This issue also appears in the following locations of the same file:
- line 238
- line 243
${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>`:""}
plugins/ember/.github/plugin/plugin.json:4
- The PR description says the Ember plugin moves from
1.0.0to1.1.0, but the manifest and generated marketplace entry use1.2.0. Align the intended release version in either the implementation or the PR description before publishing.
"version": "1.2.0",
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fd1eae93-cc9f-4777-812c-a2a9872e1c2b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.
Suppressed comments (16)
extensions/daily-focus-board/assets/board.html:126
- The focus-mode exit is a mouse-only
span, so it is skipped by keyboard navigation and has no control semantics for assistive technology. Make it a native button and update the nearby button-reset styling so it retains the intended appearance.
<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:211
- This is the status-cycle control, but a clickable
divis not keyboard-focusable or announced as a button. Render it as a native button with an accessible label; adjust the.boxbutton styling to preserve the current appearance.
<div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
extensions/daily-focus-board/assets/board.html:214
- The status pill also handles status changes but is emitted as a clickable
span, making this action unavailable from the keyboard. Use a native button and preserve the pill styling in the button reset.
<span class="pill ${pillCls}" data-cyc="${t.id}">${pillTxt}</span>
extensions/daily-focus-board/assets/board.html:216
- Each note's delete action is a clickable
span, so keyboard users cannot remove notes and screen readers do not identify it as a control. Render a labeled native button and reset its visual button styles.
${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:238
- Momentum-item deletion is implemented with a clickable
span, which is neither keyboard-operable nor exposed as a button. Use a labeled native button and retain the existing icon 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
- Parked-thought deletion is also a mouse-only
span; keyboard and assistive-technology users cannot invoke it. Convert it to a labeled native button and apply the shared icon-button reset.
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/board-core.mjs:67
todayKey()uses UTC, while the board clock and the standalone skill use the user's local date. In time zones offset from UTC this can create tomorrow's/yesterday's state file and label the recap with the wrong day. Build the key from local date components instead.
export function todayKey() { return new Date().toISOString().slice(0, 10); }
extensions/daily-focus-board/board-core.mjs:79
- A positive fractional goal below 0.5 passes the guard but rounds to
0. That creates a counter which is immediately considered done (value >= goal) and later divides by zero in the UI. Keep normalized goals at least 1.
o.goal = Math.round(goal);
extensions/daily-focus-board/board-core.mjs:94
- Seeded/existing state can contain duplicate task IDs because normalization validates each ID but never enforces uniqueness. Duplicate cards then share one progress entry, and selector-based UI actions can update the wrong rendered card. Deduplicate IDs during normalization, as
opAddTaskalready does for live additions.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/board-core.mjs:222
- The public
focusaction says supplyingtaskIdenters focus and omitting it clears focus, but this operation toggles the task off when it is already focused. Repeating an idempotent “focus task X” agent action therefore unexpectedly exits focus mode. Assign the requested ID directly; the UI already has explicit clear-focus calls.
p.focus = p.focus === id ? null : id;
extensions/daily-focus-board/assets/board.html:121
- These primary controls are clickable
spanelements, so keyboard and switch-device users cannot focus or activate Focus mode or Reduce motion. Use native buttons (and keep their pressed state synchronized) rather than mouse-only spans.
This issue also appears in the following locations of the same file:
- line 126
- line 211
- line 214
- line 216
- line 238
- ...and 1 more
<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 the first state load calls
loop(), this function schedules another animation frame forever, even whenpartsis empty. A focus board may remain open all day, so this continuously wakes the renderer and wastes CPU/battery. Schedule frames only while particles exist, as the standalone template already 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:31
- This adds and bundles a file-backed canvas extension, but the PR title, What/Files sections, and stated
1.0.0 → 1.1.0bump describe only the localStorage skill; the manifest actually publishes1.2.0. Update the PR description to cover the extension's local server, file writes, API/security model, generated marketplace entry, and intended version (or split that scope) so reviewers and release notes match what ships.
"x-awesome-copilot": {
"extensions": [
"./extensions/daily-focus-board/"
]
extensions/daily-focus-board/assets/board.html:170
- Reduced motion is driven only by the stored
state.rmflag, which defaults tofalse; this canvas never checksprefers-reduced-motion, despite the design documentation stating that the board honors the OS preference by default. It also leaves already-running canvas particles active when the flag turns on. Initialize frommatchMediaand clear/cancel active particles when reduced motion becomes effective.
document.body.classList.toggle("rm",!!state.rm);
document.getElementById("rmchip").classList.toggle("on",!!state.rm);
extensions/daily-focus-board/board-core.mjs:103
- A syntactically valid focus ID is retained even when normalization removed or no longer contains that task. The canvas then enters focus mode with no focused card, dimming every task and showing an empty focus banner. Validate the focus ID against
doc.tasksas well.
p.focus = validId(p.focus) ? p.focus : null;
extensions/daily-focus-board/assets/board.html:217
- The extension always shows “not today,” including on completed tasks, whereas the standalone board hides carryover once a task is done. Carrying a completed task removes it from today's completion total and reports it as tomorrow's work. Hide this control for
donetasks and reject the same transition inopCarryso chat actions cannot create that inconsistent state either.
<div class="cardfoot"><button class="soft" data-carry="${t.id}">${carried?"↩ bring back to today":"⤳ not today"}</button></div>
An array is typeof "object", so progress:[] (or a top-level array state file) was kept as the container; JSON.stringify then drops the non-index props (counters, t, day...), the API returns progress:[] and the canvas crashes reading state.counters. Guard both with !Array.isArray, matching looksLikeBoard. Headless regression tests added (48/48). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fd1eae93-cc9f-4777-812c-a2a9872e1c2b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (10)
extensions/daily-focus-board/board-core.mjs:94
- Normalization currently accepts duplicate task IDs. Both rendered cards then share one progress entry and selector key, so actions can update the wrong visual card; deduplicate IDs while normalizing the seed/file.
doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : [];
extensions/daily-focus-board/assets/board.html:211
- The status checkbox is a click-only
div, which makes the board's core status transition unavailable from the keyboard. Use a native button for this control.
<div class="box ${st}" data-cyc="${t.id}">${st==="done"?"✓":(st==="doing"?"…":"")}</div>
extensions/daily-focus-board/board-core.mjs:79
- A positive fractional goal below 0.5 passes this check but rounds to zero. That makes the counter immediately "done" and causes the UI to calculate
0 / 0; clamp the normalized goal to at least one (or reject non-integers at the API boundary).
o.goal = Math.round(goal);
extensions/daily-focus-board/board-core.mjs:171
- A counter seeded with nonzero progress is reported as
todountil it exceeds its initial value. For example,start: 8displays 8/30 while the status remains "to do"; status should reflect any positive progress.
return v >= t.goal ? "done" : (v > (t.start || 0) ? "doing" : "todo");
skills/daily-focus-board/scripts/serve-board.ps1:15
- The script explicitly tolerates non-Windows platforms above, but
Start-Process -WindowStyleis Windows-only and makes thispwshhelper fail on macOS/Linux. AddWindowStyleonly when running 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/board-core.mjs:67
todayKey()derives the board day from UTC, so users near midnight can get tomorrow's or yesterday's state filename and recap date. Build the key from local date parts, as the standalone board already does.
This issue also appears in the following locations of the same file:
- line 79
- line 94
- line 171
export function todayKey() { return new Date().toISOString().slice(0, 10); }
extensions/daily-focus-board/assets/board.html:121
- These primary toggles are click-only spans, so keyboard and assistive-technology users cannot operate them. Use native buttons for the interactive controls.
This issue also appears on line 211 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:294
- Once the board loads, this loop schedules a new animation frame forever, even when there are no confetti particles (and even in reduced-motion mode), keeping the canvas active at display refresh rate. Stop scheduling when idle and restart from
burst.
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/extension.mjs:145
- The action contract says a supplied
taskIdenters Focus mode, butopFocustoggles the same ID off when it is already focused. Make this agent-facing action idempotently set/clear focus; the UI endpoint can retain toggle behavior.
handler: (ctx) => act(ctx, doc => opFocus(doc, ctx.input.taskId || null)),
plugins/ember/.github/plugin/plugin.json:4
- The PR description states an Ember version bump to 1.1.0 and describes only the skill, while this changes the version to 1.2.0 and also bundles a substantial new canvas extension. Align the PR description/release scope with the code, or use the documented version if the extension is not intended for this PR.
"version": "1.2.0",
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 theemberplugin.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
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; version1.0.0 -> 1.1.0.plugins/ember/README.md— adds a Components row.Notes
Opening as a draft for final review.