Skip to content

Add CatPilot Canvas extension#2359

Open
tanure wants to merge 10 commits into
github:mainfrom
tanure:add-catpilot-canvas-extension
Open

Add CatPilot Canvas extension#2359
tanure wants to merge 10 commits into
github:mainfrom
tanure:add-catpilot-canvas-extension

Conversation

@tanure

@tanure tanure commented Jul 20, 2026

Copy link
Copy Markdown

Pull Request Checklist

  • I have read and followed the CONTRIBUTING.md guidelines.
  • I have read and followed the Guidance for submissions involving paid services.
  • My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
  • The file follows the required naming convention.
  • The content is clearly structured and follows the example format.
  • I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
  • I have run npm start and verified that README.md is up to date.
  • I am targeting the main branch for this pull request.

Description

Adds the CatPilot Canvas extension at extensions/catpilot-canvas/ — a visual command center for CatPilot that lets you manage everything CatPilot tracks without leaving the Copilot app. It reads and writes the same config-driven storage as the CatPilot CLI, agent, skills, and MCP server, so every surface stays in sync.


Type of Contribution

  • New instruction file.
  • New prompt file.
  • New agent file.
  • New plugin.
  • New skill file.
  • New agentic workflow.
  • New canvas extension.
  • Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
  • Other (please specify):

Additional Notes

Add CatPilot Canvas extension

This adds a canvas extension at extensions/catpilot-canvas/.

What you get

  • Dashboard — summary cards, open-tasks-by-priority donut, activity chart, focus list, recent-activity feed, a Today / Week / Month filter, and a 🍅 Focus analytics section.
  • Taskslist (table), board (kanban) and 📅 calendar views. The board has Overdue · To do · Blocked · Done columns with drag-and-drop and a Status selector on create/edit; the calendar offers Month / Week layouts with per-day priority-coloured chips, a field chooser, today highlight and a shared detail popup. Inline complete/edit everywhere.
  • Pomodoro timer — configurable focus / short-break / long-break durations, a global persistent mini-timer dock that keeps ticking on every view with colour-coded session badges and Pause / Resume / Stop controls, end-of-session notifications, and classic long-break-after-4-focus break suggestions.
  • Journal, Milestones, Memos, Learning, Growth, Projects — browse, add, detail views.
  • Reports — generate GitHub Copilot executive reports per period (markdown/HTML).
  • Timeline — 7/14/30-day activity rail with agent actions.
  • Settings — interactive config wizard with a non-destructive migration preview gated behind explicit approval.
  • Help — in-canvas capabilities guide.
  • Markdown editor on every text field (toolbar, live preview, Generate with Copilot) + a global Ask Copilot button.
  • Light / dark themes.

Checklist

  • extensions/catpilot-canvas/.github/plugin/plugin.json with logo: "assets/preview.png" and extensions: ".", name matches the folder, no x-awesome-copilot, no canvas.json.
  • assets/preview.png primary screenshot.
  • npm run plugin:validate passes (✅ extension catpilot-canvas is valid).

Updated to sync with CatPilot v0.7.0: adds the Pomodoro timer (global mini-timer dock, pause/resume/stop, break suggestions), the Blocked task status + board columns, and the Tasks calendar view; refreshes the description/keywords and regenerates marketplace.json.

Updated again for CatPilot v0.8.0: redesigned the Tasks kanban board — colour-coded, scrollable columns with per-column quick-add (+ New prefills the column’s status), plus cleaner high-contrast cards with priority-coloured accent bars and clear header / title / footer separation.


By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.


Update — v0.8.1 (2026-07-23)

Synced the extension to CatPilot v0.8.1:

  • Board cards now clearly show the due date — a bold, high-contrast 📅 chip with the date and a relative label (Today / Tomorrow / in 3d / 2d overdue).
  • Backlog column for open tasks with no due date, with drag-to-clear / drag-to-schedule handling.
  • Emoji empty-state pattern for every board column (📭 / 🎉 / 🧯 / 🌱).

Revalidated with
pm run plugin:validate (✅ all 70 plugins / 20 extensions valid).

A visual command center canvas for CatPilot: tasks (list + board), journal,
milestones, memos, learning, growth, projects, Copilot-generated reports, an
activity timeline, interactive settings/migration, a markdown editor on every
field, and light/dark themes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cc565b3b-2964-445e-bb79-88466b6a5494
Copilot AI review requested due to automatic review settings July 20, 2026 13:36
@tanure
tanure requested a review from aaronpowell as a code owner July 20, 2026 13:36
@github-actions github-actions Bot added canvas-extension PR touches canvas extensions new-submission PR adds at least one new contribution labels Jul 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds CatPilot as a GitHub Copilot Canvas extension with a local storage engine and interactive SPA.

Changes:

  • Adds dashboards, task management, reports, timelines, and settings.
  • Integrates CatPilot storage and Copilot agent actions.
  • Adds extension metadata, documentation, and visual assets.

Reviewed changes

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

Show a summary per file
File Description
extensions/catpilot-canvas/extension.mjs Canvas server, APIs, and actions
extensions/catpilot-canvas/catpilot-store.mjs CatPilot storage engine
extensions/catpilot-canvas/ui/app.js Canvas application behavior
extensions/catpilot-canvas/ui/index.html Application shell
extensions/catpilot-canvas/ui/styles.css Layout and themes
extensions/catpilot-canvas/ui/hero.svg Vector CatPilot artwork
extensions/catpilot-canvas/ui/hero.png UI CatPilot artwork
extensions/catpilot-canvas/assets/preview.png Marketplace preview
extensions/catpilot-canvas/README.md Extension documentation
extensions/catpilot-canvas/package.json Package metadata
extensions/catpilot-canvas/package-lock.json Locked dependencies
extensions/catpilot-canvas/.github/plugin/plugin.json Plugin manifest
Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file

