ember: add daily-focus-board skill (EF/neurodivergent-friendly) - #2452
ember: add daily-focus-board skill (EF/neurodivergent-friendly)#2452jennyf19 wants to merge 16 commits into
Conversation
|
🟡 Contributor Reputation Check: MEDIUM risk
Maintainers: please review this contributor before merging. |
🔒 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
aaronpowell
left a comment
There was a problem hiding this comment.
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
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)
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
innerHTMLassignment, 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
spanpattern, 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 is1.2.0.eng/generate-marketplace.mjs:54-59copies the manifest version, so this stale output makes plugin discovery/install metadata inconsistent. Regenerate it or update this entry to1.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 localduevalues 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.1passes the check but rounds to0. 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
todobecause this compares the current value with itsstartvalue. For example,start: 8displays 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
mutatetreats an undefined mutator result as success. Mostop*functions return undefined both after a valid mutation and for an unknown task, wrong task type, or empty input, so actions such asset_statusandset_countcan returnok: truewithout 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
innerHTMLwithout 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
spanelements, 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/spanelements wired only withonclick. 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>
|
@aaronpowell great call — done. I pushed a canvas extension version to this PR ( 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 One honest flag: |
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
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 (17)
extensions/daily-focus-board/board-core.mjs:73
- A positive fractional goal below
0.5passes the check but rounds to0. 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-motionsetting. 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
emojivalue directly intoinnerHTML, 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, butopFocustoggles 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
todobecause its current value equalsstart. 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 dountil 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 whenpartsis 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
emojiis seed/state data but is inserted intoinnerHTMLwithout 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
spanelements, 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.0to1.1.0, while both this manifest and the generated marketplace use1.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>
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
There was a problem hiding this comment.
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
innerHTMLwithout 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; escapeit.emojihere 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/spanelements. 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 anaria-labelsuch 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
spanwith 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 descriptivearia-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
partsis empty, so merely opening the board keeps the browser rendering at display refresh rate indefinitely. Start the RAF fromburstand 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.5passes the guard but rounds to zero. The task then becomes immediately done and the canvas computes0 / 0, producing aNaN%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
normalizepromising a well-formed document. An existing board withday: [null]orbrain: [null]passeslooksLikeBoardand then crashes the canvas when renderingn.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;mutatetreats 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
startis the initial current count, not a baseline to subtract. Withstart: 8, the task has visible progress but this reportstodountil it reaches 9; the standalone board correctly treats any positive count as in progress. Usev > 0here and in the canvas copy ofstatusOf.
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
startvalue astodo, even though it already has progress. This diverges from the standalone board and from the corrected server-side status calculation; classify any positive value asdoing.
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
taskIdenters Focus mode and omitting it clears focus, butopFocustoggles the value. Callingfocustwice 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 torm:false, so users who request reduced motion at the OS level still receive confetti and transitions. This contradicts the documented behavior inskills/daily-focus-board/references/neurodivergent-design.md:30. Incorporate the media query into both styling andburstsuppression.
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-pressedsynchronized 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
donetasks and haveopCarryreject 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>
… + 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
There was a problem hiding this comment.
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/spanelements with no keyboard semantics, so keyboard and assistive-technology users cannot advance a task's status. Use realbuttonelements 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 calculatesv / 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.0to1.1.0, while the manifest and generated marketplace entry use1.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-pressedsynchronized when state changes, as inskills/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>
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
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 (11)
skills/daily-focus-board/assets/board.template.html:423
- This validator allows object-prototype names such as
__proto__andtoString. Because task IDs index plain state objects, those IDs makeensureTaskState()mutate inherited global objects instead of initializing task state;dayalso 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__ortoStringresolve through the prototype of the state maps, anddaycollides 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 astodobecause its current value equalsstart. 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
-WindowStyleis unsupported byStart-Processon Linux/macOS, so this helper fails underpwshon the same non-Windows platforms that the precedingGet-NetTCPConnectionfallback explicitly accommodates. 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/.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
spanelements, 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 maintainaria-pressedfor 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 whenpartsbecomes 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, andday). 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:
… 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
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);}
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.