diff --git a/src/resources/extensions/sf/commands-schedule.js b/src/resources/extensions/sf/commands-schedule.js index 72c83d97e..3ccf89066 100644 --- a/src/resources/extensions/sf/commands-schedule.js +++ b/src/resources/extensions/sf/commands-schedule.js @@ -81,12 +81,7 @@ function _formatTable(rows) { function _findEntry(store, scope, idPrefix) { const entries = store.readEntries(scope); - const match = entries.find((e) => e.id.startsWith(idPrefix)); - if (!match) { - const exact = entries.find((e) => e.id === idPrefix); - if (exact) return { entry: exact, entries }; - } - return { entry: match, entries }; + return { entry: entries.find((e) => e.id.startsWith(idPrefix)) ?? null, entries }; } // ─── Subcommands ──────────────────────────────────────────────────────────── @@ -206,7 +201,7 @@ async function listItems(args, ctx) { } else if (showAll) { entries = store.readEntries(scope); } else { - entries = store.readEntries(scope).filter((e) => e.status === "pending"); + entries = store.readEntries(scope).filter((e) => e.status === "pending" || e.status === "snoozed"); } entries.sort((a, b) => new Date(a.due_at) - new Date(b.due_at)); @@ -310,12 +305,14 @@ async function snoozeItem(args, ctx) { return; } + const now = new Date().toISOString(); const newDue = new Date(new Date(entry.due_at).getTime() + offsetMs).toISOString(); const updated = { ...entry, - status: "snoozed", + status: "pending", due_at: newDue, - created_at: new Date().toISOString(), + created_at: now, + snoozed_at: now, }; store.appendEntry("project", updated); ctx.ui.notify(`Snoozed: ${entry.id}\nNew due: ${newDue}`, "success"); diff --git a/src/resources/extensions/sf/schedule/schedule-store.js b/src/resources/extensions/sf/schedule/schedule-store.js index e6d92240f..bc518b218 100644 --- a/src/resources/extensions/sf/schedule/schedule-store.js +++ b/src/resources/extensions/sf/schedule/schedule-store.js @@ -165,7 +165,7 @@ function _findDue(basePath, scope, now) { const entries = _readEntries(basePath, scope); const nowMs = _toTimestamp(now); return entries - .filter((e) => e.status === "pending" && _toTimestamp(e.due_at) <= nowMs) + .filter((e) => (e.status === "pending" || e.status === "snoozed") && _toTimestamp(e.due_at) <= nowMs) .sort((a, b) => _toTimestamp(a.due_at) - _toTimestamp(b.due_at)); } @@ -187,7 +187,7 @@ function _findUpcoming(basePath, scope, now, windowDays) { return entries .filter( (e) => - e.status === "pending" && + (e.status === "pending" || e.status === "snoozed") && _toTimestamp(e.due_at) > nowMs && _toTimestamp(e.due_at) <= cutoff, ) diff --git a/src/resources/extensions/sf/schedule/schedule-types.js b/src/resources/extensions/sf/schedule/schedule-types.js index 8f68cc356..9fa35065b 100644 --- a/src/resources/extensions/sf/schedule/schedule-types.js +++ b/src/resources/extensions/sf/schedule/schedule-types.js @@ -72,6 +72,7 @@ * @property {SchedulePayload} payload Kind-specific data * @property {ScheduleCreatedBy} created_by Who created the entry * @property {boolean} [auto_dispatch] If true and kind='reminder', surface as dispatch input in auto-mode when due. Defaults false. + * @property {string} [snoozed_at] ISO-8601 timestamp; set when the entry was last snoozed */ // ─── Guards ───────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/sf/tests/commands-schedule.test.mjs b/src/resources/extensions/sf/tests/commands-schedule.test.mjs index da8ff7212..9772c9808 100644 --- a/src/resources/extensions/sf/tests/commands-schedule.test.mjs +++ b/src/resources/extensions/sf/tests/commands-schedule.test.mjs @@ -154,6 +154,22 @@ describe("handleSchedule", () => { assert.equal(ctx2.notifications[0].type, "success"); assert.ok(ctx2.notifications[0].msg.includes("Snoozed")); }); + + it("snoozed item still appears in list", async () => { + const ctx1 = mockCtx(); + await handleSchedule("add --in 1d item that will be snoozed", ctx1); + const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1]; + assert.ok(id); + + await handleSchedule(`snooze ${id.slice(0, 8)} --by 2d`, mockCtx()); + + const ctx3 = mockCtx(); + await handleSchedule("list", ctx3); + assert.ok( + ctx3.notifications[0].msg.includes("item that will be snoozed"), + "snoozed item must still appear in default list", + ); + }); }); describe("run", () => { diff --git a/src/resources/extensions/sf/tests/schedule-store.test.mjs b/src/resources/extensions/sf/tests/schedule-store.test.mjs index 9c86367d8..5a8acbaa3 100644 --- a/src/resources/extensions/sf/tests/schedule-store.test.mjs +++ b/src/resources/extensions/sf/tests/schedule-store.test.mjs @@ -187,6 +187,18 @@ describe("schedule-store", () => { assert.equal(due[0].id, past.id); }); + it("returns snoozed entries whose new due_at has passed", () => { + const snoozed = makeEntry({ + due_at: "2024-01-01T00:00:00.000Z", + status: "snoozed", + snoozed_at: "2023-12-01T00:00:00.000Z", + }); + store.appendEntry("project", snoozed); + const due = store.findDue("project", "2024-06-01T00:00:00.000Z"); + assert.equal(due.length, 1); + assert.equal(due[0].id, snoozed.id); + }); + it("sorts results by due_at ascending", () => { const e1 = makeEntry({ due_at: "2024-01-02T00:00:00.000Z" }); const e2 = makeEntry({ due_at: "2024-01-01T00:00:00.000Z" }); @@ -226,7 +238,7 @@ describe("schedule-store", () => { assert.equal(upcoming[0].id, soon.id); }); - it("excludes non-pending entries", () => { + it("excludes done/cancelled entries", () => { const soonDone = makeEntry({ due_at: "2024-01-02T00:00:00.000Z", status: "done", @@ -241,6 +253,23 @@ describe("schedule-store", () => { assert.equal(upcoming.length, 0); }); + it("includes snoozed entries within the window", () => { + const snoozed = makeEntry({ + due_at: "2024-01-02T00:00:00.000Z", + status: "snoozed", + snoozed_at: "2023-12-01T00:00:00.000Z", + }); + store.appendEntry("project", snoozed); + + const upcoming = store.findUpcoming( + "project", + "2024-01-01T00:00:00.000Z", + 7, + ); + assert.equal(upcoming.length, 1); + assert.equal(upcoming[0].id, snoozed.id); + }); + it("sorts results by due_at ascending", () => { const e1 = makeEntry({ due_at: "2024-01-03T00:00:00.000Z" }); const e2 = makeEntry({ due_at: "2024-01-02T00:00:00.000Z" });