"markdown-editor"
],
"logo": "assets/preview.png",
"extensions": "."
Comment on lines +143 to +147
if (p.startsWith("/api/")) {
try {
const data = await handleApi(req, url);
if (data === null) return sendJson(res, 404, { error: "Not found" });
return sendJson(res, 200, data);
Comment on lines +385 to +386
const p = path.join(dir, filename);
if (!path.resolve(p).startsWith(path.resolve(dir))) throw new Error("Invalid memo path");
Comment on lines +462 to +463
const p = path.join(dir, filename);
if (!path.resolve(p).startsWith(path.resolve(dir))) throw new Error("Invalid note path");
Comment thread extensions/catpilot-canvas/ui/app.js Outdated
try {
const { report } = await api(`/api/reports/${encodeURIComponent(filename)}`);
let body;
if (report.format === "html") { body = el("iframe", { class: "report-iframe" }); body.srcdoc = report.content; }
Comment on lines +352 to +355
@media (max-width: 720px) {
.app { grid-template-columns: 1fr; }
.sidebar { display: none; }
}
table.tbl tbody tr { transition: background var(--sq); }
table.tbl tbody tr:hover { background: var(--panel-hover); }
.row-actions { display: flex; gap: 4px; opacity: 0; transition: opacity var(--sq); justify-content: flex-end; }
table.tbl tr:hover .row-actions { opacity: 1; }
if (plan.needsMigration && (migration === "move" || migration === "copy")) {
for (const item of plan.items) {
if (!item.willMove) continue;
try { migrated += moveOrCopyPath(item.from, item.to, item.isDir, migration); } catch { /* best-effort per item */ }
// mutating the active config.
function resolveDomainPaths(baseDir, root, partitioning) {
const storageRoot = path.resolve(baseDir, root);
const partition = getPartitionFolder(partitioning);
}

function resolveFilePath(type, config) {
const partition = getPartitionFolder(config.storage.partitioning);
Address the 18 inline review findings on PR github#2359:

Security & correctness
- Block cross-origin/non-loopback API requests (Host, Origin and
  Sec-Fetch-Site guards) in extension.mjs.
- Harden memo/note reads against path traversal via basename + parent-dir
  checks; return sanitized filenames.
- Escape markdown table cells (pipes/newlines) on write and decode on read
  so task/milestone fields round-trip safely.
- Migration never overwrites: moveOrCopyPath skips existing destinations and
  reports skipped items; applyConfigChange validates the whole plan and fails
  before persisting on any error.
- Config changes now persist to the resolved config path.

Cross-partition aggregation
- Dashboard "last 3 days", the activity timeline and generated-report journal
  now aggregate across every storage partition (day/week/month), skipping
  dotfolders (.trash/.git/.obsidian). Editable views and counts stay
  scoped to the current partition to avoid ID collisions.
- Add last-7 / last-30 report periods (enum + range handling).

Accessibility & UI
- Nav items are real <button> elements with aria-current and aria-hidden
  icons; focus-visible outline and button reset in CSS.
- Fix double-escaping of markdown list items.
- Sandbox the report preview iframe and give it a title.
- Config modal: require a preview before apply, surface skipped items and
  destination conflicts.
- Responsive top nav (horizontal scroll) instead of hiding the sidebar.
- Table row actions reveal on focus-within for keyboard users.
@tanure

tanure commented Jul 20, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough automated review! I pushed a commit that addresses all of the inline findings. Summary:

Security & correctness

  • API server now rejects cross-origin / non-loopback requests (Host, Origin and Sec-Fetch-Site guards).
  • Memo/note reads are hardened against path traversal (basename + parent-dir checks) and return sanitized filenames.
  • Markdown table cells are now escaped on write and decoded on read, so task/milestone titles containing | or newlines round-trip correctly.
  • Migration never overwrites: existing destinations are skipped and reported, and the full plan is validated before anything is persisted. Config changes are now written back to the resolved config path.

Cross-partition aggregation

  • The dashboard "last 3 days", the activity timeline and the generated-report journal now aggregate across every storage partition (day/week/month), skipping dotfolders (.trash/.git/.obsidian). Editable views and per-domain counts intentionally stay scoped to the current partition to avoid ID collisions and edit regressions.
  • Added last-7 / last-30 report periods.

Accessibility & UI

  • Nav items are now real <button> elements with aria-current and aria-hidden icons, plus a focus-visible outline.
  • Fixed double-escaping of markdown list items.
  • The report preview iframe is now sandboxed and titled.
  • The config modal requires a preview before applying and surfaces skipped items / destination conflicts.
  • The sidebar collapses into a scrollable horizontal nav on narrow screens instead of disappearing, and table row actions reveal on :focus-within for keyboard users.

I re-ran npm run plugin:validate and npm run build (marketplace.json regenerated) and validated the running canvas end-to-end: loopback requests return 200 while spoofed Host, cross-site Sec-Fetch-Site and non-loopback Origin all return 403.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (2)

extensions/catpilot-canvas/catpilot-store.mjs:84

  • Onboarding always writes the global config even when CATPILOT_CONFIG or CATPILOT_ROOT selects another canonical path. If that selected file does not yet exist, /api/status remains unconfigured immediately after this write and startApp() fails. Write to the path returned by resolveConfigPath() so setup creates the config that subsequent loads will actually use.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/ui/app.js:1051

  • Editing a field while the preview request is in flight calls invalidate(), but the older response later restores previewedBody and enables applying the superseded values. Re-check that the form still matches the submitted body before accepting the response.
            try { const plan = await api("/api/config/plan", { method: "POST", body: b }); previewedBody = b; renderPlan(plan); }

Comment on lines +59 to +60
store.writeConfig(await readBody(req));
return { ok: true, ...store.configStatus() };

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. First-run onboarding has no previously-configured source to migrate from, so the Move/Copy choices were genuine no-ops. I removed the migration select from the onboarding form and writeConfig now always records migration mode 'adopt'. Move/Copy migration remains available from Settings, which uses the plan -> preview -> approve -> apply flow.

Comment on lines +944 to +949
function resolveDomainPaths(baseDir, root, partitioning) {
const storageRoot = path.resolve(baseDir, root);
const partition = getPartitionFolder(partitioning);
const out = {};
for (const [type, fileName] of Object.entries(DEFAULT_FILES)) {
out[type] = { path: path.join(storageRoot, partition, fileName), isDir: !/\.[a-z]+$/i.test(fileName) };

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. resolveDomainPaths now takes an optional files argument and merges DEFAULT_FILES with config.storage.files. All three callers (getConfig and both planConfigChange resolves) plus the root-migration loop now pass config.storage.files, so a customized filename map is honored everywhere.

Comment on lines +416 to +418
const filename = `${todayISO()}_${slugify(params.title)}.md`;
const p = path.join(dir, filename);
fs.writeFileSync(p, `# ${params.title}\n\n${params.content || "Add your memo content here."}`, "utf8");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. createMemo now routes the target path through a new uniquePath() helper that appends -2, -3... before the extension when a file already exists, and returns the actual basename. Two memos with the same title on the same day no longer overwrite each other.

Comment on lines +492 to +497
const filename = `${todayISO()}_${slugify(params.title)}.md`;
const p = path.join(dir, filename);
if (!path.resolve(p).startsWith(path.resolve(dir))) throw new Error("Invalid note path");
const frontmatter = { catpilot: DOMAIN_TAG[domain], title: params.title, date: todayISO(), ...(params.frontmatter || {}) };
const body = params.body ? `\n${params.body}\n` : "\n";
fs.writeFileSync(p, `${toFrontmatter(frontmatter)}\n\n# ${params.title}\n${body}`, "utf8");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. addNote (learning/growth/projects) uses the same uniquePath() helper and returns the real basename, so a same-title note on the same day is preserved instead of overwritten.

Comment on lines +815 to +818
const filename = `report-${nowStamp()}${slug ? "-" + slug : ""}.${ext}`;
let content = body || "";
if (ext === "md" && title && !/^#\s/m.test(content)) content = `# ${title}\n\n${content}`;
fs.writeFileSync(path.join(dir, filename), content, "utf8");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. saveReport (and therefore generateReport, which delegates to it) now allocates a unique filename via uniquePath() and returns the final basename + date, so two reports generated in the same minute no longer collide.

Comment thread extensions/catpilot-canvas/ui/app.js Outdated
Comment on lines +243 to +247
try {
await VIEWS[view](content);
} catch (err) {
content.innerHTML = "";
content.append(errorBox(err));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. go() now increments a navToken per navigation and builds the view into detached staging nodes (content + view-actions), committing to the DOM only when the token is still current. A slow view that resolves after the user has navigated away is discarded instead of painting into the newer view.

Comment thread extensions/catpilot-canvas/ui/app.js Outdated
};

// ---------------------------------------------------------------- reports
const PERIODS = [["this-week", "This week"], ["last-week", "Last week"], ["this-month", "This month"], ["last-month", "Last month"], ["last-7", "Last 7 days"], ["last-30", "Last 30 days"]];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. Added an 'All time' entry to the PERIODS array. The backend periodRange and the report action's period enum already accepted 'all'; the UI now exposes it.

Comment thread extensions/catpilot-canvas/ui/app.js Outdated
Comment on lines +55 to +61
const backdrop = el("div", { class: "modal-backdrop", onclick: (e) => { if (e.target === backdrop) closeModal(); } });
const modal = el("div", { class: "modal" });
if (width) modal.style.width = `min(${width}px, 100%)`;
modal.append(
el("div", { class: "modal-head" },
el("h3", { text: title }),
el("button", { class: "icon-btn", text: "✕", onclick: closeModal })),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. openModal now sets role=dialog, aria-modal=true and aria-labelledby (associated with the title), traps Tab within the dialog, moves focus to the first focusable element on open, and restores focus to the previously-focused element on close.

Comment thread extensions/catpilot-canvas/ui/app.js Outdated
function taskRow(t) {
const done = t.status.toLowerCase() === "done";
const tr = el("tr", { class: done ? "done-row" : "", dataset: { taskTitle: t.title } });
const check = el("input", { type: "checkbox", style: "width:auto", ...(done ? { checked: "checked" } : {}) });

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. The task row checkbox now gets an aria-label (Complete <title> / Reopen <title>) so it has an accessible name.

Comment on lines +195 to +202
async function ensureServer() {
if (sharedServer) return sharedServer;
const server = createServer((req, res) => {
requestListener(req, res).catch(() => { try { res.writeHead(500); res.end(); } catch { /* */ } });
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
sharedServer = { server, url: `http://127.0.0.1:${port}/` };

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in cbc8f85. ensureServer() now caches its in-flight init promise; concurrent open() calls await the same promise instead of each binding a new loopback listener. The cached promise is cleared on failure and on server close so a later reopen re-inits cleanly.

- catpilot-store: add uniquePath() so creating a memo, note, or report
  never silently overwrites an existing file with the same title/timestamp;
  return the actual (possibly suffixed) basename to callers
- catpilot-store: bound timeline milestone events to <= today so far-future
  target dates don't leak into "recent activity"
- catpilot-store: resolveDomainPaths now honors config.storage.files instead
  of always using DEFAULT_FILES (config, plan preview, and migration loop)
- catpilot-store: writeConfig always adopts on first-run onboarding (no prior
  source to migrate); migration stays available via Settings plan/apply
- extension.mjs: cache ensureServer() init promise so concurrent opens share
  one loopback server instead of racing to bind two
- ui: onboarding form drops the no-op migration select
- ui/app.js: navigation token guard + detached staging so a slow view can't
  paint into a newer view; add "All time" report period; modal gets
  role=dialog/aria-modal/labelledby + focus trap and restore; task checkbox
  gets an accessible name
Copilot AI review requested due to automatic review settings July 20, 2026 14:40
@tanure

tanure commented Jul 20, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough second pass. All 11 findings are addressed in cbc8f85:

Data safety (no silent overwrites)

  • New uniquePath() helper backs memo, note, and report creation — a same-title/timestamp file gets a -2/-3… suffix instead of clobbering the existing one, and the real basename is returned to callers.
  • Timeline milestone events are now bounded to <= today.
  • resolveDomainPaths honors config.storage.files across config view, plan preview, and the root-migration loop.

Concurrency

  • ensureServer() caches its init promise (no double-bind under concurrent opens).
  • Router go() uses a nav-token guard with detached staging so a stale view can't paint over a newer one.

Onboarding / a11y

  • First-run onboarding drops the no-op Move/Copy migration select; writeConfig always adopts (migration still available from Settings via plan→approve→apply).
  • Reports gain an All time period.
  • Modal gets role=dialog + aria-modal + labelled title + focus trap/restore; task checkboxes get accessible names.

Each thread has a specific reply. Validated at runtime: duplicate memo/report creation yields suffixed filenames, all period renders, config plan enumerates all domains.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (3)

extensions/catpilot-canvas/ui/app.js:268

  • The navigation token protects stage, but actionsHost remains shared mutable state. If a slow view (for example Tasks) resolves after a newer navigation starts, its post-await actionsHost.append(...) writes into the newer view's action stage before the stale token is checked, leaving controls from the wrong view in the top bar. Pass actionStage into each view instead of assigning it globally.
        actionsHost = actionStage;

extensions/catpilot-canvas/catpilot-store.mjs:876

  • The selected report period is not used when loading tasks or milestones: both paths resolve only the current partition, and all current tasks feed the KPIs. Consequently “last month” and “last week” reports can show the same current totals and omit records from the requested partition, contradicting the per-period reporting behavior. Load/filter records for since..until, or label these figures explicitly as a current snapshot.
    const tasks = parseTasksTable(readFileOrCreate(resolveFilePath("tasks", config)));
    const milestones = parseMilestones(readFileOrCreate(resolveFilePath("milestones", config)));
    const journal = collectJournal(config);
    const { since, until, label } = periodRange(period);

extensions/catpilot-canvas/catpilot-store.mjs:1123

  • Migration failures are collected only after earlier items have already been renamed. If a later item fails, the config remains on the old root while successfully moved files exist only under the new root, making that data disappear from CatPilot until a successful retry. This contradicts the claimed non-destructive/consistent migration; preflight and roll back moved items, or use a staged transaction before changing either source or config.
                const r = moveOrCopyPath(item.from, item.to, item.isDir, migration);
                moved += r.moved;
                skipped += r.skipped;
            } catch (err) {
                errors.push({ type: item.type, from: item.from, to: item.to, error: String(err && err.message || err) });

Comment thread extensions/catpilot-canvas/extension.mjs
const { prompt, timeout } = await readBody(req);
if (!prompt || !String(prompt).trim()) throw new Error("prompt is required");
if (!sessionRef?.sendAndWait) return { ok: false, error: "No active session." };
const ev = await sessionRef.sendAndWait(String(prompt), Number(timeout) || 120000);
Comment thread extensions/catpilot-canvas/ui/app.js Outdated
Comment on lines +84 to +87
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
config.__configPath = GLOBAL_CONFIG_PATH;
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/ui/app.js
Comment thread extensions/catpilot-canvas/ui/app.js
Comment thread extensions/catpilot-canvas/ui/app.js
<!-- Modal host -->
<div id="modal-root"></div>
<!-- Toasts -->
<div id="toasts" class="toasts"></div>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 20, 2026 14:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (7)

extensions/catpilot-canvas/ui/app.js:542

  • Dropping between Overdue and To do is always a no-op: both branches only set status: "Open", while column membership is derived from dueDate. The card therefore returns to its original column after reload even though both columns advertise a valid drop target. Either disable those drops or update the due date according to clearly defined rescheduling semantics.
                    if (col.key === "done") await api(`/api/tasks/${id}/complete`, { method: "POST" });
                    else await api(`/api/tasks/${id}`, { method: "PUT", body: { status: "Open" } });

extensions/catpilot-canvas/ui/index.html:71

  • Async success and error messages are inserted here without a live-region role, so screen-reader users are not notified when saves, deletes, generation, or migration operations finish. Expose the toast host as a polite status region.
    <div id="toasts" class="toasts"></div>

extensions/catpilot-canvas/catpilot-store.mjs:87

  • Onboarding always writes the global config even when CATPILOT_CONFIG or CATPILOT_ROOT selected a different missing config path. In that environment setup reports success, but the next loadConfig() still resolves the missing environment path and the app fails to start. Write to the path returned by resolveConfigPath() so setup and subsequent reads agree.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/catpilot-store.mjs:1131

  • A move migration can partially relocate files before a later item fails; this then leaves the old config active while successfully moved data is no longer at the configured paths. Contrary to the comment above, config and data do drift until the user retries the exact migration. Stage/rollback moves or otherwise make the operation transactional before reporting failure.
    if (errors.length) {
        // Config is left untouched; already-moved files stay where they landed
        // but the source→config mapping is unchanged, so a retry is safe.
        throw Object.assign(new Error(`Migration failed for ${errors.length} item(s); configuration was not changed.`), {

extensions/catpilot-canvas/catpilot-store.mjs:1142

  • All data moves complete before the config is persisted. If this write fails (permissions, disk full, or an I/O error), the function exits with the old config still active while the files have already moved to the new paths. Include config persistence in the migration transaction or roll the moves back on failure.
    const configPath = persistConfig(config);

extensions/catpilot-canvas/catpilot-store.mjs:544

  • This recursively treats every matching basename under the configured root as CatPilot data instead of restricting matches to valid partition paths. With the suggested Obsidian-vault root, an unrelated journal.md, tasks.md, or memos/ elsewhere in the vault is included in timelines/reports and may be moved during a root migration. Enumerate only the supported day/week/month partition layouts before collecting or migrating files.
        if (e.name === targetName && ((isDir && e.isDirectory()) || (!isDir && e.isFile()))) {
            acc.push(full);
        } else if (e.isDirectory() && !e.name.startsWith(".")) {
            // Skip dotfolders (.trash, .git, .obsidian, …) — they are tooling
            // state, not CatPilot partitions.

extensions/catpilot-canvas/catpilot-store.mjs:876

  • The selected reporting period is not applied to tasks: this always loads the current partition and computes all task/completion KPIs from it. For example, a “Last month” report generated this month reports this month's tasks as though they were tracked last month. Load the relevant partition(s) and filter by task lifecycle dates, or stop presenting these task metrics as period-specific until the format records those dates.
    const tasks = parseTasksTable(readFileOrCreate(resolveFilePath("tasks", config)));
    const milestones = parseMilestones(readFileOrCreate(resolveFilePath("milestones", config)));
    const journal = collectJournal(config);
    const { since, until, label } = periodRange(period);

Comment thread extensions/catpilot-canvas/ui/app.js
Comment thread extensions/catpilot-canvas/ui/app.js
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Comment thread extensions/catpilot-canvas/ui/app.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 21, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 13 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (11)

extensions/catpilot-canvas/catpilot-store.mjs:87

  • Onboarding always writes the global config, but config resolution gives CATPILOT_CONFIG and CATPILOT_ROOT precedence. If either variable points to a not-yet-created config, setup reports success while subsequent requests still resolve the missing env-selected path and the app immediately fails as unconfigured. Write to the path returned by resolveConfigPath() so onboarding creates the configuration it will actually load.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/ui/app.js:543

  • Dropping between Overdue and To do cannot change columns because both branches only set status: "Open", while column membership is determined by the unchanged due date. For example, an overdue task dropped on To do immediately renders back under Overdue. Disable those invalid drop targets or update/clear the due date according to an explicit user choice.
                const id = e.dataTransfer.getData("text/plain");
                try {
                    if (col.key === "done") await api(`/api/tasks/${id}/complete`, { method: "POST" });
                    else await api(`/api/tasks/${id}`, { method: "PUT", body: { status: "Open" } });
                    await refreshSummary(); go("tasks");

extensions/catpilot-canvas/catpilot-store.mjs:1075

  • Move mode relies exclusively on renameSync, which throws EXDEV when the new storage root is on another drive or filesystem—a common reason to change storage roots. The repository already handles this case by falling back to copy-and-remove in eng/materialize-plugins.mjs:27-42; use equivalent cross-device handling here so migration to an external volume works.
function moveOrCopyPath(src, dest, isDir, mode) {
    if (!fs.existsSync(src)) return { moved: 0, skipped: 0 };
    if (isDir) {
        ensureDir(dest);

extensions/catpilot-canvas/ui/index.html:71

  • Toast messages are the only feedback for many save, error, and agent operations, but this dynamically updated container is not a live region, so screen-reader users receive no status announcement. Mark it as a polite status region (and consider assertive announcements for errors).
    <div id="toasts" class="toasts"></div>

extensions/catpilot-canvas/ui/app.js:694

  • Each memo card is the only control that opens its content, but it is a non-focusable div with a click handler. Keyboard users cannot reach or activate memo details. Render this as a semantic button/link, or add button semantics, focusability, and Enter/Space handling with a visible focus style.
        memos.forEach((m) => grid.append(el("div", { class: "card note-card hoverable", onclick: () => memoDetail(m.filename) },
            el("h4", { text: m.title }),
            el("div", { class: "note-foot" }, el("span", { class: "tag", text: m.date || "" }), el("span", { class: "muted small", text: "📄 memo" })))));

extensions/catpilot-canvas/ui/app.js:742

  • Learning, growth, and project cards are non-focusable div elements whose details are available only through onclick, leaving keyboard users unable to open the note body. Use a semantic interactive element or provide button role, tabindex, Enter/Space activation, and visible focus styling.
                grid.append(el("div", { class: "card note-card hoverable", onclick: () => domainNoteDetail(domain, n.filename) },
                    el("h4", { text: fm.title || n.filename }),
                    domainChips(domain, fm),
                    el("div", { class: "note-foot" }, el("span", { class: "tag", text: fm.date || "" }))));

extensions/catpilot-canvas/ui/app.js:1003

  • Report cards expose the report reader only through a click handler on a non-focusable div, so keyboard users cannot open generated reports. Make each card a semantic button/link, or add complete keyboard button behavior and a visible focus indicator.
        reports.forEach((r) => grid.append(el("div", { class: "card report-card hoverable", onclick: () => reportDetail(r.filename) },
            el("div", { class: "report-ic", text: r.format === "html" ? "🌐" : "📄" }),
            el("h4", { text: r.title }),
            el("div", { class: "note-foot" }, el("span", { class: "tag", text: r.date || "" }), el("span", { class: "tag", text: r.format || "md" })))));

extensions/catpilot-canvas/catpilot-store.mjs:166

  • This escaping is not compatible with the linked CatPilot storage reader: tanure/cat-copilot/lib/cli-utils.js parses task rows with plain line.split('|'), so a value written as foo \| bar is still split into two cells and shifts the due date, priority, and remaining columns when read by the CLI/MCP surfaces. Either reject/encode these characters in a representation understood by every CatPilot reader or update the shared parser before allowing them here.
function encodeCell(v) {
    return String(v == null ? "" : v).replace(/\r?\n/g, "<br>").replace(/\|/g, "\\|");

extensions/catpilot-canvas/ui/app.js:268

  • The navigation token does not isolate actionsHost: every go() overwrites this global before the awaited view fetch resolves. A stale Tasks/Timeline renderer can therefore append its switcher into a newer navigation's actionStage, which then passes the token check and displays the wrong controls. Pass the local actionStage into the view renderer instead of reading a shared mutable host.
        actionsHost = actionStage;

extensions/catpilot-canvas/catpilot-store.mjs:1122

  • Move mode mutates each source immediately, but failures are only checked after all items and the config is persisted later. If a later item or persistConfig fails, already-renamed files remain solely under the new path while the active config still points to the old path, contradicting the non-destructive migration guarantee and making those records disappear from CatPilot until manual recovery. Stage/copy all data first and commit the config before deleting sources, or roll back every successful move on any failure.
            try {
                const r = moveOrCopyPath(item.from, item.to, item.isDir, migration);
                moved += r.moved;
                skipped += r.skipped;
            } catch (err) {

extensions/catpilot-canvas/catpilot-store.mjs:824

  • listReports() extracts <title>/<h1> for HTML files, but readReport() only searches for a Markdown heading. Opening an HTML report therefore replaces its displayed title with the filename even though the list showed the document title. Apply the same format-specific title extraction here.
    let title = safe;
    const h = content.match(/^#\s+(.+)$/m);
    if (h) title = h[1].trim();
    return { filename: safe, title, content, format: ext === "md" ? "markdown" : "html", date: reportDate(safe) };

Comment thread extensions/catpilot-canvas/catpilot-store.mjs
Copilot AI review requested due to automatic review settings July 21, 2026 09:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 13 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (10)

extensions/catpilot-canvas/extension.mjs:132

  • sendAndWait expects the message options object as its first argument (the other extensions and SDK examples use { prompt }). Passing the raw string means “Generate with Copilot” does not send the requested prompt correctly. Wrap the prompt and keep the timeout as the second argument.
        const ev = await sessionRef.sendAndWait(String(prompt), Number(timeout) || 120000);

extensions/catpilot-canvas/ui/app.js:268

  • The navigation token does not fully isolate view actions because actionsHost remains shared mutable state. If Tasks starts loading, Timeline navigation replaces this variable, and Tasks resolves first, its switcher is appended to Timeline's staging node; the winning view then commits both controls. Pass this navigation's actionStage directly to the renderer (or otherwise bind action writes to token) instead of using the global host.
        actionsHost = actionStage;

extensions/catpilot-canvas/catpilot-store.mjs:87

  • Onboarding always writes the global config even when CATPILOT_CONFIG or CATPILOT_ROOT selected a different missing config path. In that case /api/status continues resolving the nonexistent environment-selected file, so setup reports success but startApp() immediately fails with NOT_CONFIGURED. Write to resolveConfigPath().path; it already falls back to the global path when no override exists.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/catpilot-store.mjs:1051

  • When root and partitioning are changed together, this branch migrates only the current partition and then points the config at the new root. Historical journal/memo/note/report partitions remain under the old root, so timeline and cross-partition summaries can no longer discover them. Either reject combined changes or move every old-root partition while preserving its relative layout before applying the new partitioning mode.
    } else {
        // Partitioning change (re-bucketing history is ambiguous) or same root:
        // operate on the current partition only.
        for (const [type, meta] of Object.entries(from.paths)) {
            items.push(mkItem(type, meta.isDir, meta.path, to.paths[type].path));

extensions/catpilot-canvas/catpilot-store.mjs:1082

  • renameSync fails with EXDEV when the new storage root is on another filesystem or Windows drive, a normal use case for this settings wizard. Directory-backed domains therefore cannot be moved and may leave an already-partial migration behind. Handle EXDEV with copy-then-delete (or use a cross-device move helper) while preserving the no-overwrite behavior.
            else fs.renameSync(s, d);

extensions/catpilot-canvas/catpilot-store.mjs:1090

  • This file-path branch also uses renameSync, so moving tasks, journal, or milestones to a root on another filesystem deterministically fails with EXDEV. Use the same copy-then-delete fallback as the directory branch, without overwriting an existing destination.
    else fs.renameSync(src, dest);

extensions/catpilot-canvas/ui/app.js:694

  • Memo cards are clickable div elements with no role, tab stop, or keyboard activation, and there is no other control for opening a memo. Keyboard users therefore cannot read memo contents. Render a button/link, or add button semantics plus Enter/Space handling.
        memos.forEach((m) => grid.append(el("div", { class: "card note-card hoverable", onclick: () => memoDetail(m.filename) },
            el("h4", { text: m.title }),
            el("div", { class: "note-foot" }, el("span", { class: "tag", text: m.date || "" }), el("span", { class: "muted small", text: "📄 memo" })))));

extensions/catpilot-canvas/ui/app.js:742

  • These learning/growth/project cards are the only way to open note details, but clickable div elements are not keyboard focusable or activatable. Use a semantic button/link or provide role="button", tabindex="0", and Enter/Space handling.
                grid.append(el("div", { class: "card note-card hoverable", onclick: () => domainNoteDetail(domain, n.filename) },
                    el("h4", { text: fm.title || n.filename }),
                    domainChips(domain, fm),
                    el("div", { class: "note-foot" }, el("span", { class: "tag", text: fm.date || "" }))));

extensions/catpilot-canvas/ui/app.js:1003

  • Report cards can only be opened with a pointer because the clickable div has no keyboard semantics. Make each card a button/link or add a tab stop, button role, and Enter/Space activation so keyboard users can access saved reports.
        reports.forEach((r) => grid.append(el("div", { class: "card report-card hoverable", onclick: () => reportDetail(r.filename) },
            el("div", { class: "report-ic", text: r.format === "html" ? "🌐" : "📄" }),
            el("h4", { text: r.title }),
            el("div", { class: "note-foot" }, el("span", { class: "tag", text: r.date || "" }), el("span", { class: "tag", text: r.format || "md" })))));

extensions/catpilot-canvas/ui/index.html:71

  • Toast messages are inserted dynamically but this host is not a live region, so screen readers do not announce success or failure feedback. Mark the container as a polite status/live region (errors can be made assertive separately if desired).
    <div id="toasts" class="toasts"></div>

Comment on lines +1012 to +1015
const del = el("button", { class: "btn btn-danger", text: "Delete", onclick: async () => {
try { await api(`/api/reports/${encodeURIComponent(filename)}`, { method: "DELETE" }); toast("Report deleted", "", "ok"); closeModal(); if (state.view === "reports") go("reports"); }
catch (e) { toast("Error", e.message, "err"); }
} });
Sync the catpilot-canvas extension with the latest CatPilot 0.7.0 release:

- Pomodoro timer with configurable focus / short-break / long-break, a global
  persistent mini-timer dock (pause / resume / stop, per-type colours), end-of-
  session notifications and long-break-after-4-focus break suggestions.
- Tasks board gains Overdue / To do / Blocked / Done columns with a status
  selector, plus a new Month / Week calendar view with per-day priority chips,
  a field chooser, today highlight and the shared detail popup.
- Dashboard Today / Week / Month filter and a focus-analytics section.

Refresh the extension description/keywords (pomodoro, timer, calendar) and
regenerate marketplace.json accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 22, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (4)

extensions/catpilot-canvas/catpilot-store.mjs:1504

  • Move migration is not transactional: each source is renamed immediately, errors are collected, and no completed moves are rolled back. If a later item fails, the config remains pointed at the old root while earlier files now exist only at the new root, despite the stated guarantee that config and data never drift. Use copy-and-verify followed by config commit and source cleanup, or track and roll back completed moves; also handle cross-device rename failures.
                const r = moveOrCopyPath(item.from, item.to, item.isDir, migration);
                moved += r.moved;
                skipped += r.skipped;
            } catch (err) {
                errors.push({ type: item.type, from: item.from, to: item.to, error: String(err && err.message || err) });

extensions/catpilot-canvas/catpilot-store.mjs:89

  • Onboarding always writes the global file, but resolveConfigPath() always prioritizes CATPILOT_CONFIG or CATPILOT_ROOT even when that target does not exist. In that environment setup reports success, then every subsequent API still resolves the missing env path and remains unconfigured. Write the configuration to the path selected by the resolver.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/catpilot-store.mjs:168

  • This escaping is not compatible with the linked CatPilot v0.7.0 parser: lib/cli-utils.js still uses line.split('|') and does not decode \\| or <br>. A task or Pomodoro value containing a pipe/newline written by Canvas is therefore read differently or shifted into the wrong columns by the CLI/MCP, despite the exact-format parity claim. Align the canonical parser/formatter across all clients or reject unsupported cell characters.
function encodeCell(v) {
    return String(v == null ? "" : v).replace(/\r?\n/g, "<br>").replace(/\|/g, "\\|");

extensions/catpilot-canvas/catpilot-store.mjs:1184

  • HTML report titles are extracted in listReports(), but this detail reader only looks for a Markdown # heading. Clicking an HTML report therefore changes the modal/iframe accessible title from the displayed report title to its filename. Apply the existing <title>/<h1> extraction for HTML here too.
    let title = safe;
    const h = content.match(/^#\s+(.+)$/m);
    if (h) title = h[1].trim();
    return { filename: safe, title, content, format: ext === "md" ? "markdown" : "html", date: reportDate(safe) };

Comment on lines +783 to +784
const chip = el("div", { class: `cal-chip pr-${priorityRank(t.priority)}${done ? " done" : ""}`,
dataset: { taskTitle: t.title }, title: t.title, onclick: () => taskDetail(t) });
Comment on lines +71 to +73
<div id="pomo-dock" class="pomo-dock hidden" aria-live="polite"></div>
<!-- Toasts -->
<div id="toasts" class="toasts"></div>
btn(`🍅 Start focus (${min("focus")}m)`, "btn-primary", () => choose("focus")),
btn("Skip", "", () => choose(null)));
}
openModal({
Comment on lines +403 to +405
const started = Date.parse(session.startedAt);
const endedAt = new Date();
const actualMin = Math.max(0, Math.round(((endedAt.getTime() - started) / 1000) / 60));
Comment on lines +475 to +477
if (!active.pausedAt) {
active.pausedAt = new Date().toISOString();
fs.writeFileSync(pomodoroActivePath(config), JSON.stringify(active, null, 2), "utf8");
Comment on lines +1413 to +1416
if (rootChanged && !partChanged) {
// Root-only change: migrate EVERY partition, preserving the relative
// layout, so historical data is not left behind in the old root.
for (const [type, fileName] of Object.entries({ ...DEFAULT_FILES, ...(config.storage.files || {}) })) {
Comment on lines +251 to +258
properties: {
title: { type: "string" },
due: { type: "string", description: "YYYY-MM-DD" },
priority: { type: "string", description: "P0/P1/P2/P3 or High/Med/Low" },
tags: { type: "string" },
context: { type: "string" },
},
},
description: "Visual command center for CatPilot: dashboard, tasks, journal, milestones, memos, learning, growth, projects, timeline, Copilot reports, settings/migration and a help guide — with charts and agent actions.",
inputSchema: {
type: "object",
properties: { view: { type: "string", description: "Optional initial view: dashboard|timeline|tasks|journal|milestones|memos|learning|growth|projects|reports|settings|help" } },
Sync the Tasks board redesign into the extension runtime files:
colour-coded scrollable columns with per-column quick-add, and rebuilt
cards with priority accent bars and clear header/title/footer separation.

Mirrors tanure/cat-copilot v0.8.0.
Copilot AI review requested due to automatic review settings July 23, 2026 08:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (5)

extensions/catpilot-canvas/catpilot-store.mjs:89

  • Onboarding always writes the global config even when CATPILOT_CONFIG or CATPILOT_ROOT makes resolveConfigPath() point elsewhere. In that environment /api/setup reports success, but subsequent configStatus()/loadConfig() still look at the missing environment-selected path, so startup remains unconfigured. Write to the resolved target path instead.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/catpilot-store.mjs:168

  • This escaping format is not understood by CatPilot v0.8.0's canonical lib/cli-utils.js, which parses rows with line.split('|'). For example, a canvas task title containing | is written as \|, but the CLI still splits it into extra columns and shifts due date/priority/tags. This breaks the PR's same-storage interoperability promise; either update all CatPilot surfaces to the same codec or reject/normalize unsupported table delimiters here.
function encodeCell(v) {
    return String(v == null ? "" : v).replace(/\r?\n/g, "<br>").replace(/\|/g, "\\|");

extensions/catpilot-canvas/catpilot-store.mjs:477

  • pausedAt is canvas-only state: CatPilot v0.8.0's canonical lib/pomodoro.js computes elapsed time solely from startedAt and does not inspect pausedAt. While the canvas shows a paused timer, the CLI/MCP timer continues counting down, contradicting the claimed cross-surface synchronization. Add the same pause semantics to the canonical engine before persisting this state, or avoid exposing pause here.
    if (!active.pausedAt) {
        active.pausedAt = new Date().toISOString();
        fs.writeFileSync(pomodoroActivePath(config), JSON.stringify(active, null, 2), "utf8");

extensions/catpilot-canvas/catpilot-store.mjs:1512

  • A failed move is not atomic as claimed: earlier items have already been renamed out of the configured source before this error is thrown, while the config remains pointed at that source. Those domains immediately disappear from CatPilot, and abandoning the retry leaves data stranded. Stage copies and switch only after all succeed, or track successful renames and roll them back on failure.
    if (errors.length) {
        // Config is left untouched; already-moved files stay where they landed
        // but the source→config mapping is unchanged, so a retry is safe.
        throw Object.assign(new Error(`Migration failed for ${errors.length} item(s); configuration was not changed.`), {

extensions/catpilot-canvas/catpilot-store.mjs:1432

  • Changing both root and partitioning takes this else path and migrates only the current partition. After the config switches to the new root, every historical partition left under the old root becomes invisible, and the preview does not list it. Either reject combined changes or enumerate every old-root partition and map it safely into the destination.
    } else {
        // Partitioning change (re-bucketing history is ambiguous) or same root:
        // operate on the current partition only.
        for (const [type, meta] of Object.entries(from.paths)) {
            items.push(mkItem(type, meta.isDir, meta.path, to.paths[type].path));

Comment on lines +1406 to +1409
// Log the finished session, then start the chosen next one (if any).
async function choose(nextType, carryTask) {
try {
await api("/api/pomodoro/complete", { method: "POST", body: {} });
Comment thread extensions/catpilot-canvas/ui/app.js Outdated
Comment on lines +625 to +629
function renderTaskBoard(holder, tasks) {
const cols = [
{ key: "overdue", title: "⏰ Overdue", status: "Open", filter: (t) => t.status.toLowerCase() === "open" && t.dueDate && t.dueDate < todayISO() },
{ key: "open", title: "◻ To do", status: "Open", filter: (t) => t.status.toLowerCase() === "open" && !(t.dueDate && t.dueDate < todayISO()) },
{ key: "blocked", title: "⛔ Blocked", status: "Blocked", filter: (t) => t.status.toLowerCase() === "blocked" },
Comment on lines +1334 to +1336
pomoSyncAccum += 1;
if (pomo.active && !pomo.active.paused) {
pomo.remaining = Math.max(0, pomo.remaining - 1);
Sync the CatPilot canvas UI to cat-copilot v0.8.1:
- Board cards now render the due date as a bold, high-contrast chip (icon +
  date + relative label: Today / Tomorrow / in 3d / 2d overdue).
- Backlog column for open tasks with no due date, with drag-to-clear /
  drag-to-schedule handling.
- Consistent emoji empty-state for every board column.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f970696-ac1b-4e45-b901-9ea6f67f5689
Copilot AI review requested due to automatic review settings July 23, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (6)

extensions/catpilot-canvas/catpilot-store.mjs:89

  • When CATPILOT_CONFIG or CATPILOT_ROOT points to a config that does not exist yet, resolveConfigPath() keeps selecting that location, but onboarding writes only ~/.catpilot/config.json. The returned status therefore remains unconfigured and the newly started UI immediately fails. Write the initial config to the path selected by resolveConfigPath() so environment-based setups can be bootstrapped.
    fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
    fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
    config.__baseDir = configBaseDir(GLOBAL_CONFIG_PATH);
    config.__configPath = GLOBAL_CONFIG_PATH;

extensions/catpilot-canvas/catpilot-store.mjs:1471

  • Move migrations fail when the old and new storage roots are on different filesystems or Windows drives because renameSync throws EXDEV. Storage roots are arbitrary user-selected paths, so this is a normal migration scenario. Handle EXDEV with copy-then-remove (and apply the same handling to the directory-entry rename above) before reporting the move as failed.
    if (mode === "copy") fs.copyFileSync(src, dest);
    else fs.renameSync(src, dest);

extensions/catpilot-canvas/catpilot-store.mjs:405

  • Completing or cancelling while paused counts the entire paused interval as active time because endedAt is used even though the timer is frozen at pausedAt. This inflates actualMin and all focus analytics. Use pausedAt as the effective elapsed-time endpoint while retaining the real completion timestamp in ended.
    const endedAt = new Date();
    const actualMin = Math.max(0, Math.round(((endedAt.getTime() - started) / 1000) / 60));

extensions/catpilot-canvas/ui/index.html:71

  • The dock is rebuilt every second as the countdown changes, so placing aria-live="polite" on the whole container can make a screen reader announce timer updates continuously, including repeated controls and labels. Keep the ticking time outside a live region and announce only meaningful state changes or expiry.
    <div id="pomo-dock" class="pomo-dock hidden" aria-live="polite"></div>

extensions/catpilot-canvas/ui/index.html:73

  • Validation failures and asynchronous operation results are shown only through this toast container, but it is not a live region, so screen-reader users receive no feedback. Mark it as an atomic polite status region (or use assertive announcements for errors).
    <div id="toasts" class="toasts"></div>

extensions/catpilot-canvas/catpilot-store.mjs:915

  • This recursive basename search treats every matching file or directory anywhere in the storage root as a CatPilot partition. Because onboarding explicitly allows an Obsidian vault and names such as projects, reports, memos, and journal.md are common there, timeline aggregation can ingest unrelated content and a root migration can move unrelated user files. Enumerate only paths matching the configured day/week/month partition layout (while honoring custom relative mappings) instead of scanning the whole vault by basename.
    return walkFind(storageRoot, fileName, isDir, 0, []);

Comment on lines +59 to +61
<button class="icon-btn agent-icon" id="agent-btn" title="Ask Copilot">✨</button>
<button class="icon-btn" id="refresh-btn" title="Refresh">⟳</button>
<button class="icon-btn" id="theme-btn" title="Toggle theme">🌙</button>
The canvas reads and writes the **same** config-driven storage as the CatPilot CLI, agent, skills, and MCP server, so every surface stays in sync.

- **Dashboard** — hero greeting, summary cards, an open-tasks-by-priority donut, a last-3-days activity chart, a focus list and a recent-activity feed, plus a **Today / Week / Month** filter and a **🍅 Focus** analytics section (focus sessions, focus time, completed, abandoned).
- **Tasks** — switch between **list (table)**, **board (kanban)** and **📅 calendar** views. The board has **Overdue · To do · Blocked · Done** columns with drag-and-drop and a **status selector** on create/edit; the calendar offers **Month / Week** layouts with per-day priority-coloured task chips, a field chooser, today highlight and the shared detail popup. Complete inline, edit/save locally, and open a detail popup from any view.
root.innerHTML = "";
const cards = [
{ icon: "🐱", title: "What is this canvas?", body: "A visual command center for **CatPilot**. Everything you see reads and writes the *same files* CatPilot's CLI, agent and MCP server use — so edits here show up everywhere. Nothing is duplicated." },
{ icon: "🧭", title: "Navigation", body: "The sidebar covers every CatPilot domain: **Tasks, Journal, Milestones, Memos, Learning, Growth, Projects**. Each has add buttons, inline edit and detail popups. **Tasks** offers a table and a kanban board with **Overdue · To do · Blocked · Done** columns, a status selector on create/edit, and **All / Today / 7 days** due-date filters." },
- Fix clipped Kanban board card footers: cards are flex: 0 0 auto so the
  To do column no longer shrinks cards below content height and clips the
  due-date pill + tag chips when many cards are present.
- Add a muted "No due date" chip for undated open board cards.
- Harden per-request cache-busting for canvas assets.
Copilot AI review requested due to automatic review settings July 23, 2026 11:05
@tanure

tanure commented Jul 23, 2026

Copy link
Copy Markdown
Author

Synced CatPilot canvas extension to v0.8.2: fixes clipped Kanban board card footers (cards are now flex: 0 0 auto so the To do column no longer shrinks cards below content height and clips the due-date pill + tags), adds a No due date chip for undated open cards, and hardens per-request asset cache-busting. Plugin validation passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Files not reviewed (1)
  • extensions/catpilot-canvas/package-lock.json: Generated file
Comments suppressed due to low confidence (7)

extensions/catpilot-canvas/catpilot-store.mjs:398

  • This history loader reads only the current partition. Consequently, with the default monthly partitioning, “Last month” and “All time” productivity reports omit previous files, and rolling week/month dashboard stats are incomplete at partition boundaries. Aggregate pomodoro.md files across partitions for read/report operations while keeping appends targeted at the current partition.
function loadPomodoroHistory(config) {
    const filePath = resolveFilePath("pomodoro", config);
    const content = readFileOrCreate(filePath, POMODORO_HISTORY_HEADER + formatPomodoroTable([]));
    return { filePath, sessions: parsePomodoroTable(content) };

extensions/catpilot-canvas/catpilot-store.mjs:477

  • Pausing is not synchronized with the advertised CatPilot v0.8.1 surfaces: upstream lib/pomodoro.js ignores pausedAt and derives remaining time solely from startedAt, so the CLI/MCP timer continues counting down while this canvas shows it paused. Use a pause representation understood by every CatPilot surface (or add support upstream) before exposing pause/resume here.
    if (!active.pausedAt) {
        active.pausedAt = new Date().toISOString();
        fs.writeFileSync(pomodoroActivePath(config), JSON.stringify(active, null, 2), "utf8");

extensions/catpilot-canvas/catpilot-store.mjs:405

  • Completing or stopping a session while it is paused counts the entire paused interval as focus time because actualMin always uses the current time. Freeze the elapsed calculation at pausedAt while retaining the real completion timestamp.
    const started = Date.parse(session.startedAt);
    const endedAt = new Date();
    const actualMin = Math.max(0, Math.round(((endedAt.getTime() - started) / 1000) / 60));

extensions/catpilot-canvas/catpilot-store.mjs:915

  • This recursively treats every matching name anywhere under the storage root as a CatPilot partition. Since onboarding explicitly supports an Obsidian vault, unrelated journal.md files or generic projects/reports folders can be included in analytics and even moved by root migration. Restrict discovery to the configured day/week/month partition layouts instead of scanning arbitrary descendants.
function allPartitionPaths(config, type) {
    const base = config.__baseDir || process.cwd();
    const storageRoot = path.resolve(base, config.storage.root);
    const fileName = (config.storage.files && config.storage.files[type]) || DEFAULT_FILES[type];
    if (!fileName) return [];
    const isDir = !/\.[a-z]+$/i.test(fileName);
    return walkFind(storageRoot, fileName, isDir, 0, []);

extensions/catpilot-canvas/catpilot-store.mjs:1418

  • Root migration enumerates only partitioned DEFAULT_FILES, but the active timer lives at <storageRoot>/pomodoro-active.json. Changing roots during a running session updates the config without moving that file, so the timer immediately disappears from every surface and is stranded in the old root. Include this root-level state in the plan/apply flow or block root changes while a timer is active.
        for (const [type, fileName] of Object.entries({ ...DEFAULT_FILES, ...(config.storage.files || {}) })) {
            const isDir = !/\.[a-z]+$/i.test(fileName);
            const srcs = allPartitionPaths(config, type);

extensions/catpilot-canvas/catpilot-store.mjs:1472

  • Move migrations fail whenever the new storage root is on another filesystem or drive because renameSync throws EXDEV; changing roots is precisely where that is common. Fall back to copy-then-delete for EXDEV (including the directory-entry branch above) so cross-volume migrations work.
    if (fs.existsSync(dest)) return { moved: 0, skipped: 1 };
    ensureDir(path.dirname(dest));
    if (mode === "copy") fs.copyFileSync(src, dest);
    else fs.renameSync(src, dest);
    return { moved: 1, skipped: 0 };

extensions/catpilot-canvas/catpilot-store.mjs:168

  • The backslash pipe encoding is not compatible with CatPilot v0.8.1’s shared parser, which uses line.split("|") and does not decode escapes. A title/context containing | therefore round-trips in this canvas but is split into extra columns by the CLI/MCP, contradicting the same-storage guarantee. Use the shared format’s accepted sanitization or update every CatPilot parser together.
// Markdown-table cells are pipe/newline delimited, so any user value that
// contains `|` or a line break must be escaped on write and restored on read,
// otherwise it spills into extra cells/rows and corrupts the table.
function encodeCell(v) {
    return String(v == null ? "" : v).replace(/\r?\n/g, "<br>").replace(/\|/g, "\\|");

Comment on lines +707 to +709
if (t.dueDate) {
const state = done ? "" : overdue ? " overdue" : (t.dueDate === todayISO() ? " today" : "");
foot.append(el("span", { class: `bc-due${state}`, title: `Due ${t.dueDate}`, text: `📅 ${t.dueDate}` }));
Comment on lines +573 to +577
function renderTaskList(holder, tasks) {
const open = tasks.filter((t) => t.status.toLowerCase() === "open");
const blocked = tasks.filter((t) => t.status.toLowerCase() === "blocked");
const done = tasks.filter((t) => t.status.toLowerCase() === "done");
const ordered = [...open, ...blocked, ...done];
Comment on lines +1428 to +1433
} else {
// Partitioning change (re-bucketing history is ambiguous) or same root:
// operate on the current partition only.
for (const [type, meta] of Object.entries(from.paths)) {
items.push(mkItem(type, meta.isDir, meta.path, to.paths[type].path));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

canvas-extension PR touches canvas extensions new-submission PR adds at least one new contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants